From 07365e518330289149dd2135424c49fad19f401d Mon Sep 17 00:00:00 2001 From: Keith Pope Date: Fri, 5 Aug 2016 10:29:09 +0100 Subject: Add config option to project to allow custom .gitlab-ci.yml location --- .../projects/pipelines_settings_controller.rb | 2 +- app/models/ci/pipeline.rb | 10 +++- app/models/project.rb | 13 ++++++ .../projects/pipelines_settings/show.html.haml | 7 +++ ...20160804142904_add_ci_config_file_to_project.rb | 18 ++++++++ db/schema.rb | 1 + doc/api/projects.md | 3 ++ doc/user/project/pipelines/settings.md | 54 ++++++++++++++++++++++ lib/api/entities.rb | 1 + lib/api/projects.rb | 6 +++ spec/models/ci/pipeline_spec.rb | 30 ++++++++++++ spec/models/project_spec.rb | 1 + spec/requests/api/projects_spec.rb | 4 +- 13 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20160804142904_add_ci_config_file_to_project.rb create mode 100644 doc/user/project/pipelines/settings.md diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 9136633b87a..d23418a9aa3 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -30,7 +30,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController def update_params params.require(:project).permit( :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, - :public_builds + :public_builds, :ci_config_file ) end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2cf9892edc5..e6cd71a7bf2 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -218,14 +218,22 @@ module Ci return @ci_yaml_file if defined?(@ci_yaml_file) @ci_yaml_file ||= begin - blob = project.repository.blob_at(sha, '.gitlab-ci.yml') + blob = project.repository.blob_at(sha, ci_yaml_file_path) blob.load_all_data!(project.repository) blob.data rescue + self.yaml_errors = 'Failed to load CI config file' nil end end + def ci_yaml_file_path + return '.gitlab-ci.yml' if project.ci_config_file.blank? + return project.ci_config_file if File.extname(project.ci_config_file.to_s) == '.yml' + + File.join(project.ci_config_file || '', '.gitlab-ci.yml') + end + def environments builds.where.not(environment: nil).success.pluck(:environment).uniq end diff --git a/app/models/project.rb b/app/models/project.rb index 88e4bd14860..272c89798b6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -154,6 +154,11 @@ class Project < ActiveRecord::Base # Validations validates :creator, presence: true, on: :create validates :description, length: { maximum: 2000 }, allow_blank: true + validates :ci_config_file, + format: { without: Gitlab::Regex.directory_traversal_regex, + message: Gitlab::Regex.directory_traversal_regex_message }, + length: { maximum: 255 }, + allow_blank: true validates :name, presence: true, length: { within: 0..255 }, @@ -182,6 +187,7 @@ class Project < ActiveRecord::Base add_authentication_token_field :runners_token before_save :ensure_runners_token + before_validation :clean_ci_config_file mount_uploader :avatar, AvatarUploader @@ -986,6 +992,7 @@ class Project < ActiveRecord::Base visibility_level: visibility_level, path_with_namespace: path_with_namespace, default_branch: default_branch, + ci_config_file: ci_config_file } # Backward compatibility @@ -1349,4 +1356,10 @@ class Project < ActiveRecord::Base shared_projects.any? end + + def clean_ci_config_file + return unless self.ci_config_file + # Cleanup path removing leading/trailing slashes + self.ci_config_file = ci_config_file.gsub(/^\/+|\/+$/, '') + end end diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 8c7222bfe3d..25a991cdbfc 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -32,6 +32,13 @@ = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light' = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' %p.help-block per build in minutes + .form-group + = f.label :ci_config_file, 'Custom CI Config File', class: 'label-light' + = f.text_field :ci_config_file, class: 'form-control', placeholder: '.gitlab-ci.yml' + %p.help-block + Optionally specify the location of your CI config file E.g. my/path or my/path/.my-config.yml. + Default is to use '.gitlab-ci.yml' in the repository root. + .form-group = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light' .input-group diff --git a/db/migrate/20160804142904_add_ci_config_file_to_project.rb b/db/migrate/20160804142904_add_ci_config_file_to_project.rb new file mode 100644 index 00000000000..4b9860c5f74 --- /dev/null +++ b/db/migrate/20160804142904_add_ci_config_file_to_project.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddCiConfigFileToProject < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_column :projects, :ci_config_file, :string + end + + def down + remove_column :projects, :ci_config_file + end +end diff --git a/db/schema.rb b/db/schema.rb index 56da70b3c02..26883a72f1f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -889,6 +889,7 @@ ActiveRecord::Schema.define(version: 20160926145521) do t.boolean "has_external_wiki" t.boolean "lfs_enabled" t.text "description_html" + t.string "ci_config_file" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree diff --git a/doc/api/projects.md b/doc/api/projects.md index 27436a052da..af229326eee 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -604,6 +604,7 @@ Parameters: | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | +| `ci_config_file` | boolean | no | The relative path to the CI config file (E.g. my/path or my/path/.gitlab-ci.yml or my/path/my-config.yml) | ### Create project for user @@ -636,6 +637,7 @@ Parameters: | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | +| `ci_config_file` | boolean | no | The relative path to the CI config file (E.g. my/path or my/path/.gitlab-ci.yml or my/path/my-config.yml) | ### Edit project @@ -667,6 +669,7 @@ Parameters: | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | +| `ci_config_file` | boolean | no | The relative path to the CI config file (E.g. my/path or my/path/.gitlab-ci.yml or my/path/my-config.yml) | On success, method returns 200 with the updated project. If parameters are invalid, 400 is returned. diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md new file mode 100644 index 00000000000..272ee71bfed --- /dev/null +++ b/doc/user/project/pipelines/settings.md @@ -0,0 +1,54 @@ +# Project Pipeline Settings + +This section covers project level pipeline settings. + +## Clone vs Fetch + +You can select to either `git fetch` or `git clone` your project before +each build. Fetching is faster as you are only pulling recent updates +but cloning has the advantage of giving you a clean project. + +## Timeout + +This is the total time in minutes that a build is allowed to run. The +default is 222 minutes. + +## Custom CI Config File + +> - [Introduced][ce-15041] in GitLab 8.13. + +By default we look for the `.gitlab-ci.yml` file in the projects root +directory. If you require a different location **within** the repository +you can set a custom filepath that will be used to lookup the config file, +this filepath should be **relative** to the root. + +Here are some valid examples: + +> * .gitlab-ci.yml +> * .my-custom-file.yml +> * my/path/.gitlab-ci.yml +> * my/path/.my-custom-file.yml + +## Test Coverage Parsing + +As each testing framework has different output, you need to specify a +regex to extract the summary code coverage information from your test +commands output. The regex will be applied to the `STDOUT` of your command. + +Here are some examples of popular testing frameworks/languages: + +> * Simplecov (Ruby) - `\(\d+.\d+\%\) covered` +> * pytest-cov (Python) - `\d+\%\s*$` +> * phpunit --coverage-text --colors=never (PHP) - `^\s*Lines:\s*\d+.\d+\%` +> * gcovr (C/C++) - `^TOTAL.*\s+(\d+\%)$` +> * tap --coverage-report=text-summary (Node.js) - `^Statements\s*:\s*([^%]+)` + + +## Public Pipelines + +You can select if the pipeline should be publicly accessible or not. + +## Runners Token + +This is a secure token that is used to checkout the project from the +Gitlab instance. This should be a cryptographically secure random hash. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index feaa0c213bf..c84a7ef19db 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -96,6 +96,7 @@ module API expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:user]) && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds + expose :ci_config_file expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links.all, options) end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index c24e8e8bd9b..291e7b689bf 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -118,12 +118,14 @@ module API # public_builds (optional) # lfs_enabled (optional) # request_access_enabled (optional) - Allow users to request member access + # ci_config_file (optional) # Example Request # POST /projects post do required_attributes! [:name] attrs = attributes_for_keys [:builds_enabled, :container_registry_enabled, + :ci_config_file, :description, :import_url, :issues_enabled, @@ -173,12 +175,14 @@ module API # public_builds (optional) # lfs_enabled (optional) # request_access_enabled (optional) - Allow users to request member access + # ci_config_file (optional) # Example Request # POST /projects/user/:user_id post "user/:user_id" do authenticated_as_admin! user = User.find(params[:user_id]) attrs = attributes_for_keys [:builds_enabled, + :ci_config_file, :default_branch, :description, :import_url, @@ -256,11 +260,13 @@ module API # visibility_level (optional) - visibility level of a project # public_builds (optional) # lfs_enabled (optional) + # ci_config_file (optional) # Example Request # PUT /projects/:id put ':id' do attrs = attributes_for_keys [:builds_enabled, :container_registry_enabled, + :ci_config_file, :default_branch, :description, :issues_enabled, diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 550a890797e..8d774666d2b 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -403,6 +403,36 @@ describe Ci::Pipeline, models: true do end end + describe 'yaml config file resolution' do + let(:project) { FactoryGirl.build(:project) } + + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + it 'uses custom ci config file path when present' do + allow(project).to receive(:ci_config_file) { 'custom/path' } + expect(pipeline.ci_yaml_file_path).to eq('custom/path/.gitlab-ci.yml') + end + it 'uses root when custom path is nil' do + allow(project).to receive(:ci_config_file) { nil } + expect(pipeline.ci_yaml_file_path).to eq('.gitlab-ci.yml') + end + it 'uses root when custom path is empty' do + allow(project).to receive(:ci_config_file) { '' } + expect(pipeline.ci_yaml_file_path).to eq('.gitlab-ci.yml') + end + it 'allows custom filename' do + allow(project).to receive(:ci_config_file) { 'custom/path/.my-config.yml' } + expect(pipeline.ci_yaml_file_path).to eq('custom/path/.my-config.yml') + end + it 'custom filename must be yml' do + allow(project).to receive(:ci_config_file) { 'custom/path/.my-config.cnf' } + expect(pipeline.ci_yaml_file_path).to eq('custom/path/.my-config.cnf/.gitlab-ci.yml') + end + it 'reports error if the file is not found' do + pipeline.ci_yaml_file + expect(pipeline.yaml_errors).to eq('Failed to load CI config file') + end + end + describe '#execute_hooks' do let!(:build_a) { create_build('a', 0) } let!(:build_b) { create_build('b', 1) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8aadfcb439b..363b5ff1913 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -118,6 +118,7 @@ describe Project, models: true do it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) } it { is_expected.to validate_length_of(:path).is_within(0..255) } it { is_expected.to validate_length_of(:description).is_within(0..2000) } + it { is_expected.to validate_length_of(:ci_config_file).is_within(0..255) } it { is_expected.to validate_presence_of(:creator) } it { is_expected.to validate_presence_of(:namespace) } it { is_expected.to validate_presence_of(:repository_storage) } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 5f19638b460..80e5deb7f92 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -256,7 +256,8 @@ describe API::API, api: true do merge_requests_enabled: false, wiki_enabled: false, only_allow_merge_if_build_succeeds: false, - request_access_enabled: true + request_access_enabled: true, + ci_config_file: 'a/custom/path' }) post api('/projects', user), project @@ -503,6 +504,7 @@ describe API::API, api: true do expect(json_response['star_count']).to be_present expect(json_response['forks_count']).to be_present expect(json_response['public_builds']).to be_present + expect(json_response['ci_config_file']).to be_nil expect(json_response['shared_with_groups']).to be_an Array expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) -- cgit v1.2.1 From b2592f01da50367f1c6a2acf4ef701d3a7661d36 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Dec 2016 14:39:39 +1100 Subject: Store capybara-screenshots folder as artifacts for RSpec and Spinach --- .gitlab-ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e522d47d19d..475346dcd34 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -69,9 +69,11 @@ stages: - knapsack rspec "--color --format documentation" artifacts: expire_in: 31d + when: always paths: - - knapsack/ - coverage/ + - knapsack/ + - tmp/capybara/ .spinach-knapsack: &spinach-knapsack stage: test @@ -87,9 +89,11 @@ stages: - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: expire_in: 31d + when: always paths: - - knapsack/ - coverage/ + - knapsack/ + - tmp/capybara/ # Prepare and merge knapsack tests -- cgit v1.2.1 From a61c19778180a8316321ac9ca6f84de76a6c23b3 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Dec 2016 14:45:34 +1100 Subject: Don't disable capybara-screenshot in CI environment --- features/support/capybara.rb | 9 +++------ spec/support/capybara.rb | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/features/support/capybara.rb b/features/support/capybara.rb index 47372df152d..0b6a0981a3c 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -1,5 +1,6 @@ require 'spinach/capybara' require 'capybara/poltergeist' +require 'capybara-screenshot/spinach' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15 @@ -20,12 +21,8 @@ end Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = false -unless ENV['CI'] || ENV['CI_SERVER'] - require 'capybara-screenshot/spinach' - - # Keep only the screenshots generated from the last failing test suite - Capybara::Screenshot.prune_strategy = :keep_last_run -end +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run Spinach.hooks.before_run do TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER'] diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 16d5f2bf0b8..ebb1f30f090 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,6 +1,7 @@ require 'capybara/rails' require 'capybara/rspec' require 'capybara/poltergeist' +require 'capybara-screenshot/rspec' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 @@ -21,12 +22,8 @@ end Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = true -unless ENV['CI'] || ENV['CI_SERVER'] - require 'capybara-screenshot/rspec' - - # Keep only the screenshots generated from the last failing test suite - Capybara::Screenshot.prune_strategy = :keep_last_run -end +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run RSpec.configure do |config| config.before(:suite) do -- cgit v1.2.1 From 7a399b7061d4d374f01ddaa75ae7fba53ca4cb6b Mon Sep 17 00:00:00 2001 From: Matthieu Tardy Date: Mon, 9 Jan 2017 07:38:13 +0100 Subject: Strip reference prefixes on branch creation Signed-off-by: Matthieu Tardy --- ...ranch-names-with-reference-prefixes-results-in-buggy-branches.yml | 4 ++++ lib/gitlab/git_ref_validator.rb | 3 +++ spec/lib/git_ref_validator_spec.rb | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml diff --git a/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml new file mode 100644 index 00000000000..e82cbf00cfb --- /dev/null +++ b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml @@ -0,0 +1,4 @@ +--- +title: Strip reference prefixes on branch creation +merge_request: 8498 +author: Matthieu Tardy diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb index 4d83d8e72a8..0e87ee30c98 100644 --- a/lib/gitlab/git_ref_validator.rb +++ b/lib/gitlab/git_ref_validator.rb @@ -5,6 +5,9 @@ module Gitlab # # Returns true for a valid reference name, false otherwise def validate(ref_name) + return false if ref_name.start_with?('refs/heads/') + return false if ref_name.start_with?('refs/remotes/') + Gitlab::Utils.system_silent( %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name})) end diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb index dc57e94f193..cc8daa535d6 100644 --- a/spec/lib/git_ref_validator_spec.rb +++ b/spec/lib/git_ref_validator_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::GitRefValidator, lib: true do it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('feature/refs/heads/foo')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey } @@ -17,4 +18,8 @@ describe Gitlab::GitRefValidator, lib: true do it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/heads/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/remotes/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/heads/feature')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/remotes/origin')).to be_falsey } end -- cgit v1.2.1 From c252c03401881fd7dbf7fab984285c402eb31d5f Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Sun, 9 Oct 2016 23:40:58 +0100 Subject: Added raven and raven-vue plugin, updated gon_helper with data needed for raven and created raven_config, required by application.js Added is_production to define sentry environment Removed as much jQuery as possible Added public_sentry_dsn application_settings helper method Use URI module instead of regex for public dsn Removed raven-vue and load raven on if sentry is enabled Add load_script spec added raven_config spec added class_spec_helper and tests added sentry_helper spec added feature spec --- app/assets/javascripts/application.js | 1 + .../javascripts/lib/utils/load_script.js.es6 | 26 + app/assets/javascripts/raven_config.js.es6 | 66 + app/helpers/sentry_helper.rb | 8 + config/application.rb | 1 + lib/gitlab/gon_helper.rb | 6 + spec/features/raven_js_spec.rb | 24 + spec/helpers/sentry_helper_spec.rb | 15 + spec/javascripts/class_spec_helper.js.es6 | 10 + spec/javascripts/class_spec_helper_spec.js.es6 | 35 + spec/javascripts/lib/utils/load_script_spec.js.es6 | 95 + spec/javascripts/raven_config_spec.js.es6 | 142 ++ spec/javascripts/spec_helper.js | 1 + vendor/assets/javascripts/raven.js | 2547 ++++++++++++++++++++ 14 files changed, 2977 insertions(+) create mode 100644 app/assets/javascripts/lib/utils/load_script.js.es6 create mode 100644 app/assets/javascripts/raven_config.js.es6 create mode 100644 spec/features/raven_js_spec.rb create mode 100644 spec/helpers/sentry_helper_spec.rb create mode 100644 spec/javascripts/class_spec_helper.js.es6 create mode 100644 spec/javascripts/class_spec_helper_spec.js.es6 create mode 100644 spec/javascripts/lib/utils/load_script_spec.js.es6 create mode 100644 spec/javascripts/raven_config_spec.js.es6 create mode 100644 vendor/assets/javascripts/raven.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index f0615481ed2..94902e560a8 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -62,6 +62,7 @@ /*= require_directory . */ /*= require fuzzaldrin-plus */ /*= require es6-promise.auto */ +/*= require raven_config */ (function () { document.addEventListener('page:fetch', function () { diff --git a/app/assets/javascripts/lib/utils/load_script.js.es6 b/app/assets/javascripts/lib/utils/load_script.js.es6 new file mode 100644 index 00000000000..351d96530ed --- /dev/null +++ b/app/assets/javascripts/lib/utils/load_script.js.es6 @@ -0,0 +1,26 @@ +(() => { + const global = window.gl || (window.gl = {}); + + class LoadScript { + static load(source, id = '') { + if (!source) return Promise.reject('source url must be defined'); + if (id && document.querySelector(`#${id}`)) return Promise.reject('script id already exists'); + return new Promise((resolve, reject) => this.appendScript(source, id, resolve, reject)); + } + + static appendScript(source, id, resolve, reject) { + const scriptElement = document.createElement('script'); + scriptElement.type = 'text/javascript'; + if (id) scriptElement.id = id; + scriptElement.onload = resolve; + scriptElement.onerror = reject; + scriptElement.src = source; + + document.body.appendChild(scriptElement); + } + } + + global.LoadScript = LoadScript; + + return global.LoadScript; +})(); diff --git a/app/assets/javascripts/raven_config.js.es6 b/app/assets/javascripts/raven_config.js.es6 new file mode 100644 index 00000000000..e15eeb9f9cd --- /dev/null +++ b/app/assets/javascripts/raven_config.js.es6 @@ -0,0 +1,66 @@ +/* global Raven */ + +/*= require lib/utils/load_script */ + +(() => { + const global = window.gl || (window.gl = {}); + + class RavenConfig { + static init(options = {}) { + this.options = options; + if (!this.options.sentryDsn || !this.options.ravenAssetUrl) return Promise.reject('sentry dsn and raven asset url is required'); + return global.LoadScript.load(this.options.ravenAssetUrl, 'raven-js') + .then(() => { + this.configure(); + this.bindRavenErrors(); + if (this.options.currentUserId) this.setUser(); + }); + } + + static configure() { + Raven.config(this.options.sentryDsn, { + whitelistUrls: this.options.whitelistUrls, + environment: this.options.isProduction ? 'production' : 'development', + }).install(); + } + + static setUser() { + Raven.setUserContext({ + id: this.options.currentUserId, + }); + } + + static bindRavenErrors() { + $(document).on('ajaxError.raven', this.handleRavenErrors); + } + + static handleRavenErrors(event, req, config, err) { + const error = err || req.statusText; + Raven.captureMessage(error, { + extra: { + type: config.type, + url: config.url, + data: config.data, + status: req.status, + response: req.responseText.substring(0, 100), + error, + event, + }, + }); + } + } + + global.RavenConfig = RavenConfig; + + document.addEventListener('DOMContentLoaded', () => { + if (!window.gon) return; + + global.RavenConfig.init({ + sentryDsn: gon.sentry_dsn, + ravenAssetUrl: gon.raven_asset_url, + currentUserId: gon.current_user_id, + whitelistUrls: [gon.gitlab_url], + isProduction: gon.is_production, + }).catch($.noop); + }); +})(); diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb index 3d255df66a0..19de38ac52d 100644 --- a/app/helpers/sentry_helper.rb +++ b/app/helpers/sentry_helper.rb @@ -6,4 +6,12 @@ module SentryHelper def sentry_context Gitlab::Sentry.context(current_user) end + + def sentry_dsn_public + sentry_dsn = ApplicationSetting.current.sentry_dsn + return unless sentry_dsn + uri = URI.parse(sentry_dsn) + uri.password = nil + uri.to_s + end end diff --git a/config/application.rb b/config/application.rb index f00e58a36ca..8af6eccf3fe 100644 --- a/config/application.rb +++ b/config/application.rb @@ -92,6 +92,7 @@ module Gitlab config.assets.precompile << "katex.css" config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" + config.assets.precompile << "raven.js" config.assets.precompile << "graphs/graphs_bundle.js" config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "network/network_bundle.js" diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index b8a5ac907a4..70ffb68c9ab 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -1,3 +1,5 @@ +include SentryHelper + module Gitlab module GonHelper def add_gon_variables @@ -10,6 +12,10 @@ module Gitlab gon.award_menu_url = emojis_path gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css') gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js') + gon.sentry_dsn = sentry_dsn_public if sentry_enabled? + gon.raven_asset_url = ActionController::Base.helpers.asset_path('raven.js') if sentry_enabled? + gon.gitlab_url = Gitlab.config.gitlab.url + gon.is_production = Rails.env.production? if current_user gon.current_user_id = current_user.id diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb new file mode 100644 index 00000000000..e64da1e3a97 --- /dev/null +++ b/spec/features/raven_js_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +feature 'RavenJS', feature: true, js: true do + let(:raven_path) { '/raven.js' } + + it 'should not load raven if sentry is disabled' do + visit new_user_session_path + + expect(has_requested_raven).to eq(false) + end + + it 'should load raven if sentry is enabled' do + allow_any_instance_of(ApplicationController).to receive_messages(sentry_dsn_public: 'https://mock:sentry@dsn/path', + sentry_enabled?: true) + + visit new_user_session_path + + expect(has_requested_raven).to eq(true) + end + + def has_requested_raven + page.driver.network_traffic.one? {|request| request.url.end_with?(raven_path)} + end +end diff --git a/spec/helpers/sentry_helper_spec.rb b/spec/helpers/sentry_helper_spec.rb new file mode 100644 index 00000000000..35ecf9355e1 --- /dev/null +++ b/spec/helpers/sentry_helper_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe SentryHelper do + describe '#sentry_dsn_public' do + it 'returns nil if no sentry_dsn is set' do + allow(ApplicationSetting.current).to receive(:sentry_dsn).and_return(nil) + expect(helper.sentry_dsn_public).to eq(nil) + end + + it 'returns the uri string with no password if sentry_dsn is set' do + allow(ApplicationSetting.current).to receive(:sentry_dsn).and_return('https://test:dsn@host/path') + expect(helper.sentry_dsn_public).to eq('https://test@host/path') + end + end +end diff --git a/spec/javascripts/class_spec_helper.js.es6 b/spec/javascripts/class_spec_helper.js.es6 new file mode 100644 index 00000000000..3a04e170924 --- /dev/null +++ b/spec/javascripts/class_spec_helper.js.es6 @@ -0,0 +1,10 @@ +/* eslint-disable no-unused-vars */ + +class ClassSpecHelper { + static itShouldBeAStaticMethod(base, method) { + return it('should be a static method', () => { + expect(base[method]).toBeDefined(); + expect(base.prototype[method]).toBeUndefined(); + }); + } +} diff --git a/spec/javascripts/class_spec_helper_spec.js.es6 b/spec/javascripts/class_spec_helper_spec.js.es6 new file mode 100644 index 00000000000..d1155f1bd1e --- /dev/null +++ b/spec/javascripts/class_spec_helper_spec.js.es6 @@ -0,0 +1,35 @@ +/* global ClassSpecHelper */ +//= require ./class_spec_helper + +describe('ClassSpecHelper', () => { + describe('.itShouldBeAStaticMethod', function () { + beforeEach(() => { + class TestClass { + instanceMethod() { this.prop = 'val'; } + static staticMethod() {} + } + + this.TestClass = TestClass; + }); + + ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod'); + + it('should have a defined spec', () => { + expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method'); + }); + + it('should pass for a static method', () => { + const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod'); + expect(spec.status()).toBe('passed'); + }); + + it('should fail for an instance method', (done) => { + const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod'); + spec.resultCallback = (result) => { + expect(result.status).toBe('failed'); + done(); + }; + spec.execute(); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/load_script_spec.js.es6 b/spec/javascripts/lib/utils/load_script_spec.js.es6 new file mode 100644 index 00000000000..52c53327695 --- /dev/null +++ b/spec/javascripts/lib/utils/load_script_spec.js.es6 @@ -0,0 +1,95 @@ +/* global ClassSpecHelper */ + +/*= require lib/utils/load_script */ +/*= require class_spec_helper */ + +describe('LoadScript', () => { + const global = window.gl || (window.gl = {}); + const LoadScript = global.LoadScript; + + it('should be defined in the global scope', () => { + expect(LoadScript).toBeDefined(); + }); + + describe('.load', () => { + ClassSpecHelper.itShouldBeAStaticMethod(LoadScript, 'load'); + + it('should reject if no source argument is provided', () => { + spyOn(Promise, 'reject'); + LoadScript.load(); + expect(Promise.reject).toHaveBeenCalledWith('source url must be defined'); + }); + + it('should reject if the script id already exists', () => { + spyOn(Promise, 'reject'); + spyOn(document, 'querySelector').and.returnValue({}); + LoadScript.load('src.js', 'src-id'); + + expect(Promise.reject).toHaveBeenCalledWith('script id already exists'); + }); + + it('should return a promise on completion', () => { + expect(LoadScript.load('src.js')).toEqual(jasmine.any(Promise)); + }); + + it('should call appendScript when the promise is constructed', () => { + spyOn(LoadScript, 'appendScript'); + LoadScript.load('src.js', 'src-id'); + + expect(LoadScript.appendScript).toHaveBeenCalledWith('src.js', 'src-id', jasmine.any(Promise.resolve.constructor), jasmine.any(Promise.reject.constructor)); + }); + }); + + describe('.appendScript', () => { + beforeEach(() => { + spyOn(document.body, 'appendChild'); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(LoadScript, 'appendScript'); + + describe('when called', () => { + let mockScriptTag; + + beforeEach(() => { + mockScriptTag = {}; + spyOn(document, 'createElement').and.returnValue(mockScriptTag); + LoadScript.appendScript('src.js', 'src-id', () => {}, () => {}); + }); + + it('should create a script tag', () => { + expect(document.createElement).toHaveBeenCalledWith('script'); + }); + + it('should set the MIME type', () => { + expect(mockScriptTag.type).toBe('text/javascript'); + }); + + it('should set the script id', () => { + expect(mockScriptTag.id).toBe('src-id'); + }); + + it('should set an onload handler', () => { + expect(mockScriptTag.onload).toEqual(jasmine.any(Function)); + }); + + it('should set an onerror handler', () => { + expect(mockScriptTag.onerror).toEqual(jasmine.any(Function)); + }); + + it('should set the src attribute', () => { + expect(mockScriptTag.src).toBe('src.js'); + }); + + it('should append the script tag to the body element', () => { + expect(document.body.appendChild).toHaveBeenCalledWith(mockScriptTag); + }); + }); + + it('should not set the script id if no id is provided', () => { + const mockScriptTag = {}; + spyOn(document, 'createElement').and.returnValue(mockScriptTag); + LoadScript.appendScript('src.js', undefined); + expect(mockScriptTag.id).toBeUndefined(); + }); + }); +}); diff --git a/spec/javascripts/raven_config_spec.js.es6 b/spec/javascripts/raven_config_spec.js.es6 new file mode 100644 index 00000000000..25df2cec75f --- /dev/null +++ b/spec/javascripts/raven_config_spec.js.es6 @@ -0,0 +1,142 @@ +/* global ClassSpecHelper */ + +/*= require raven */ +/*= require lib/utils/load_script */ +/*= require raven_config */ +/*= require class_spec_helper */ + +describe('RavenConfig', () => { + const global = window.gl || (window.gl = {}); + const RavenConfig = global.RavenConfig; + + it('should be defined in the global scope', () => { + expect(RavenConfig).toBeDefined(); + }); + + describe('.init', () => { + beforeEach(() => { + spyOn(global.LoadScript, 'load').and.callThrough(); + spyOn(document, 'querySelector').and.returnValue(undefined); + spyOn(RavenConfig, 'configure'); + spyOn(RavenConfig, 'bindRavenErrors'); + spyOn(RavenConfig, 'setUser'); + spyOn(Promise, 'reject'); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'init'); + + describe('when called', () => { + let options; + let initPromise; + + beforeEach(() => { + options = { + sentryDsn: '//sentryDsn', + ravenAssetUrl: '//ravenAssetUrl', + currentUserId: 1, + whitelistUrls: ['//gitlabUrl'], + isProduction: true, + }; + initPromise = RavenConfig.init(options); + }); + + it('should set the options property', () => { + expect(RavenConfig.options).toEqual(options); + }); + + it('should load a #raven-js script with the raven asset URL', () => { + expect(global.LoadScript.load).toHaveBeenCalledWith(options.ravenAssetUrl, 'raven-js'); + }); + + it('should return a promise', () => { + expect(initPromise).toEqual(jasmine.any(Promise)); + }); + + it('should call the configure method', () => { + initPromise.then(() => { + expect(RavenConfig.configure).toHaveBeenCalled(); + }); + }); + + it('should call the error bindings method', () => { + initPromise.then(() => { + expect(RavenConfig.bindRavenErrors).toHaveBeenCalled(); + }); + }); + + it('should call setUser', () => { + initPromise.then(() => { + expect(RavenConfig.setUser).toHaveBeenCalled(); + }); + }); + }); + + it('should not call setUser if there is no current user ID', () => { + RavenConfig.init({ + sentryDsn: '//sentryDsn', + ravenAssetUrl: '//ravenAssetUrl', + currentUserId: undefined, + whitelistUrls: ['//gitlabUrl'], + isProduction: true, + }); + + expect(RavenConfig.setUser).not.toHaveBeenCalled(); + }); + + it('should reject if there is no Sentry DSN', () => { + RavenConfig.init({ + sentryDsn: undefined, + ravenAssetUrl: '//ravenAssetUrl', + currentUserId: 1, + whitelistUrls: ['//gitlabUrl'], + isProduction: true, + }); + + expect(Promise.reject).toHaveBeenCalledWith('sentry dsn and raven asset url is required'); + }); + + it('should reject if there is no Raven asset URL', () => { + RavenConfig.init({ + sentryDsn: '//sentryDsn', + ravenAssetUrl: undefined, + currentUserId: 1, + whitelistUrls: ['//gitlabUrl'], + isProduction: true, + }); + + expect(Promise.reject).toHaveBeenCalledWith('sentry dsn and raven asset url is required'); + }); + }); + + describe('.configure', () => { + ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'configure'); + + describe('when called', () => { + beforeEach(() => {}); + }); + }); + + describe('.setUser', () => { + ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'setUser'); + + describe('when called', () => { + beforeEach(() => {}); + }); + }); + + describe('.bindRavenErrors', () => { + ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'bindRavenErrors'); + + describe('when called', () => { + beforeEach(() => {}); + }); + }); + + describe('.handleRavenErrors', () => { + ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'handleRavenErrors'); + + describe('when called', () => { + beforeEach(() => {}); + }); + }); +}); diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index f8e3aca29fa..ee6e6279ac9 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -11,6 +11,7 @@ /*= require jquery.turbolinks */ /*= require bootstrap */ /*= require underscore */ +/*= require es6-promise.auto */ // Teaspoon includes some support files, but you can use anything from your own // support path too. diff --git a/vendor/assets/javascripts/raven.js b/vendor/assets/javascripts/raven.js new file mode 100644 index 00000000000..ba416b26be1 --- /dev/null +++ b/vendor/assets/javascripts/raven.js @@ -0,0 +1,2547 @@ +/*! Raven.js 3.9.1 (7bbae7d) | github.com/getsentry/raven-js */ + +/* + * Includes TraceKit + * https://github.com/getsentry/TraceKit + * + * Copyright 2016 Matt Robenolt and other contributors + * Released under the BSD license + * https://github.com/getsentry/raven-js/blob/master/LICENSE + * + */ + +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Raven = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { + var thisPos = stack.indexOf(this) + ~thisPos ? stack.splice(thisPos + 1) : stack.push(this) + ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key) + if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value) + } + else stack.push(value) + + return replacer == null ? value : replacer.call(this, key, value) + } +} + +},{}],2:[function(_dereq_,module,exports){ +'use strict'; + +function RavenConfigError(message) { + this.name = 'RavenConfigError'; + this.message = message; +} +RavenConfigError.prototype = new Error(); +RavenConfigError.prototype.constructor = RavenConfigError; + +module.exports = RavenConfigError; + +},{}],3:[function(_dereq_,module,exports){ +'use strict'; + +var wrapMethod = function(console, level, callback) { + var originalConsoleLevel = console[level]; + var originalConsole = console; + + if (!(level in console)) { + return; + } + + var sentryLevel = level === 'warn' + ? 'warning' + : level; + + console[level] = function () { + var args = [].slice.call(arguments); + + var msg = '' + args.join(' '); + var data = {level: sentryLevel, logger: 'console', extra: {'arguments': args}}; + callback && callback(msg, data); + + // this fails for some browsers. :( + if (originalConsoleLevel) { + // IE9 doesn't allow calling apply on console functions directly + // See: https://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function#answer-5473193 + Function.prototype.apply.call( + originalConsoleLevel, + originalConsole, + args + ); + } + }; +}; + +module.exports = { + wrapMethod: wrapMethod +}; + +},{}],4:[function(_dereq_,module,exports){ +(function (global){ +/*global XDomainRequest:false, __DEV__:false*/ +'use strict'; + +var TraceKit = _dereq_(6); +var RavenConfigError = _dereq_(2); +var stringify = _dereq_(1); + +var wrapConsoleMethod = _dereq_(3).wrapMethod; + +var dsnKeys = 'source protocol user pass host port path'.split(' '), + dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/; + +function now() { + return +new Date(); +} + +// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785) +var _window = typeof window !== 'undefined' ? window + : typeof global !== 'undefined' ? global + : typeof self !== 'undefined' ? self + : {}; +var _document = _window.document; + +// First, check for JSON support +// If there is no JSON, we no-op the core features of Raven +// since JSON is required to encode the payload +function Raven() { + this._hasJSON = !!(typeof JSON === 'object' && JSON.stringify); + // Raven can run in contexts where there's no document (react-native) + this._hasDocument = !isUndefined(_document); + this._lastCapturedException = null; + this._lastEventId = null; + this._globalServer = null; + this._globalKey = null; + this._globalProject = null; + this._globalContext = {}; + this._globalOptions = { + logger: 'javascript', + ignoreErrors: [], + ignoreUrls: [], + whitelistUrls: [], + includePaths: [], + crossOrigin: 'anonymous', + collectWindowErrors: true, + maxMessageLength: 0, + stackTraceLimit: 50, + autoBreadcrumbs: true + }; + this._ignoreOnError = 0; + this._isRavenInstalled = false; + this._originalErrorStackTraceLimit = Error.stackTraceLimit; + // capture references to window.console *and* all its methods first + // before the console plugin has a chance to monkey patch + this._originalConsole = _window.console || {}; + this._originalConsoleMethods = {}; + this._plugins = []; + this._startTime = now(); + this._wrappedBuiltIns = []; + this._breadcrumbs = []; + this._lastCapturedEvent = null; + this._keypressTimeout; + this._location = _window.location; + this._lastHref = this._location && this._location.href; + + for (var method in this._originalConsole) { // eslint-disable-line guard-for-in + this._originalConsoleMethods[method] = this._originalConsole[method]; + } +} + +/* + * The core Raven singleton + * + * @this {Raven} + */ + +Raven.prototype = { + // Hardcode version string so that raven source can be loaded directly via + // webpack (using a build step causes webpack #1617). Grunt verifies that + // this value matches package.json during build. + // See: https://github.com/getsentry/raven-js/issues/465 + VERSION: '3.9.1', + + debug: false, + + TraceKit: TraceKit, // alias to TraceKit + + /* + * Configure Raven with a DSN and extra options + * + * @param {string} dsn The public Sentry DSN + * @param {object} options Optional set of of global options [optional] + * @return {Raven} + */ + config: function(dsn, options) { + var self = this; + + if (self._globalServer) { + this._logDebug('error', 'Error: Raven has already been configured'); + return self; + } + if (!dsn) return self; + + var globalOptions = self._globalOptions; + + // merge in options + if (options) { + each(options, function(key, value){ + // tags and extra are special and need to be put into context + if (key === 'tags' || key === 'extra' || key === 'user') { + self._globalContext[key] = value; + } else { + globalOptions[key] = value; + } + }); + } + + self.setDSN(dsn); + + // "Script error." is hard coded into browsers for errors that it can't read. + // this is the result of a script being pulled in from an external domain and CORS. + globalOptions.ignoreErrors.push(/^Script error\.?$/); + globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/); + + // join regexp rules into one big rule + globalOptions.ignoreErrors = joinRegExp(globalOptions.ignoreErrors); + globalOptions.ignoreUrls = globalOptions.ignoreUrls.length ? joinRegExp(globalOptions.ignoreUrls) : false; + globalOptions.whitelistUrls = globalOptions.whitelistUrls.length ? joinRegExp(globalOptions.whitelistUrls) : false; + globalOptions.includePaths = joinRegExp(globalOptions.includePaths); + globalOptions.maxBreadcrumbs = Math.max(0, Math.min(globalOptions.maxBreadcrumbs || 100, 100)); // default and hard limit is 100 + + var autoBreadcrumbDefaults = { + xhr: true, + console: true, + dom: true, + location: true + }; + + var autoBreadcrumbs = globalOptions.autoBreadcrumbs; + if ({}.toString.call(autoBreadcrumbs) === '[object Object]') { + autoBreadcrumbs = objectMerge(autoBreadcrumbDefaults, autoBreadcrumbs); + } else if (autoBreadcrumbs !== false) { + autoBreadcrumbs = autoBreadcrumbDefaults; + } + globalOptions.autoBreadcrumbs = autoBreadcrumbs; + + TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors; + + // return for chaining + return self; + }, + + /* + * Installs a global window.onerror error handler + * to capture and report uncaught exceptions. + * At this point, install() is required to be called due + * to the way TraceKit is set up. + * + * @return {Raven} + */ + install: function() { + var self = this; + if (self.isSetup() && !self._isRavenInstalled) { + TraceKit.report.subscribe(function () { + self._handleOnErrorStackInfo.apply(self, arguments); + }); + self._instrumentTryCatch(); + if (self._globalOptions.autoBreadcrumbs) + self._instrumentBreadcrumbs(); + + // Install all of the plugins + self._drainPlugins(); + + self._isRavenInstalled = true; + } + + Error.stackTraceLimit = self._globalOptions.stackTraceLimit; + return this; + }, + + /* + * Set the DSN (can be called multiple time unlike config) + * + * @param {string} dsn The public Sentry DSN + */ + setDSN: function(dsn) { + var self = this, + uri = self._parseDSN(dsn), + lastSlash = uri.path.lastIndexOf('/'), + path = uri.path.substr(1, lastSlash); + + self._dsn = dsn; + self._globalKey = uri.user; + self._globalSecret = uri.pass && uri.pass.substr(1); + self._globalProject = uri.path.substr(lastSlash + 1); + + self._globalServer = self._getGlobalServer(uri); + + self._globalEndpoint = self._globalServer + + '/' + path + 'api/' + self._globalProject + '/store/'; + }, + + /* + * Wrap code within a context so Raven can capture errors + * reliably across domains that is executed immediately. + * + * @param {object} options A specific set of options for this context [optional] + * @param {function} func The callback to be immediately executed within the context + * @param {array} args An array of arguments to be called with the callback [optional] + */ + context: function(options, func, args) { + if (isFunction(options)) { + args = func || []; + func = options; + options = undefined; + } + + return this.wrap(options, func).apply(this, args); + }, + + /* + * Wrap code within a context and returns back a new function to be executed + * + * @param {object} options A specific set of options for this context [optional] + * @param {function} func The function to be wrapped in a new context + * @param {function} func A function to call before the try/catch wrapper [optional, private] + * @return {function} The newly wrapped functions with a context + */ + wrap: function(options, func, _before) { + var self = this; + // 1 argument has been passed, and it's not a function + // so just return it + if (isUndefined(func) && !isFunction(options)) { + return options; + } + + // options is optional + if (isFunction(options)) { + func = options; + options = undefined; + } + + // At this point, we've passed along 2 arguments, and the second one + // is not a function either, so we'll just return the second argument. + if (!isFunction(func)) { + return func; + } + + // We don't wanna wrap it twice! + try { + if (func.__raven__) { + return func; + } + + // If this has already been wrapped in the past, return that + if (func.__raven_wrapper__ ){ + return func.__raven_wrapper__ ; + } + } catch (e) { + // Just accessing custom props in some Selenium environments + // can cause a "Permission denied" exception (see raven-js#495). + // Bail on wrapping and return the function as-is (defers to window.onerror). + return func; + } + + function wrapped() { + var args = [], i = arguments.length, + deep = !options || options && options.deep !== false; + + if (_before && isFunction(_before)) { + _before.apply(this, arguments); + } + + // Recursively wrap all of a function's arguments that are + // functions themselves. + while(i--) args[i] = deep ? self.wrap(options, arguments[i]) : arguments[i]; + + try { + return func.apply(this, args); + } catch(e) { + self._ignoreNextOnError(); + self.captureException(e, options); + throw e; + } + } + + // copy over properties of the old function + for (var property in func) { + if (hasKey(func, property)) { + wrapped[property] = func[property]; + } + } + wrapped.prototype = func.prototype; + + func.__raven_wrapper__ = wrapped; + // Signal that this function has been wrapped already + // for both debugging and to prevent it to being wrapped twice + wrapped.__raven__ = true; + wrapped.__inner__ = func; + + return wrapped; + }, + + /* + * Uninstalls the global error handler. + * + * @return {Raven} + */ + uninstall: function() { + TraceKit.report.uninstall(); + + this._restoreBuiltIns(); + + Error.stackTraceLimit = this._originalErrorStackTraceLimit; + this._isRavenInstalled = false; + + return this; + }, + + /* + * Manually capture an exception and send it over to Sentry + * + * @param {error} ex An exception to be logged + * @param {object} options A specific set of options for this error [optional] + * @return {Raven} + */ + captureException: function(ex, options) { + // If not an Error is passed through, recall as a message instead + if (!isError(ex)) { + return this.captureMessage(ex, objectMerge({ + trimHeadFrames: 1, + stacktrace: true // if we fall back to captureMessage, default to attempting a new trace + }, options)); + } + + // Store the raw exception object for potential debugging and introspection + this._lastCapturedException = ex; + + // TraceKit.report will re-raise any exception passed to it, + // which means you have to wrap it in try/catch. Instead, we + // can wrap it here and only re-raise if TraceKit.report + // raises an exception different from the one we asked to + // report on. + try { + var stack = TraceKit.computeStackTrace(ex); + this._handleStackInfo(stack, options); + } catch(ex1) { + if(ex !== ex1) { + throw ex1; + } + } + + return this; + }, + + /* + * Manually send a message to Sentry + * + * @param {string} msg A plain message to be captured in Sentry + * @param {object} options A specific set of options for this message [optional] + * @return {Raven} + */ + captureMessage: function(msg, options) { + // config() automagically converts ignoreErrors from a list to a RegExp so we need to test for an + // early call; we'll error on the side of logging anything called before configuration since it's + // probably something you should see: + if (!!this._globalOptions.ignoreErrors.test && this._globalOptions.ignoreErrors.test(msg)) { + return; + } + + options = options || {}; + + var data = objectMerge({ + message: msg + '' // Make sure it's actually a string + }, options); + + if (this._globalOptions.stacktrace || (options && options.stacktrace)) { + var ex; + // create a stack trace from this point; just trim + // off extra frames so they don't include this function call (or + // earlier Raven.js library fn calls) + try { + throw new Error(msg); + } catch (ex1) { + ex = ex1; + } + + // null exception name so `Error` isn't prefixed to msg + ex.name = null; + + options = objectMerge({ + // fingerprint on msg, not stack trace (legacy behavior, could be + // revisited) + fingerprint: msg, + trimHeadFrames: (options.trimHeadFrames || 0) + 1 + }, options); + + var stack = TraceKit.computeStackTrace(ex); + var frames = this._prepareFrames(stack, options); + data.stacktrace = { + // Sentry expects frames oldest to newest + frames: frames.reverse() + } + } + + // Fire away! + this._send(data); + + return this; + }, + + captureBreadcrumb: function (obj) { + var crumb = objectMerge({ + timestamp: now() / 1000 + }, obj); + + if (isFunction(this._globalOptions.breadcrumbCallback)) { + var result = this._globalOptions.breadcrumbCallback(crumb); + + if (isObject(result) && !isEmptyObject(result)) { + crumb = result; + } else if (result === false) { + return this; + } + } + + this._breadcrumbs.push(crumb); + if (this._breadcrumbs.length > this._globalOptions.maxBreadcrumbs) { + this._breadcrumbs.shift(); + } + return this; + }, + + addPlugin: function(plugin /*arg1, arg2, ... argN*/) { + var pluginArgs = [].slice.call(arguments, 1); + + this._plugins.push([plugin, pluginArgs]); + if (this._isRavenInstalled) { + this._drainPlugins(); + } + + return this; + }, + + /* + * Set/clear a user to be sent along with the payload. + * + * @param {object} user An object representing user data [optional] + * @return {Raven} + */ + setUserContext: function(user) { + // Intentionally do not merge here since that's an unexpected behavior. + this._globalContext.user = user; + + return this; + }, + + /* + * Merge extra attributes to be sent along with the payload. + * + * @param {object} extra An object representing extra data [optional] + * @return {Raven} + */ + setExtraContext: function(extra) { + this._mergeContext('extra', extra); + + return this; + }, + + /* + * Merge tags to be sent along with the payload. + * + * @param {object} tags An object representing tags [optional] + * @return {Raven} + */ + setTagsContext: function(tags) { + this._mergeContext('tags', tags); + + return this; + }, + + /* + * Clear all of the context. + * + * @return {Raven} + */ + clearContext: function() { + this._globalContext = {}; + + return this; + }, + + /* + * Get a copy of the current context. This cannot be mutated. + * + * @return {object} copy of context + */ + getContext: function() { + // lol javascript + return JSON.parse(stringify(this._globalContext)); + }, + + + /* + * Set environment of application + * + * @param {string} environment Typically something like 'production'. + * @return {Raven} + */ + setEnvironment: function(environment) { + this._globalOptions.environment = environment; + + return this; + }, + + /* + * Set release version of application + * + * @param {string} release Typically something like a git SHA to identify version + * @return {Raven} + */ + setRelease: function(release) { + this._globalOptions.release = release; + + return this; + }, + + /* + * Set the dataCallback option + * + * @param {function} callback The callback to run which allows the + * data blob to be mutated before sending + * @return {Raven} + */ + setDataCallback: function(callback) { + var original = this._globalOptions.dataCallback; + this._globalOptions.dataCallback = isFunction(callback) + ? function (data) { return callback(data, original); } + : callback; + + return this; + }, + + /* + * Set the breadcrumbCallback option + * + * @param {function} callback The callback to run which allows filtering + * or mutating breadcrumbs + * @return {Raven} + */ + setBreadcrumbCallback: function(callback) { + var original = this._globalOptions.breadcrumbCallback; + this._globalOptions.breadcrumbCallback = isFunction(callback) + ? function (data) { return callback(data, original); } + : callback; + + return this; + }, + + /* + * Set the shouldSendCallback option + * + * @param {function} callback The callback to run which allows + * introspecting the blob before sending + * @return {Raven} + */ + setShouldSendCallback: function(callback) { + var original = this._globalOptions.shouldSendCallback; + this._globalOptions.shouldSendCallback = isFunction(callback) + ? function (data) { return callback(data, original); } + : callback; + + return this; + }, + + /** + * Override the default HTTP transport mechanism that transmits data + * to the Sentry server. + * + * @param {function} transport Function invoked instead of the default + * `makeRequest` handler. + * + * @return {Raven} + */ + setTransport: function(transport) { + this._globalOptions.transport = transport; + + return this; + }, + + /* + * Get the latest raw exception that was captured by Raven. + * + * @return {error} + */ + lastException: function() { + return this._lastCapturedException; + }, + + /* + * Get the last event id + * + * @return {string} + */ + lastEventId: function() { + return this._lastEventId; + }, + + /* + * Determine if Raven is setup and ready to go. + * + * @return {boolean} + */ + isSetup: function() { + if (!this._hasJSON) return false; // needs JSON support + if (!this._globalServer) { + if (!this.ravenNotConfiguredError) { + this.ravenNotConfiguredError = true; + this._logDebug('error', 'Error: Raven has not been configured.'); + } + return false; + } + return true; + }, + + afterLoad: function () { + // TODO: remove window dependence? + + // Attempt to initialize Raven on load + var RavenConfig = _window.RavenConfig; + if (RavenConfig) { + this.config(RavenConfig.dsn, RavenConfig.config).install(); + } + }, + + showReportDialog: function (options) { + if (!_document) // doesn't work without a document (React native) + return; + + options = options || {}; + + var lastEventId = options.eventId || this.lastEventId(); + if (!lastEventId) { + throw new RavenConfigError('Missing eventId'); + } + + var dsn = options.dsn || this._dsn; + if (!dsn) { + throw new RavenConfigError('Missing DSN'); + } + + var encode = encodeURIComponent; + var qs = ''; + qs += '?eventId=' + encode(lastEventId); + qs += '&dsn=' + encode(dsn); + + var user = options.user || this._globalContext.user; + if (user) { + if (user.name) qs += '&name=' + encode(user.name); + if (user.email) qs += '&email=' + encode(user.email); + } + + var globalServer = this._getGlobalServer(this._parseDSN(dsn)); + + var script = _document.createElement('script'); + script.async = true; + script.src = globalServer + '/api/embed/error-page/' + qs; + (_document.head || _document.body).appendChild(script); + }, + + /**** Private functions ****/ + _ignoreNextOnError: function () { + var self = this; + this._ignoreOnError += 1; + setTimeout(function () { + // onerror should trigger before setTimeout + self._ignoreOnError -= 1; + }); + }, + + _triggerEvent: function(eventType, options) { + // NOTE: `event` is a native browser thing, so let's avoid conflicting wiht it + var evt, key; + + if (!this._hasDocument) + return; + + options = options || {}; + + eventType = 'raven' + eventType.substr(0,1).toUpperCase() + eventType.substr(1); + + if (_document.createEvent) { + evt = _document.createEvent('HTMLEvents'); + evt.initEvent(eventType, true, true); + } else { + evt = _document.createEventObject(); + evt.eventType = eventType; + } + + for (key in options) if (hasKey(options, key)) { + evt[key] = options[key]; + } + + if (_document.createEvent) { + // IE9 if standards + _document.dispatchEvent(evt); + } else { + // IE8 regardless of Quirks or Standards + // IE9 if quirks + try { + _document.fireEvent('on' + evt.eventType.toLowerCase(), evt); + } catch(e) { + // Do nothing + } + } + }, + + /** + * Wraps addEventListener to capture UI breadcrumbs + * @param evtName the event name (e.g. "click") + * @returns {Function} + * @private + */ + _breadcrumbEventHandler: function(evtName) { + var self = this; + return function (evt) { + // reset keypress timeout; e.g. triggering a 'click' after + // a 'keypress' will reset the keypress debounce so that a new + // set of keypresses can be recorded + self._keypressTimeout = null; + + // It's possible this handler might trigger multiple times for the same + // event (e.g. event propagation through node ancestors). Ignore if we've + // already captured the event. + if (self._lastCapturedEvent === evt) + return; + + self._lastCapturedEvent = evt; + var elem = evt.target; + + var target; + + // try/catch htmlTreeAsString because it's particularly complicated, and + // just accessing the DOM incorrectly can throw an exception in some circumstances. + try { + target = htmlTreeAsString(elem); + } catch (e) { + target = ''; + } + + self.captureBreadcrumb({ + category: 'ui.' + evtName, // e.g. ui.click, ui.input + message: target + }); + }; + }, + + /** + * Wraps addEventListener to capture keypress UI events + * @returns {Function} + * @private + */ + _keypressEventHandler: function() { + var self = this, + debounceDuration = 1000; // milliseconds + + // TODO: if somehow user switches keypress target before + // debounce timeout is triggered, we will only capture + // a single breadcrumb from the FIRST target (acceptable?) + return function (evt) { + var target = evt.target, + tagName = target && target.tagName; + + // only consider keypress events on actual input elements + // this will disregard keypresses targeting body (e.g. tabbing + // through elements, hotkeys, etc) + if (!tagName || tagName !== 'INPUT' && tagName !== 'TEXTAREA' && !target.isContentEditable) + return; + + // record first keypress in a series, but ignore subsequent + // keypresses until debounce clears + var timeout = self._keypressTimeout; + if (!timeout) { + self._breadcrumbEventHandler('input')(evt); + } + clearTimeout(timeout); + self._keypressTimeout = setTimeout(function () { + self._keypressTimeout = null; + }, debounceDuration); + }; + }, + + /** + * Captures a breadcrumb of type "navigation", normalizing input URLs + * @param to the originating URL + * @param from the target URL + * @private + */ + _captureUrlChange: function(from, to) { + var parsedLoc = parseUrl(this._location.href); + var parsedTo = parseUrl(to); + var parsedFrom = parseUrl(from); + + // because onpopstate only tells you the "new" (to) value of location.href, and + // not the previous (from) value, we need to track the value of the current URL + // state ourselves + this._lastHref = to; + + // Use only the path component of the URL if the URL matches the current + // document (almost all the time when using pushState) + if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host) + to = parsedTo.relative; + if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host) + from = parsedFrom.relative; + + this.captureBreadcrumb({ + category: 'navigation', + data: { + to: to, + from: from + } + }); + }, + + /** + * Install any queued plugins + */ + _instrumentTryCatch: function() { + var self = this; + + var wrappedBuiltIns = self._wrappedBuiltIns; + + function wrapTimeFn(orig) { + return function (fn, t) { // preserve arity + // Make a copy of the arguments to prevent deoptimization + // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments + var args = new Array(arguments.length); + for(var i = 0; i < args.length; ++i) { + args[i] = arguments[i]; + } + var originalCallback = args[0]; + if (isFunction(originalCallback)) { + args[0] = self.wrap(originalCallback); + } + + // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it + // also supports only two arguments and doesn't care what this is, so we + // can just call the original function directly. + if (orig.apply) { + return orig.apply(this, args); + } else { + return orig(args[0], args[1]); + } + }; + } + + var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; + + function wrapEventTarget(global) { + var proto = _window[global] && _window[global].prototype; + if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) { + fill(proto, 'addEventListener', function(orig) { + return function (evtName, fn, capture, secure) { // preserve arity + try { + if (fn && fn.handleEvent) { + fn.handleEvent = self.wrap(fn.handleEvent); + } + } catch (err) { + // can sometimes get 'Permission denied to access property "handle Event' + } + + // More breadcrumb DOM capture ... done here and not in `_instrumentBreadcrumbs` + // so that we don't have more than one wrapper function + var before, + clickHandler, + keypressHandler; + + if (autoBreadcrumbs && autoBreadcrumbs.dom && (global === 'EventTarget' || global === 'Node')) { + // NOTE: generating multiple handlers per addEventListener invocation, should + // revisit and verify we can just use one (almost certainly) + clickHandler = self._breadcrumbEventHandler('click'); + keypressHandler = self._keypressEventHandler(); + before = function (evt) { + // need to intercept every DOM event in `before` argument, in case that + // same wrapped method is re-used for different events (e.g. mousemove THEN click) + // see #724 + if (!evt) return; + + if (evt.type === 'click') + return clickHandler(evt); + else if (evt.type === 'keypress') + return keypressHandler(evt); + }; + } + return orig.call(this, evtName, self.wrap(fn, undefined, before), capture, secure); + }; + }, wrappedBuiltIns); + fill(proto, 'removeEventListener', function (orig) { + return function (evt, fn, capture, secure) { + try { + fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn); + } catch (e) { + // ignore, accessing __raven_wrapper__ will throw in some Selenium environments + } + return orig.call(this, evt, fn, capture, secure); + }; + }, wrappedBuiltIns); + } + } + + fill(_window, 'setTimeout', wrapTimeFn, wrappedBuiltIns); + fill(_window, 'setInterval', wrapTimeFn, wrappedBuiltIns); + if (_window.requestAnimationFrame) { + fill(_window, 'requestAnimationFrame', function (orig) { + return function (cb) { + return orig(self.wrap(cb)); + }; + }, wrappedBuiltIns); + } + + // event targets borrowed from bugsnag-js: + // https://github.com/bugsnag/bugsnag-js/blob/master/src/bugsnag.js#L666 + var eventTargets = ['EventTarget', 'Window', 'Node', 'ApplicationCache', 'AudioTrackList', 'ChannelMergerNode', 'CryptoOperation', 'EventSource', 'FileReader', 'HTMLUnknownElement', 'IDBDatabase', 'IDBRequest', 'IDBTransaction', 'KeyOperation', 'MediaController', 'MessagePort', 'ModalWindow', 'Notification', 'SVGElementInstance', 'Screen', 'TextTrack', 'TextTrackCue', 'TextTrackList', 'WebSocket', 'WebSocketWorker', 'Worker', 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'XMLHttpRequestUpload']; + for (var i = 0; i < eventTargets.length; i++) { + wrapEventTarget(eventTargets[i]); + } + + var $ = _window.jQuery || _window.$; + if ($ && $.fn && $.fn.ready) { + fill($.fn, 'ready', function (orig) { + return function (fn) { + return orig.call(this, self.wrap(fn)); + }; + }, wrappedBuiltIns); + } + }, + + + /** + * Instrument browser built-ins w/ breadcrumb capturing + * - XMLHttpRequests + * - DOM interactions (click/typing) + * - window.location changes + * - console + * + * Can be disabled or individually configured via the `autoBreadcrumbs` config option + */ + _instrumentBreadcrumbs: function () { + var self = this; + var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; + + var wrappedBuiltIns = self._wrappedBuiltIns; + + function wrapProp(prop, xhr) { + if (prop in xhr && isFunction(xhr[prop])) { + fill(xhr, prop, function (orig) { + return self.wrap(orig); + }); // intentionally don't track filled methods on XHR instances + } + } + + if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in _window) { + var xhrproto = XMLHttpRequest.prototype; + fill(xhrproto, 'open', function(origOpen) { + return function (method, url) { // preserve arity + + // if Sentry key appears in URL, don't capture + if (isString(url) && url.indexOf(self._globalKey) === -1) { + this.__raven_xhr = { + method: method, + url: url, + status_code: null + }; + } + + return origOpen.apply(this, arguments); + }; + }, wrappedBuiltIns); + + fill(xhrproto, 'send', function(origSend) { + return function (data) { // preserve arity + var xhr = this; + + function onreadystatechangeHandler() { + if (xhr.__raven_xhr && (xhr.readyState === 1 || xhr.readyState === 4)) { + try { + // touching statusCode in some platforms throws + // an exception + xhr.__raven_xhr.status_code = xhr.status; + } catch (e) { /* do nothing */ } + self.captureBreadcrumb({ + type: 'http', + category: 'xhr', + data: xhr.__raven_xhr + }); + } + } + + var props = ['onload', 'onerror', 'onprogress']; + for (var j = 0; j < props.length; j++) { + wrapProp(props[j], xhr); + } + + if ('onreadystatechange' in xhr && isFunction(xhr.onreadystatechange)) { + fill(xhr, 'onreadystatechange', function (orig) { + return self.wrap(orig, undefined, onreadystatechangeHandler); + } /* intentionally don't track this instrumentation */); + } else { + // if onreadystatechange wasn't actually set by the page on this xhr, we + // are free to set our own and capture the breadcrumb + xhr.onreadystatechange = onreadystatechangeHandler; + } + + return origSend.apply(this, arguments); + }; + }, wrappedBuiltIns); + } + + if (autoBreadcrumbs.xhr && 'fetch' in _window) { + fill(_window, 'fetch', function(origFetch) { + return function (fn, t) { // preserve arity + // Make a copy of the arguments to prevent deoptimization + // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments + var args = new Array(arguments.length); + for(var i = 0; i < args.length; ++i) { + args[i] = arguments[i]; + } + + var method = 'GET'; + + if (args[1] && args[1].method) { + method = args[1].method; + } + + var fetchData = { + method: method, + url: args[0], + status_code: null + }; + + self.captureBreadcrumb({ + type: 'http', + category: 'fetch', + data: fetchData + }); + + return origFetch.apply(this, args).then(function (response) { + fetchData.status_code = response.status; + + return response; + }); + }; + }, wrappedBuiltIns); + } + + // Capture breadcrumbs from any click that is unhandled / bubbled up all the way + // to the document. Do this before we instrument addEventListener. + if (autoBreadcrumbs.dom && this._hasDocument) { + if (_document.addEventListener) { + _document.addEventListener('click', self._breadcrumbEventHandler('click'), false); + _document.addEventListener('keypress', self._keypressEventHandler(), false); + } + else { + // IE8 Compatibility + _document.attachEvent('onclick', self._breadcrumbEventHandler('click')); + _document.attachEvent('onkeypress', self._keypressEventHandler()); + } + } + + // record navigation (URL) changes + // NOTE: in Chrome App environment, touching history.pushState, *even inside + // a try/catch block*, will cause Chrome to output an error to console.error + // borrowed from: https://github.com/angular/angular.js/pull/13945/files + var chrome = _window.chrome; + var isChromePackagedApp = chrome && chrome.app && chrome.app.runtime; + var hasPushState = !isChromePackagedApp && _window.history && history.pushState; + if (autoBreadcrumbs.location && hasPushState) { + // TODO: remove onpopstate handler on uninstall() + var oldOnPopState = _window.onpopstate; + _window.onpopstate = function () { + var currentHref = self._location.href; + self._captureUrlChange(self._lastHref, currentHref); + + if (oldOnPopState) { + return oldOnPopState.apply(this, arguments); + } + }; + + fill(history, 'pushState', function (origPushState) { + // note history.pushState.length is 0; intentionally not declaring + // params to preserve 0 arity + return function (/* state, title, url */) { + var url = arguments.length > 2 ? arguments[2] : undefined; + + // url argument is optional + if (url) { + // coerce to string (this is what pushState does) + self._captureUrlChange(self._lastHref, url + ''); + } + + return origPushState.apply(this, arguments); + }; + }, wrappedBuiltIns); + } + + if (autoBreadcrumbs.console && 'console' in _window && console.log) { + // console + var consoleMethodCallback = function (msg, data) { + self.captureBreadcrumb({ + message: msg, + level: data.level, + category: 'console' + }); + }; + + each(['debug', 'info', 'warn', 'error', 'log'], function (_, level) { + wrapConsoleMethod(console, level, consoleMethodCallback); + }); + } + + }, + + _restoreBuiltIns: function () { + // restore any wrapped builtins + var builtin; + while (this._wrappedBuiltIns.length) { + builtin = this._wrappedBuiltIns.shift(); + + var obj = builtin[0], + name = builtin[1], + orig = builtin[2]; + + obj[name] = orig; + } + }, + + _drainPlugins: function() { + var self = this; + + // FIX ME TODO + each(this._plugins, function(_, plugin) { + var installer = plugin[0]; + var args = plugin[1]; + installer.apply(self, [self].concat(args)); + }); + }, + + _parseDSN: function(str) { + var m = dsnPattern.exec(str), + dsn = {}, + i = 7; + + try { + while (i--) dsn[dsnKeys[i]] = m[i] || ''; + } catch(e) { + throw new RavenConfigError('Invalid DSN: ' + str); + } + + if (dsn.pass && !this._globalOptions.allowSecretKey) { + throw new RavenConfigError('Do not specify your secret key in the DSN. See: http://bit.ly/raven-secret-key'); + } + + return dsn; + }, + + _getGlobalServer: function(uri) { + // assemble the endpoint from the uri pieces + var globalServer = '//' + uri.host + + (uri.port ? ':' + uri.port : ''); + + if (uri.protocol) { + globalServer = uri.protocol + ':' + globalServer; + } + return globalServer; + }, + + _handleOnErrorStackInfo: function() { + // if we are intentionally ignoring errors via onerror, bail out + if (!this._ignoreOnError) { + this._handleStackInfo.apply(this, arguments); + } + }, + + _handleStackInfo: function(stackInfo, options) { + var frames = this._prepareFrames(stackInfo, options); + + this._triggerEvent('handle', { + stackInfo: stackInfo, + options: options + }); + + this._processException( + stackInfo.name, + stackInfo.message, + stackInfo.url, + stackInfo.lineno, + frames, + options + ); + }, + + _prepareFrames: function(stackInfo, options) { + var self = this; + var frames = []; + if (stackInfo.stack && stackInfo.stack.length) { + each(stackInfo.stack, function(i, stack) { + var frame = self._normalizeFrame(stack); + if (frame) { + frames.push(frame); + } + }); + + // e.g. frames captured via captureMessage throw + if (options && options.trimHeadFrames) { + for (var j = 0; j < options.trimHeadFrames && j < frames.length; j++) { + frames[j].in_app = false; + } + } + } + frames = frames.slice(0, this._globalOptions.stackTraceLimit); + return frames; + }, + + + _normalizeFrame: function(frame) { + if (!frame.url) return; + + // normalize the frames data + var normalized = { + filename: frame.url, + lineno: frame.line, + colno: frame.column, + 'function': frame.func || '?' + }; + + normalized.in_app = !( // determine if an exception came from outside of our app + // first we check the global includePaths list. + !!this._globalOptions.includePaths.test && !this._globalOptions.includePaths.test(normalized.filename) || + // Now we check for fun, if the function name is Raven or TraceKit + /(Raven|TraceKit)\./.test(normalized['function']) || + // finally, we do a last ditch effort and check for raven.min.js + /raven\.(min\.)?js$/.test(normalized.filename) + ); + + return normalized; + }, + + _processException: function(type, message, fileurl, lineno, frames, options) { + var stacktrace; + if (!!this._globalOptions.ignoreErrors.test && this._globalOptions.ignoreErrors.test(message)) return; + + message += ''; + + if (frames && frames.length) { + fileurl = frames[0].filename || fileurl; + // Sentry expects frames oldest to newest + // and JS sends them as newest to oldest + frames.reverse(); + stacktrace = {frames: frames}; + } else if (fileurl) { + stacktrace = { + frames: [{ + filename: fileurl, + lineno: lineno, + in_app: true + }] + }; + } + + if (!!this._globalOptions.ignoreUrls.test && this._globalOptions.ignoreUrls.test(fileurl)) return; + if (!!this._globalOptions.whitelistUrls.test && !this._globalOptions.whitelistUrls.test(fileurl)) return; + + var data = objectMerge({ + // sentry.interfaces.Exception + exception: { + values: [{ + type: type, + value: message, + stacktrace: stacktrace + }] + }, + culprit: fileurl + }, options); + + // Fire away! + this._send(data); + }, + + _trimPacket: function(data) { + // For now, we only want to truncate the two different messages + // but this could/should be expanded to just trim everything + var max = this._globalOptions.maxMessageLength; + if (data.message) { + data.message = truncate(data.message, max); + } + if (data.exception) { + var exception = data.exception.values[0]; + exception.value = truncate(exception.value, max); + } + + return data; + }, + + _getHttpData: function() { + if (!this._hasDocument || !_document.location || !_document.location.href) { + return; + } + + var httpData = { + headers: { + 'User-Agent': navigator.userAgent + } + }; + + httpData.url = _document.location.href; + + if (_document.referrer) { + httpData.headers.Referer = _document.referrer; + } + + return httpData; + }, + + + _send: function(data) { + var globalOptions = this._globalOptions; + + var baseData = { + project: this._globalProject, + logger: globalOptions.logger, + platform: 'javascript' + }, httpData = this._getHttpData(); + + if (httpData) { + baseData.request = httpData; + } + + // HACK: delete `trimHeadFrames` to prevent from appearing in outbound payload + if (data.trimHeadFrames) delete data.trimHeadFrames; + + data = objectMerge(baseData, data); + + // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge + data.tags = objectMerge(objectMerge({}, this._globalContext.tags), data.tags); + data.extra = objectMerge(objectMerge({}, this._globalContext.extra), data.extra); + + // Send along our own collected metadata with extra + data.extra['session:duration'] = now() - this._startTime; + + if (this._breadcrumbs && this._breadcrumbs.length > 0) { + // intentionally make shallow copy so that additions + // to breadcrumbs aren't accidentally sent in this request + data.breadcrumbs = { + values: [].slice.call(this._breadcrumbs, 0) + }; + } + + // If there are no tags/extra, strip the key from the payload alltogther. + if (isEmptyObject(data.tags)) delete data.tags; + + if (this._globalContext.user) { + // sentry.interfaces.User + data.user = this._globalContext.user; + } + + // Include the environment if it's defined in globalOptions + if (globalOptions.environment) data.environment = globalOptions.environment; + + // Include the release if it's defined in globalOptions + if (globalOptions.release) data.release = globalOptions.release; + + // Include server_name if it's defined in globalOptions + if (globalOptions.serverName) data.server_name = globalOptions.serverName; + + if (isFunction(globalOptions.dataCallback)) { + data = globalOptions.dataCallback(data) || data; + } + + // Why?????????? + if (!data || isEmptyObject(data)) { + return; + } + + // Check if the request should be filtered or not + if (isFunction(globalOptions.shouldSendCallback) && !globalOptions.shouldSendCallback(data)) { + return; + } + + this._sendProcessedPayload(data); + }, + + _getUuid: function () { + return uuid4(); + }, + + _sendProcessedPayload: function(data, callback) { + var self = this; + var globalOptions = this._globalOptions; + + // Send along an event_id if not explicitly passed. + // This event_id can be used to reference the error within Sentry itself. + // Set lastEventId after we know the error should actually be sent + this._lastEventId = data.event_id || (data.event_id = this._getUuid()); + + // Try and clean up the packet before sending by truncating long values + data = this._trimPacket(data); + + this._logDebug('debug', 'Raven about to send:', data); + + if (!this.isSetup()) return; + + var auth = { + sentry_version: '7', + sentry_client: 'raven-js/' + this.VERSION, + sentry_key: this._globalKey + }; + if (this._globalSecret) { + auth.sentry_secret = this._globalSecret; + } + + var exception = data.exception && data.exception.values[0]; + this.captureBreadcrumb({ + category: 'sentry', + message: exception + ? (exception.type ? exception.type + ': ' : '') + exception.value + : data.message, + event_id: data.event_id, + level: data.level || 'error' // presume error unless specified + }); + + var url = this._globalEndpoint; + (globalOptions.transport || this._makeRequest).call(this, { + url: url, + auth: auth, + data: data, + options: globalOptions, + onSuccess: function success() { + self._triggerEvent('success', { + data: data, + src: url + }); + callback && callback(); + }, + onError: function failure(error) { + self._triggerEvent('failure', { + data: data, + src: url + }); + error = error || new Error('Raven send failed (no additional details provided)'); + callback && callback(error); + } + }); + }, + + _makeRequest: function(opts) { + var request = new XMLHttpRequest(); + + // if browser doesn't support CORS (e.g. IE7), we are out of luck + var hasCORS = + 'withCredentials' in request || + typeof XDomainRequest !== 'undefined'; + + if (!hasCORS) return; + + var url = opts.url; + function handler() { + if (request.status === 200) { + if (opts.onSuccess) { + opts.onSuccess(); + } + } else if (opts.onError) { + opts.onError(new Error('Sentry error code: ' + request.status)); + } + } + + if ('withCredentials' in request) { + request.onreadystatechange = function () { + if (request.readyState !== 4) { + return; + } + handler(); + }; + } else { + request = new XDomainRequest(); + // xdomainrequest cannot go http -> https (or vice versa), + // so always use protocol relative + url = url.replace(/^https?:/, ''); + + // onreadystatechange not supported by XDomainRequest + request.onload = handler; + } + + // NOTE: auth is intentionally sent as part of query string (NOT as custom + // HTTP header) so as to avoid preflight CORS requests + request.open('POST', url + '?' + urlencode(opts.auth)); + request.send(stringify(opts.data)); + }, + + _logDebug: function(level) { + if (this._originalConsoleMethods[level] && this.debug) { + // In IE<10 console methods do not have their own 'apply' method + Function.prototype.apply.call( + this._originalConsoleMethods[level], + this._originalConsole, + [].slice.call(arguments, 1) + ); + } + }, + + _mergeContext: function(key, context) { + if (isUndefined(context)) { + delete this._globalContext[key]; + } else { + this._globalContext[key] = objectMerge(this._globalContext[key] || {}, context); + } + } +}; + +/*------------------------------------------------ + * utils + * + * conditionally exported for test via Raven.utils + ================================================= + */ +var objectPrototype = Object.prototype; + +function isUndefined(what) { + return what === void 0; +} + +function isFunction(what) { + return typeof what === 'function'; +} + +function isString(what) { + return objectPrototype.toString.call(what) === '[object String]'; +} + +function isObject(what) { + return typeof what === 'object' && what !== null; +} + +function isEmptyObject(what) { + for (var _ in what) return false; // eslint-disable-line guard-for-in, no-unused-vars + return true; +} + +// Sorta yanked from https://github.com/joyent/node/blob/aa3b4b4/lib/util.js#L560 +// with some tiny modifications +function isError(what) { + var toString = objectPrototype.toString.call(what); + return isObject(what) && + toString === '[object Error]' || + toString === '[object Exception]' || // Firefox NS_ERROR_FAILURE Exceptions + what instanceof Error; +} + +function each(obj, callback) { + var i, j; + + if (isUndefined(obj.length)) { + for (i in obj) { + if (hasKey(obj, i)) { + callback.call(null, i, obj[i]); + } + } + } else { + j = obj.length; + if (j) { + for (i = 0; i < j; i++) { + callback.call(null, i, obj[i]); + } + } + } +} + +function objectMerge(obj1, obj2) { + if (!obj2) { + return obj1; + } + each(obj2, function(key, value){ + obj1[key] = value; + }); + return obj1; +} + +function truncate(str, max) { + return !max || str.length <= max ? str : str.substr(0, max) + '\u2026'; +} + +/** + * hasKey, a better form of hasOwnProperty + * Example: hasKey(MainHostObject, property) === true/false + * + * @param {Object} host object to check property + * @param {string} key to check + */ +function hasKey(object, key) { + return objectPrototype.hasOwnProperty.call(object, key); +} + +function joinRegExp(patterns) { + // Combine an array of regular expressions and strings into one large regexp + // Be mad. + var sources = [], + i = 0, len = patterns.length, + pattern; + + for (; i < len; i++) { + pattern = patterns[i]; + if (isString(pattern)) { + // If it's a string, we need to escape it + // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1')); + } else if (pattern && pattern.source) { + // If it's a regexp already, we want to extract the source + sources.push(pattern.source); + } + // Intentionally skip other cases + } + return new RegExp(sources.join('|'), 'i'); +} + +function urlencode(o) { + var pairs = []; + each(o, function(key, value) { + pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + }); + return pairs.join('&'); +} + +// borrowed from https://tools.ietf.org/html/rfc3986#appendix-B +// intentionally using regex and not href parsing trick because React Native and other +// environments where DOM might not be available +function parseUrl(url) { + var match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/); + if (!match) return {}; + + // coerce to undefined values to empty string so we don't get 'undefined' + var query = match[6] || ''; + var fragment = match[8] || ''; + return { + protocol: match[2], + host: match[4], + path: match[5], + relative: match[5] + query + fragment // everything minus origin + }; +} +function uuid4() { + var crypto = _window.crypto || _window.msCrypto; + + if (!isUndefined(crypto) && crypto.getRandomValues) { + // Use window.crypto API if available + var arr = new Uint16Array(8); + crypto.getRandomValues(arr); + + // set 4 in byte 7 + arr[3] = arr[3] & 0xFFF | 0x4000; + // set 2 most significant bits of byte 9 to '10' + arr[4] = arr[4] & 0x3FFF | 0x8000; + + var pad = function(num) { + var v = num.toString(16); + while (v.length < 4) { + v = '0' + v; + } + return v; + }; + + return pad(arr[0]) + pad(arr[1]) + pad(arr[2]) + pad(arr[3]) + pad(arr[4]) + + pad(arr[5]) + pad(arr[6]) + pad(arr[7]); + } else { + // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 + return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, + v = c === 'x' ? r : r&0x3|0x8; + return v.toString(16); + }); + } +} + +/** + * Given a child DOM element, returns a query-selector statement describing that + * and its ancestors + * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz] + * @param elem + * @returns {string} + */ +function htmlTreeAsString(elem) { + /* eslint no-extra-parens:0*/ + var MAX_TRAVERSE_HEIGHT = 5, + MAX_OUTPUT_LEN = 80, + out = [], + height = 0, + len = 0, + separator = ' > ', + sepLength = separator.length, + nextStr; + + while (elem && height++ < MAX_TRAVERSE_HEIGHT) { + + nextStr = htmlElementAsString(elem); + // bail out if + // - nextStr is the 'html' element + // - the length of the string that would be created exceeds MAX_OUTPUT_LEN + // (ignore this limit if we are on the first iteration) + if (nextStr === 'html' || height > 1 && len + (out.length * sepLength) + nextStr.length >= MAX_OUTPUT_LEN) { + break; + } + + out.push(nextStr); + + len += nextStr.length; + elem = elem.parentNode; + } + + return out.reverse().join(separator); +} + +/** + * Returns a simple, query-selector representation of a DOM element + * e.g. [HTMLElement] => input#foo.btn[name=baz] + * @param HTMLElement + * @returns {string} + */ +function htmlElementAsString(elem) { + var out = [], + className, + classes, + key, + attr, + i; + + if (!elem || !elem.tagName) { + return ''; + } + + out.push(elem.tagName.toLowerCase()); + if (elem.id) { + out.push('#' + elem.id); + } + + className = elem.className; + if (className && isString(className)) { + classes = className.split(' '); + for (i = 0; i < classes.length; i++) { + out.push('.' + classes[i]); + } + } + var attrWhitelist = ['type', 'name', 'title', 'alt']; + for (i = 0; i < attrWhitelist.length; i++) { + key = attrWhitelist[i]; + attr = elem.getAttribute(key); + if (attr) { + out.push('[' + key + '="' + attr + '"]'); + } + } + return out.join(''); +} + +/** + * Polyfill a method + * @param obj object e.g. `document` + * @param name method name present on object e.g. `addEventListener` + * @param replacement replacement function + * @param track {optional} record instrumentation to an array + */ +function fill(obj, name, replacement, track) { + var orig = obj[name]; + obj[name] = replacement(orig); + if (track) { + track.push([obj, name, orig]); + } +} + +if (typeof __DEV__ !== 'undefined' && __DEV__) { + Raven.utils = { + isUndefined: isUndefined, + isFunction: isFunction, + isString: isString, + isObject: isObject, + isEmptyObject: isEmptyObject, + isError: isError, + each: each, + objectMerge: objectMerge, + truncate: truncate, + hasKey: hasKey, + joinRegExp: joinRegExp, + urlencode: urlencode, + uuid4: uuid4, + htmlTreeAsString: htmlTreeAsString, + htmlElementAsString: htmlElementAsString, + parseUrl: parseUrl, + fill: fill + }; +}; + +// Deprecations +Raven.prototype.setUser = Raven.prototype.setUserContext; +Raven.prototype.setReleaseContext = Raven.prototype.setRelease; + +module.exports = Raven; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"1":1,"2":2,"3":3,"6":6}],5:[function(_dereq_,module,exports){ +(function (global){ +/** + * Enforces a single instance of the Raven client, and the + * main entry point for Raven. If you are a consumer of the + * Raven library, you SHOULD load this file (vs raven.js). + **/ + +'use strict'; + +var RavenConstructor = _dereq_(4); + +// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785) +var _window = typeof window !== 'undefined' ? window + : typeof global !== 'undefined' ? global + : typeof self !== 'undefined' ? self + : {}; +var _Raven = _window.Raven; + +var Raven = new RavenConstructor(); + +/* + * Allow multiple versions of Raven to be installed. + * Strip Raven from the global context and returns the instance. + * + * @return {Raven} + */ +Raven.noConflict = function () { + _window.Raven = _Raven; + return Raven; +}; + +Raven.afterLoad(); + +module.exports = Raven; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"4":4}],6:[function(_dereq_,module,exports){ +(function (global){ +'use strict'; + +/* + TraceKit - Cross brower stack traces - github.com/occ/TraceKit + MIT license +*/ + +var TraceKit = { + collectWindowErrors: true, + debug: false +}; + +// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785) +var _window = typeof window !== 'undefined' ? window + : typeof global !== 'undefined' ? global + : typeof self !== 'undefined' ? self + : {}; + +// global reference to slice +var _slice = [].slice; +var UNKNOWN_FUNCTION = '?'; + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Error_types +var ERROR_TYPES_RE = /^(?:Uncaught (?:exception: )?)?((?:Eval|Internal|Range|Reference|Syntax|Type|URI)Error): ?(.*)$/; + +function getLocationHref() { + if (typeof document === 'undefined') + return ''; + + return document.location.href; +} + +/** + * TraceKit.report: cross-browser processing of unhandled exceptions + * + * Syntax: + * TraceKit.report.subscribe(function(stackInfo) { ... }) + * TraceKit.report.unsubscribe(function(stackInfo) { ... }) + * TraceKit.report(exception) + * try { ...code... } catch(ex) { TraceKit.report(ex); } + * + * Supports: + * - Firefox: full stack trace with line numbers, plus column number + * on top frame; column number is not guaranteed + * - Opera: full stack trace with line and column numbers + * - Chrome: full stack trace with line and column numbers + * - Safari: line and column number for the top frame only; some frames + * may be missing, and column number is not guaranteed + * - IE: line and column number for the top frame only; some frames + * may be missing, and column number is not guaranteed + * + * In theory, TraceKit should work on all of the following versions: + * - IE5.5+ (only 8.0 tested) + * - Firefox 0.9+ (only 3.5+ tested) + * - Opera 7+ (only 10.50 tested; versions 9 and earlier may require + * Exceptions Have Stacktrace to be enabled in opera:config) + * - Safari 3+ (only 4+ tested) + * - Chrome 1+ (only 5+ tested) + * - Konqueror 3.5+ (untested) + * + * Requires TraceKit.computeStackTrace. + * + * Tries to catch all unhandled exceptions and report them to the + * subscribed handlers. Please note that TraceKit.report will rethrow the + * exception. This is REQUIRED in order to get a useful stack trace in IE. + * If the exception does not reach the top of the browser, you will only + * get a stack trace from the point where TraceKit.report was called. + * + * Handlers receive a stackInfo object as described in the + * TraceKit.computeStackTrace docs. + */ +TraceKit.report = (function reportModuleWrapper() { + var handlers = [], + lastArgs = null, + lastException = null, + lastExceptionStack = null; + + /** + * Add a crash handler. + * @param {Function} handler + */ + function subscribe(handler) { + installGlobalHandler(); + handlers.push(handler); + } + + /** + * Remove a crash handler. + * @param {Function} handler + */ + function unsubscribe(handler) { + for (var i = handlers.length - 1; i >= 0; --i) { + if (handlers[i] === handler) { + handlers.splice(i, 1); + } + } + } + + /** + * Remove all crash handlers. + */ + function unsubscribeAll() { + uninstallGlobalHandler(); + handlers = []; + } + + /** + * Dispatch stack information to all handlers. + * @param {Object.} stack + */ + function notifyHandlers(stack, isWindowError) { + var exception = null; + if (isWindowError && !TraceKit.collectWindowErrors) { + return; + } + for (var i in handlers) { + if (handlers.hasOwnProperty(i)) { + try { + handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2))); + } catch (inner) { + exception = inner; + } + } + } + + if (exception) { + throw exception; + } + } + + var _oldOnerrorHandler, _onErrorHandlerInstalled; + + /** + * Ensures all global unhandled exceptions are recorded. + * Supported by Gecko and IE. + * @param {string} message Error message. + * @param {string} url URL of script that generated the exception. + * @param {(number|string)} lineNo The line number at which the error + * occurred. + * @param {?(number|string)} colNo The column number at which the error + * occurred. + * @param {?Error} ex The actual Error object. + */ + function traceKitWindowOnError(message, url, lineNo, colNo, ex) { + var stack = null; + + if (lastExceptionStack) { + TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message); + processLastException(); + } else if (ex) { + // New chrome and blink send along a real error object + // Let's just report that like a normal error. + // See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror + stack = TraceKit.computeStackTrace(ex); + notifyHandlers(stack, true); + } else { + var location = { + 'url': url, + 'line': lineNo, + 'column': colNo + }; + + var name = undefined; + var msg = message; // must be new var or will modify original `arguments` + var groups; + if ({}.toString.call(message) === '[object String]') { + var groups = message.match(ERROR_TYPES_RE); + if (groups) { + name = groups[1]; + msg = groups[2]; + } + } + + location.func = UNKNOWN_FUNCTION; + + stack = { + 'name': name, + 'message': msg, + 'url': getLocationHref(), + 'stack': [location] + }; + notifyHandlers(stack, true); + } + + if (_oldOnerrorHandler) { + return _oldOnerrorHandler.apply(this, arguments); + } + + return false; + } + + function installGlobalHandler () + { + if (_onErrorHandlerInstalled) { + return; + } + _oldOnerrorHandler = _window.onerror; + _window.onerror = traceKitWindowOnError; + _onErrorHandlerInstalled = true; + } + + function uninstallGlobalHandler () + { + if (!_onErrorHandlerInstalled) { + return; + } + _window.onerror = _oldOnerrorHandler; + _onErrorHandlerInstalled = false; + _oldOnerrorHandler = undefined; + } + + function processLastException() { + var _lastExceptionStack = lastExceptionStack, + _lastArgs = lastArgs; + lastArgs = null; + lastExceptionStack = null; + lastException = null; + notifyHandlers.apply(null, [_lastExceptionStack, false].concat(_lastArgs)); + } + + /** + * Reports an unhandled Error to TraceKit. + * @param {Error} ex + * @param {?boolean} rethrow If false, do not re-throw the exception. + * Only used for window.onerror to not cause an infinite loop of + * rethrowing. + */ + function report(ex, rethrow) { + var args = _slice.call(arguments, 1); + if (lastExceptionStack) { + if (lastException === ex) { + return; // already caught by an inner catch block, ignore + } else { + processLastException(); + } + } + + var stack = TraceKit.computeStackTrace(ex); + lastExceptionStack = stack; + lastException = ex; + lastArgs = args; + + // If the stack trace is incomplete, wait for 2 seconds for + // slow slow IE to see if onerror occurs or not before reporting + // this exception; otherwise, we will end up with an incomplete + // stack trace + setTimeout(function () { + if (lastException === ex) { + processLastException(); + } + }, (stack.incomplete ? 2000 : 0)); + + if (rethrow !== false) { + throw ex; // re-throw to propagate to the top level (and cause window.onerror) + } + } + + report.subscribe = subscribe; + report.unsubscribe = unsubscribe; + report.uninstall = unsubscribeAll; + return report; +}()); + +/** + * TraceKit.computeStackTrace: cross-browser stack traces in JavaScript + * + * Syntax: + * s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below) + * Returns: + * s.name - exception name + * s.message - exception message + * s.stack[i].url - JavaScript or HTML file URL + * s.stack[i].func - function name, or empty for anonymous functions (if guessing did not work) + * s.stack[i].args - arguments passed to the function, if known + * s.stack[i].line - line number, if known + * s.stack[i].column - column number, if known + * + * Supports: + * - Firefox: full stack trace with line numbers and unreliable column + * number on top frame + * - Opera 10: full stack trace with line and column numbers + * - Opera 9-: full stack trace with line numbers + * - Chrome: full stack trace with line and column numbers + * - Safari: line and column number for the topmost stacktrace element + * only + * - IE: no line numbers whatsoever + * + * Tries to guess names of anonymous functions by looking for assignments + * in the source code. In IE and Safari, we have to guess source file names + * by searching for function bodies inside all page scripts. This will not + * work for scripts that are loaded cross-domain. + * Here be dragons: some function names may be guessed incorrectly, and + * duplicate functions may be mismatched. + * + * TraceKit.computeStackTrace should only be used for tracing purposes. + * Logging of unhandled exceptions should be done with TraceKit.report, + * which builds on top of TraceKit.computeStackTrace and provides better + * IE support by utilizing the window.onerror event to retrieve information + * about the top of the stack. + * + * Note: In IE and Safari, no stack trace is recorded on the Error object, + * so computeStackTrace instead walks its *own* chain of callers. + * This means that: + * * in Safari, some methods may be missing from the stack trace; + * * in IE, the topmost function in the stack trace will always be the + * caller of computeStackTrace. + * + * This is okay for tracing (because you are likely to be calling + * computeStackTrace from the function you want to be the topmost element + * of the stack trace anyway), but not okay for logging unhandled + * exceptions (because your catch block will likely be far away from the + * inner function that actually caused the exception). + * + */ +TraceKit.computeStackTrace = (function computeStackTraceWrapper() { + /** + * Escapes special characters, except for whitespace, in a string to be + * used inside a regular expression as a string literal. + * @param {string} text The string. + * @return {string} The escaped string literal. + */ + function escapeRegExp(text) { + return text.replace(/[\-\[\]{}()*+?.,\\\^$|#]/g, '\\$&'); + } + + /** + * Escapes special characters in a string to be used inside a regular + * expression as a string literal. Also ensures that HTML entities will + * be matched the same as their literal friends. + * @param {string} body The string. + * @return {string} The escaped string. + */ + function escapeCodeAsRegExpForMatchingInsideHTML(body) { + return escapeRegExp(body).replace('<', '(?:<|<)').replace('>', '(?:>|>)').replace('&', '(?:&|&)').replace('"', '(?:"|")').replace(/\s+/g, '\\s+'); + } + + // Contents of Exception in various browsers. + // + // SAFARI: + // ex.message = Can't find variable: qq + // ex.line = 59 + // ex.sourceId = 580238192 + // ex.sourceURL = http://... + // ex.expressionBeginOffset = 96 + // ex.expressionCaretOffset = 98 + // ex.expressionEndOffset = 98 + // ex.name = ReferenceError + // + // FIREFOX: + // ex.message = qq is not defined + // ex.fileName = http://... + // ex.lineNumber = 59 + // ex.columnNumber = 69 + // ex.stack = ...stack trace... (see the example below) + // ex.name = ReferenceError + // + // CHROME: + // ex.message = qq is not defined + // ex.name = ReferenceError + // ex.type = not_defined + // ex.arguments = ['aa'] + // ex.stack = ...stack trace... + // + // INTERNET EXPLORER: + // ex.message = ... + // ex.name = ReferenceError + // + // OPERA: + // ex.message = ...message... (see the example below) + // ex.name = ReferenceError + // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message) + // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace' + + /** + * Computes stack trace information from the stack property. + * Chrome and Gecko use this property. + * @param {Error} ex + * @return {?Object.} Stack trace information. + */ + function computeStackTraceFromStackProp(ex) { + if (typeof ex.stack === 'undefined' || !ex.stack) return; + + var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, + gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i, + winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i, + lines = ex.stack.split('\n'), + stack = [], + parts, + element, + reference = /^(.*) is undefined$/.exec(ex.message); + + for (var i = 0, j = lines.length; i < j; ++i) { + if ((parts = chrome.exec(lines[i]))) { + var isNative = parts[2] && parts[2].indexOf('native') !== -1; + element = { + 'url': !isNative ? parts[2] : null, + 'func': parts[1] || UNKNOWN_FUNCTION, + 'args': isNative ? [parts[2]] : [], + 'line': parts[3] ? +parts[3] : null, + 'column': parts[4] ? +parts[4] : null + }; + } else if ( parts = winjs.exec(lines[i]) ) { + element = { + 'url': parts[2], + 'func': parts[1] || UNKNOWN_FUNCTION, + 'args': [], + 'line': +parts[3], + 'column': parts[4] ? +parts[4] : null + }; + } else if ((parts = gecko.exec(lines[i]))) { + element = { + 'url': parts[3], + 'func': parts[1] || UNKNOWN_FUNCTION, + 'args': parts[2] ? parts[2].split(',') : [], + 'line': parts[4] ? +parts[4] : null, + 'column': parts[5] ? +parts[5] : null + }; + } else { + continue; + } + + if (!element.func && element.line) { + element.func = UNKNOWN_FUNCTION; + } + + stack.push(element); + } + + if (!stack.length) { + return null; + } + + if (!stack[0].column && typeof ex.columnNumber !== 'undefined') { + // FireFox uses this awesome columnNumber property for its top frame + // Also note, Firefox's column number is 0-based and everything else expects 1-based, + // so adding 1 + stack[0].column = ex.columnNumber + 1; + } + + return { + 'name': ex.name, + 'message': ex.message, + 'url': getLocationHref(), + 'stack': stack + }; + } + + /** + * Adds information about the first frame to incomplete stack traces. + * Safari and IE require this to get complete data on the first frame. + * @param {Object.} stackInfo Stack trace information from + * one of the compute* methods. + * @param {string} url The URL of the script that caused an error. + * @param {(number|string)} lineNo The line number of the script that + * caused an error. + * @param {string=} message The error generated by the browser, which + * hopefully contains the name of the object that caused the error. + * @return {boolean} Whether or not the stack information was + * augmented. + */ + function augmentStackTraceWithInitialElement(stackInfo, url, lineNo, message) { + var initial = { + 'url': url, + 'line': lineNo + }; + + if (initial.url && initial.line) { + stackInfo.incomplete = false; + + if (!initial.func) { + initial.func = UNKNOWN_FUNCTION; + } + + if (stackInfo.stack.length > 0) { + if (stackInfo.stack[0].url === initial.url) { + if (stackInfo.stack[0].line === initial.line) { + return false; // already in stack trace + } else if (!stackInfo.stack[0].line && stackInfo.stack[0].func === initial.func) { + stackInfo.stack[0].line = initial.line; + return false; + } + } + } + + stackInfo.stack.unshift(initial); + stackInfo.partial = true; + return true; + } else { + stackInfo.incomplete = true; + } + + return false; + } + + /** + * Computes stack trace information by walking the arguments.caller + * chain at the time the exception occurred. This will cause earlier + * frames to be missed but is the only way to get any stack trace in + * Safari and IE. The top frame is restored by + * {@link augmentStackTraceWithInitialElement}. + * @param {Error} ex + * @return {?Object.} Stack trace information. + */ + function computeStackTraceByWalkingCallerChain(ex, depth) { + var functionName = /function\s+([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?\s*\(/i, + stack = [], + funcs = {}, + recursion = false, + parts, + item, + source; + + for (var curr = computeStackTraceByWalkingCallerChain.caller; curr && !recursion; curr = curr.caller) { + if (curr === computeStackTrace || curr === TraceKit.report) { + // console.log('skipping internal function'); + continue; + } + + item = { + 'url': null, + 'func': UNKNOWN_FUNCTION, + 'line': null, + 'column': null + }; + + if (curr.name) { + item.func = curr.name; + } else if ((parts = functionName.exec(curr.toString()))) { + item.func = parts[1]; + } + + if (typeof item.func === 'undefined') { + try { + item.func = parts.input.substring(0, parts.input.indexOf('{')); + } catch (e) { } + } + + if (funcs['' + curr]) { + recursion = true; + }else{ + funcs['' + curr] = true; + } + + stack.push(item); + } + + if (depth) { + // console.log('depth is ' + depth); + // console.log('stack is ' + stack.length); + stack.splice(0, depth); + } + + var result = { + 'name': ex.name, + 'message': ex.message, + 'url': getLocationHref(), + 'stack': stack + }; + augmentStackTraceWithInitialElement(result, ex.sourceURL || ex.fileName, ex.line || ex.lineNumber, ex.message || ex.description); + return result; + } + + /** + * Computes a stack trace for an exception. + * @param {Error} ex + * @param {(string|number)=} depth + */ + function computeStackTrace(ex, depth) { + var stack = null; + depth = (depth == null ? 0 : +depth); + + try { + stack = computeStackTraceFromStackProp(ex); + if (stack) { + return stack; + } + } catch (e) { + if (TraceKit.debug) { + throw e; + } + } + + try { + stack = computeStackTraceByWalkingCallerChain(ex, depth + 1); + if (stack) { + return stack; + } + } catch (e) { + if (TraceKit.debug) { + throw e; + } + } + + return { + 'name': ex.name, + 'message': ex.message, + 'url': getLocationHref() + }; + } + + computeStackTrace.augmentStackTraceWithInitialElement = augmentStackTraceWithInitialElement; + computeStackTrace.computeStackTraceFromStackProp = computeStackTraceFromStackProp; + + return computeStackTrace; +}()); + +module.exports = TraceKit; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}]},{},[5])(5) +}); -- cgit v1.2.1 From 045e4edc1abefe4c3c9d4a8064683f5414884b9f Mon Sep 17 00:00:00 2001 From: Rune Philosof Date: Wed, 22 Feb 2017 09:22:54 +0000 Subject: Enable the import_url field. Closes #28531 The field should be enabled on `project/import/new`. It shouldn't hurt to have the field enabled on `projects/new` as well. --- app/views/shared/_import_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 54b5ae2402e..1c7c73be933 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -2,7 +2,7 @@ = f.label :import_url, class: 'control-label' do %span Git repository URL .col-sm-10 - = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true + = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' .well.prepend-top-20 %ul -- cgit v1.2.1 From e43b2e81dab3cade773d479f2ae56478e3113207 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Thu, 27 Oct 2016 17:39:32 -0200 Subject: Added MR Road map --- lib/container_registry/ROADMAP.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 lib/container_registry/ROADMAP.md diff --git a/lib/container_registry/ROADMAP.md b/lib/container_registry/ROADMAP.md new file mode 100644 index 00000000000..e0a20776404 --- /dev/null +++ b/lib/container_registry/ROADMAP.md @@ -0,0 +1,7 @@ +## Road map + +### Initial thoughts + +- Determine if image names will be persisted or fetched from API +- If persisted, how to update the stored names upon modification +- If fetched, how to fetch only images of a given project -- cgit v1.2.1 From dcd4beb8eb7bb7d0c2f720ef85c3da9f97a3dfe6 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 2 Nov 2016 00:33:35 -0200 Subject: Multi-level container image names backend implementation - Adds Registry events API endpoint - Adds container_images_repository and container_images models - Changes JWT authentication to allow multi-level scopes - Adds services for container image maintenance --- app/models/container_image.rb | 58 ++++++++++++++++++++++ app/models/container_images_repository.rb | 26 ++++++++++ app/models/project.rb | 1 + .../container_registry_authentication_service.rb | 9 +++- .../container_images/create_service.rb | 16 ++++++ .../container_images/destroy_service.rb | 11 ++++ .../container_images/push_service.rb | 26 ++++++++++ .../create_service.rb | 7 +++ ...029153736_create_container_images_repository.rb | 31 ++++++++++++ .../20161031013926_create_container_image.rb | 32 ++++++++++++ lib/api/api.rb | 1 + lib/api/registry_events.rb | 52 +++++++++++++++++++ lib/container_registry/client.rb | 4 ++ lib/container_registry/tag.rb | 8 ++- 14 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 app/models/container_image.rb create mode 100644 app/models/container_images_repository.rb create mode 100644 app/services/container_images_repositories/container_images/create_service.rb create mode 100644 app/services/container_images_repositories/container_images/destroy_service.rb create mode 100644 app/services/container_images_repositories/container_images/push_service.rb create mode 100644 app/services/container_images_repositories/create_service.rb create mode 100644 db/migrate/20161029153736_create_container_images_repository.rb create mode 100644 db/migrate/20161031013926_create_container_image.rb create mode 100644 lib/api/registry_events.rb diff --git a/app/models/container_image.rb b/app/models/container_image.rb new file mode 100644 index 00000000000..dcc4a7af629 --- /dev/null +++ b/app/models/container_image.rb @@ -0,0 +1,58 @@ +class ContainerImage < ActiveRecord::Base + belongs_to :container_images_repository + + delegate :registry, :registry_path_with_namespace, :client, to: :container_images_repository + + validates :manifest, presence: true + + before_validation :update_token, on: :create + def update_token + paths = container_images_repository.allowed_paths << name_with_namespace + token = Auth::ContainerRegistryAuthenticationService.full_access_token(paths) + client.update_token(token) + end + + def path + [registry.path, name_with_namespace].compact.join('/') + end + + def name_with_namespace + [registry_path_with_namespace, name].compact.join('/') + end + + def tag(tag) + ContainerRegistry::Tag.new(self, tag) + end + + def manifest + @manifest ||= client.repository_tags(name_with_namespace) + end + + def tags + return @tags if defined?(@tags) + return [] unless manifest && manifest['tags'] + + @tags = manifest['tags'].map do |tag| + ContainerRegistry::Tag.new(self, tag) + end + end + + def blob(config) + ContainerRegistry::Blob.new(self, config) + end + + def delete_tags + return unless tags + + tags.all?(&:delete) + end + + def self.split_namespace(full_path) + image_name = full_path.split('/').last + namespace = full_path.gsub(/(.*)(#{Regexp.escape('/' + image_name)})/, '\1') + if namespace.count('/') < 1 + namespace, image_name = full_path, "" + end + return namespace, image_name + end +end diff --git a/app/models/container_images_repository.rb b/app/models/container_images_repository.rb new file mode 100644 index 00000000000..99e94d2a6d0 --- /dev/null +++ b/app/models/container_images_repository.rb @@ -0,0 +1,26 @@ +class ContainerImagesRepository < ActiveRecord::Base + + belongs_to :project + + has_many :container_images, dependent: :destroy + + delegate :client, to: :registry + + def registry_path_with_namespace + project.path_with_namespace.downcase + end + + def allowed_paths + @allowed_paths ||= [registry_path_with_namespace] + + container_images.map { |i| i.name_with_namespace } + end + + def registry + @registry ||= begin + token = Auth::ContainerRegistryAuthenticationService.full_access_token(allowed_paths) + url = Gitlab.config.registry.api_url + host_port = Gitlab.config.registry.host_port + ContainerRegistry::Registry.new(url, token: token, path: host_port) + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 411299eef63..703e24eb79a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -157,6 +157,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :project_feature, dependent: :destroy has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete + has_one :container_images_repository, dependent: :destroy has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 5cb7a86a5ee..6b83b38fa4d 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -16,7 +16,7 @@ module Auth { token: authorized_token(scope).encoded } end - def self.full_access_token(*names) + def self.full_access_token(names) registry = Gitlab.config.registry token = JSONWebToken::RSAToken.new(registry.key) token.issuer = registry.issuer @@ -61,7 +61,12 @@ module Auth end def process_repository_access(type, name, actions) - requested_project = Project.find_by_full_path(name) + # Strips image name due to lack of + # per image authentication. + # Removes only last occurence in light + # of future nested groups + namespace, _ = ContainerImage::split_namespace(name) + requested_project = Project.find_by_full_path(namespace) return unless requested_project actions = actions.select do |action| diff --git a/app/services/container_images_repositories/container_images/create_service.rb b/app/services/container_images_repositories/container_images/create_service.rb new file mode 100644 index 00000000000..0c2c69d5183 --- /dev/null +++ b/app/services/container_images_repositories/container_images/create_service.rb @@ -0,0 +1,16 @@ +module ContainerImagesRepositories + module ContainerImages + class CreateService < BaseService + def execute + @container_image = container_images_repository.container_images.create(params) + @container_image if @container_image.valid? + end + + private + + def container_images_repository + @container_images_repository ||= project.container_images_repository + end + end + end +end diff --git a/app/services/container_images_repositories/container_images/destroy_service.rb b/app/services/container_images_repositories/container_images/destroy_service.rb new file mode 100644 index 00000000000..91b8cfeea47 --- /dev/null +++ b/app/services/container_images_repositories/container_images/destroy_service.rb @@ -0,0 +1,11 @@ +module ContainerImagesRepositories + module ContainerImages + class DestroyService < BaseService + def execute(container_image) + return false unless container_image + + container_image.destroy + end + end + end +end diff --git a/app/services/container_images_repositories/container_images/push_service.rb b/app/services/container_images_repositories/container_images/push_service.rb new file mode 100644 index 00000000000..2731cf1d52e --- /dev/null +++ b/app/services/container_images_repositories/container_images/push_service.rb @@ -0,0 +1,26 @@ +module ContainerImagesRepositories + module ContainerImages + class PushService < BaseService + def execute(container_image_name, event) + find_or_create_container_image(container_image_name).valid? + end + + private + + def find_or_create_container_image(container_image_name) + options = {name: container_image_name} + container_images.find_by(options) || + ::ContainerImagesRepositories::ContainerImages::CreateService.new(project, + current_user, options).execute + end + + def container_images_repository + @container_images_repository ||= project.container_images_repository + end + + def container_images + @container_images ||= container_images_repository.container_images + end + end + end +end diff --git a/app/services/container_images_repositories/create_service.rb b/app/services/container_images_repositories/create_service.rb new file mode 100644 index 00000000000..7e9dd3abe5f --- /dev/null +++ b/app/services/container_images_repositories/create_service.rb @@ -0,0 +1,7 @@ +module ContainerImagesRepositories + class CreateService < BaseService + def execute + project.container_images_repository || ::ContainerImagesRepository.create(project: project) + end + end +end diff --git a/db/migrate/20161029153736_create_container_images_repository.rb b/db/migrate/20161029153736_create_container_images_repository.rb new file mode 100644 index 00000000000..d93180b1674 --- /dev/null +++ b/db/migrate/20161029153736_create_container_images_repository.rb @@ -0,0 +1,31 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateContainerImagesRepository < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :container_images_repositories do |t| + t.integer :project_id, null: false + end + end +end diff --git a/db/migrate/20161031013926_create_container_image.rb b/db/migrate/20161031013926_create_container_image.rb new file mode 100644 index 00000000000..94feae280a6 --- /dev/null +++ b/db/migrate/20161031013926_create_container_image.rb @@ -0,0 +1,32 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateContainerImage < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :container_images do |t| + t.integer :container_images_repository_id + t.string :name + end + end +end diff --git a/lib/api/api.rb b/lib/api/api.rb index a0282ff8deb..ed775f898d2 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -84,6 +84,7 @@ module API mount ::API::Namespaces mount ::API::Notes mount ::API::NotificationSettings + mount ::API::RegistryEvents mount ::API::Pipelines mount ::API::ProjectHooks mount ::API::Projects diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb new file mode 100644 index 00000000000..c0473051424 --- /dev/null +++ b/lib/api/registry_events.rb @@ -0,0 +1,52 @@ +module API + # RegistryEvents API + class RegistryEvents < Grape::API + # before { authenticate! } + + content_type :json, 'application/vnd.docker.distribution.events.v1+json' + + params do + requires :events, type: Array, desc: 'The ID of a project' do + requires :id, type: String, desc: 'The ID of the event' + requires :timestamp, type: String, desc: 'Timestamp of the event' + requires :action, type: String, desc: 'Action performed by event' + requires :target, type: Hash, desc: 'Target of the event' do + optional :mediaType, type: String, desc: 'Media type of the target' + optional :size, type: Integer, desc: 'Size in bytes of the target' + requires :digest, type: String, desc: 'Digest of the target' + requires :repository, type: String, desc: 'Repository of target' + optional :url, type: String, desc: 'Url of the target' + optional :tag, type: String, desc: 'Tag of the target' + end + requires :request, type: Hash, desc: 'Request of the event' do + requires :id, type: String, desc: 'The ID of the request' + optional :addr, type: String, desc: 'IP Address of the request client' + optional :host, type: String, desc: 'Hostname of the registry instance' + requires :method, type: String, desc: 'Request method' + requires :useragent, type: String, desc: 'UserAgent header of the request' + end + requires :actor, type: Hash, desc: 'Actor that initiated the event' do + optional :name, type: String, desc: 'Actor name' + end + requires :source, type: Hash, desc: 'Source of the event' do + optional :addr, type: String, desc: 'Hostname of source registry node' + optional :instanceID, type: String, desc: 'Source registry node instanceID' + end + end + end + resource :registry_events do + post do + params['events'].each do |event| + repository = event['target']['repository'] + + if event['action'] == 'push' and !!event['target']['tag'] + namespace, container_image_name = ContainerImage::split_namespace(repository) + ::ContainerImagesRepositories::ContainerImages::PushService.new( + Project::find_with_namespace(namespace), current_user + ).execute(container_image_name, event) + end + end + end + end + end +end diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index 2edddb84fc3..2cbb7bfb67d 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -15,6 +15,10 @@ module ContainerRegistry @options = options end + def update_token(token) + @options[:token] = token + end + def repository_tags(name) response_body faraday.get("/v2/#{name}/tags/list") end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 59040199920..68dd87c979d 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -22,9 +22,7 @@ module ContainerRegistry end def manifest - return @manifest if defined?(@manifest) - - @manifest = client.repository_manifest(repository.name, name) + @manifest ||= client.repository_manifest(repository.name_with_namespace, name) end def path @@ -40,7 +38,7 @@ module ContainerRegistry def digest return @digest if defined?(@digest) - @digest = client.repository_tag_digest(repository.name, name) + @digest = client.repository_tag_digest(repository.name_with_namespace, name) end def config_blob @@ -82,7 +80,7 @@ module ContainerRegistry def delete return unless digest - client.delete_repository_tag(repository.name, digest) + client.delete_repository_tag(repository.name_with_namespace, digest) end end end -- cgit v1.2.1 From eed0b85ad084ad4d13cc26907102063d9372fe75 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 23 Nov 2016 14:50:30 -0200 Subject: First iteration of container_image view - Fixes project, container_image and tag deletion - Removed container_images_repository [ci skip] --- .../stylesheets/pages/container_registry.scss | 16 ++++++++++ .../projects/container_registry_controller.rb | 28 ++++++++++++----- app/models/container_image.rb | 20 +++++++++---- app/models/container_images_repository.rb | 26 ---------------- app/models/project.rb | 20 ++++++++----- app/services/container_images/destroy_service.rb | 32 ++++++++++++++++++++ .../container_images/create_service.rb | 16 ---------- .../container_images/destroy_service.rb | 11 ------- .../container_images/push_service.rb | 26 ---------------- .../create_service.rb | 7 ----- app/services/projects/destroy_service.rb | 10 ------- .../projects/container_registry/_image.html.haml | 35 ++++++++++++++++++++++ .../projects/container_registry/_tag.html.haml | 2 +- .../projects/container_registry/index.html.haml | 20 +++---------- ...029153736_create_container_images_repository.rb | 31 ------------------- .../20161031013926_create_container_image.rb | 2 +- lib/api/registry_events.rb | 16 ++++++++-- 17 files changed, 149 insertions(+), 169 deletions(-) create mode 100644 app/assets/stylesheets/pages/container_registry.scss delete mode 100644 app/models/container_images_repository.rb create mode 100644 app/services/container_images/destroy_service.rb delete mode 100644 app/services/container_images_repositories/container_images/create_service.rb delete mode 100644 app/services/container_images_repositories/container_images/destroy_service.rb delete mode 100644 app/services/container_images_repositories/container_images/push_service.rb delete mode 100644 app/services/container_images_repositories/create_service.rb create mode 100644 app/views/projects/container_registry/_image.html.haml delete mode 100644 db/migrate/20161029153736_create_container_images_repository.rb diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss new file mode 100644 index 00000000000..7d68eae3c97 --- /dev/null +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -0,0 +1,16 @@ +/** + * Container Registry + */ + +.container-image { + border-bottom: 1px solid #f0f0f0; +} + +.container-image-head { + padding: 0px 16px; + line-height: 4; +} + +.table.tags { + margin-bottom: 0px; +} diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index d1f46497207..54bcb5f504a 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -5,17 +5,22 @@ class Projects::ContainerRegistryController < Projects::ApplicationController layout 'project' def index - @tags = container_registry_repository.tags + @images = project.container_images end def destroy url = namespace_project_container_registry_index_path(project.namespace, project) - if tag.delete - redirect_to url + if tag + delete_tag(url) else - redirect_to url, alert: 'Failed to remove tag' + if image.destroy + redirect_to url + else + redirect_to url, alert: 'Failed to remove image' + end end + end private @@ -24,11 +29,20 @@ class Projects::ContainerRegistryController < Projects::ApplicationController render_404 unless Gitlab.config.registry.enabled end - def container_registry_repository - @container_registry_repository ||= project.container_registry_repository + def delete_tag(url) + if tag.delete + image.destroy if image.tags.empty? + redirect_to url + else + redirect_to url, alert: 'Failed to remove tag' + end + end + + def image + @image ||= project.container_images.find_by(id: params[:id]) end def tag - @tag ||= container_registry_repository.tag(params[:id]) + @tag ||= image.tag(params[:tag]) if params[:tag].present? end end diff --git a/app/models/container_image.rb b/app/models/container_image.rb index dcc4a7af629..7721c53a6fc 100644 --- a/app/models/container_image.rb +++ b/app/models/container_image.rb @@ -1,23 +1,28 @@ class ContainerImage < ActiveRecord::Base - belongs_to :container_images_repository + belongs_to :project - delegate :registry, :registry_path_with_namespace, :client, to: :container_images_repository + delegate :container_registry, :container_registry_allowed_paths, + :container_registry_path_with_namespace, to: :project + + delegate :client, to: :container_registry validates :manifest, presence: true + before_destroy :delete_tags + before_validation :update_token, on: :create def update_token - paths = container_images_repository.allowed_paths << name_with_namespace + paths = container_registry_allowed_paths << name_with_namespace token = Auth::ContainerRegistryAuthenticationService.full_access_token(paths) client.update_token(token) end def path - [registry.path, name_with_namespace].compact.join('/') + [container_registry.path, name_with_namespace].compact.join('/') end def name_with_namespace - [registry_path_with_namespace, name].compact.join('/') + [container_registry_path_with_namespace, name].compact.join('/') end def tag(tag) @@ -44,7 +49,10 @@ class ContainerImage < ActiveRecord::Base def delete_tags return unless tags - tags.all?(&:delete) + digests = tags.map {|tag| tag.digest }.to_set + digests.all? do |digest| + client.delete_repository_tag(name_with_namespace, digest) + end end def self.split_namespace(full_path) diff --git a/app/models/container_images_repository.rb b/app/models/container_images_repository.rb deleted file mode 100644 index 99e94d2a6d0..00000000000 --- a/app/models/container_images_repository.rb +++ /dev/null @@ -1,26 +0,0 @@ -class ContainerImagesRepository < ActiveRecord::Base - - belongs_to :project - - has_many :container_images, dependent: :destroy - - delegate :client, to: :registry - - def registry_path_with_namespace - project.path_with_namespace.downcase - end - - def allowed_paths - @allowed_paths ||= [registry_path_with_namespace] + - container_images.map { |i| i.name_with_namespace } - end - - def registry - @registry ||= begin - token = Auth::ContainerRegistryAuthenticationService.full_access_token(allowed_paths) - url = Gitlab.config.registry.api_url - host_port = Gitlab.config.registry.host_port - ContainerRegistry::Registry.new(url, token: token, path: host_port) - end - end -end diff --git a/app/models/project.rb b/app/models/project.rb index 703e24eb79a..afaf2095a4c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -157,7 +157,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :project_feature, dependent: :destroy has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete - has_one :container_images_repository, dependent: :destroy + has_many :container_images, dependent: :destroy has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id @@ -405,15 +405,19 @@ class Project < ActiveRecord::Base path_with_namespace.downcase end - def container_registry_repository + def container_registry_allowed_paths + @container_registry_allowed_paths ||= [container_registry_path_with_namespace] + + container_images.map { |i| i.name_with_namespace } + end + + def container_registry return unless Gitlab.config.registry.enabled - @container_registry_repository ||= begin - token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_path_with_namespace) + @container_registry ||= begin + token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_allowed_paths) url = Gitlab.config.registry.api_url host_port = Gitlab.config.registry.host_port - registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) - registry.repository(container_registry_path_with_namespace) + ContainerRegistry::Registry.new(url, token: token, path: host_port) end end @@ -424,9 +428,9 @@ class Project < ActiveRecord::Base end def has_container_registry_tags? - return unless container_registry_repository + return unless container_images - container_registry_repository.tags.any? + container_images.first.tags.any? end def commit(ref = 'HEAD') diff --git a/app/services/container_images/destroy_service.rb b/app/services/container_images/destroy_service.rb new file mode 100644 index 00000000000..bc5b53fd055 --- /dev/null +++ b/app/services/container_images/destroy_service.rb @@ -0,0 +1,32 @@ +module ContainerImages + class DestroyService < BaseService + + class DestroyError < StandardError; end + + def execute(container_image) + @container_image = container_image + + return false unless can?(current_user, :remove_project, project) + + ContainerImage.transaction do + container_image.destroy! + + unless remove_container_image_tags + raise_error('Failed to remove container image tags. Please try again or contact administrator') + end + end + + true + end + + private + + def raise_error(message) + raise DestroyError.new(message) + end + + def remove_container_image_tags + container_image.delete_tags + end + end +end diff --git a/app/services/container_images_repositories/container_images/create_service.rb b/app/services/container_images_repositories/container_images/create_service.rb deleted file mode 100644 index 0c2c69d5183..00000000000 --- a/app/services/container_images_repositories/container_images/create_service.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ContainerImagesRepositories - module ContainerImages - class CreateService < BaseService - def execute - @container_image = container_images_repository.container_images.create(params) - @container_image if @container_image.valid? - end - - private - - def container_images_repository - @container_images_repository ||= project.container_images_repository - end - end - end -end diff --git a/app/services/container_images_repositories/container_images/destroy_service.rb b/app/services/container_images_repositories/container_images/destroy_service.rb deleted file mode 100644 index 91b8cfeea47..00000000000 --- a/app/services/container_images_repositories/container_images/destroy_service.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ContainerImagesRepositories - module ContainerImages - class DestroyService < BaseService - def execute(container_image) - return false unless container_image - - container_image.destroy - end - end - end -end diff --git a/app/services/container_images_repositories/container_images/push_service.rb b/app/services/container_images_repositories/container_images/push_service.rb deleted file mode 100644 index 2731cf1d52e..00000000000 --- a/app/services/container_images_repositories/container_images/push_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -module ContainerImagesRepositories - module ContainerImages - class PushService < BaseService - def execute(container_image_name, event) - find_or_create_container_image(container_image_name).valid? - end - - private - - def find_or_create_container_image(container_image_name) - options = {name: container_image_name} - container_images.find_by(options) || - ::ContainerImagesRepositories::ContainerImages::CreateService.new(project, - current_user, options).execute - end - - def container_images_repository - @container_images_repository ||= project.container_images_repository - end - - def container_images - @container_images ||= container_images_repository.container_images - end - end - end -end diff --git a/app/services/container_images_repositories/create_service.rb b/app/services/container_images_repositories/create_service.rb deleted file mode 100644 index 7e9dd3abe5f..00000000000 --- a/app/services/container_images_repositories/create_service.rb +++ /dev/null @@ -1,7 +0,0 @@ -module ContainerImagesRepositories - class CreateService < BaseService - def execute - project.container_images_repository || ::ContainerImagesRepository.create(project: project) - end - end -end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 9716a1780a9..ba410b79e8c 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -31,10 +31,6 @@ module Projects project.team.truncate project.destroy! - unless remove_registry_tags - raise_error('Failed to remove project container registry. Please try again or contact administrator') - end - unless remove_repository(repo_path) raise_error('Failed to remove project repository. Please try again or contact administrator') end @@ -68,12 +64,6 @@ module Projects end end - def remove_registry_tags - return true unless Gitlab.config.registry.enabled - - project.container_registry_repository.delete_tags - end - def raise_error(message) raise DestroyError.new(message) end diff --git a/app/views/projects/container_registry/_image.html.haml b/app/views/projects/container_registry/_image.html.haml new file mode 100644 index 00000000000..b1d62e34a97 --- /dev/null +++ b/app/views/projects/container_registry/_image.html.haml @@ -0,0 +1,35 @@ +- expanded = false +.container-image.js-toggle-container + .container-image-head + = link_to "#", class: "js-toggle-button" do + - if expanded + = icon("chevron-up") + - else + = icon("chevron-down") + + = escape_once(image.name) + = clipboard_button(clipboard_text: "docker pull #{image.path}") + .controls.hidden-xs.pull-right + = link_to namespace_project_container_registry_path(@project.namespace, @project, image.id), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = icon("trash cred") + + + .container-image-tags.js-toggle-content{ class: ("hide" unless expanded) } + - if image.tags.blank? + %li + .nothing-here-block No tags in Container Registry for this container image. + + - else + .table-holder + %table.table.tags + %thead + %tr + %th Name + %th Image ID + %th Size + %th Created + - if can?(current_user, :update_container_image, @project) + %th + + - image.tags.each do |tag| + = render 'tag', tag: tag diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index 10822b6184c..00345ec26de 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -25,5 +25,5 @@ - if can?(current_user, :update_container_image, @project) %td.content .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, tag.name), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index 993da27310f..f074ce6be6d 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -19,21 +19,9 @@ %br docker push #{escape_once(@project.container_registry_repository_url)} - - if @tags.blank? - %li - .nothing-here-block No images in Container Registry for this project. + - if @images.blank? + .nothing-here-block No container images in Container Registry for this project. - else - .table-holder - %table.table.tags - %thead - %tr - %th Name - %th Image ID - %th Size - %th Created - - if can?(current_user, :update_container_image, @project) - %th - - - @tags.each do |tag| - = render 'tag', tag: tag + - @images.each do |image| + = render 'image', image: image diff --git a/db/migrate/20161029153736_create_container_images_repository.rb b/db/migrate/20161029153736_create_container_images_repository.rb deleted file mode 100644 index d93180b1674..00000000000 --- a/db/migrate/20161029153736_create_container_images_repository.rb +++ /dev/null @@ -1,31 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class CreateContainerImagesRepository < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - # Set this constant to true if this migration requires downtime. - DOWNTIME = false - - # When a migration requires downtime you **must** uncomment the following - # constant and define a short and easy to understand explanation as to why the - # migration requires downtime. - # DOWNTIME_REASON = '' - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - - def change - create_table :container_images_repositories do |t| - t.integer :project_id, null: false - end - end -end diff --git a/db/migrate/20161031013926_create_container_image.rb b/db/migrate/20161031013926_create_container_image.rb index 94feae280a6..85c0913b8f3 100644 --- a/db/migrate/20161031013926_create_container_image.rb +++ b/db/migrate/20161031013926_create_container_image.rb @@ -25,7 +25,7 @@ class CreateContainerImage < ActiveRecord::Migration def change create_table :container_images do |t| - t.integer :container_images_repository_id + t.integer :project_id t.string :name end end diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb index c0473051424..dc7279d2b75 100644 --- a/lib/api/registry_events.rb +++ b/lib/api/registry_events.rb @@ -41,9 +41,19 @@ module API if event['action'] == 'push' and !!event['target']['tag'] namespace, container_image_name = ContainerImage::split_namespace(repository) - ::ContainerImagesRepositories::ContainerImages::PushService.new( - Project::find_with_namespace(namespace), current_user - ).execute(container_image_name, event) + project = Project::find_with_namespace(namespace) + + if project + container_image = project.container_images.find_or_create_by(name: container_image_name) + + if container_image.valid? + puts('Valid!') + else + render_api_error!({ error: "Failed to create container image!" }, 400) + end + else + not_found!('Project') + end end end end -- cgit v1.2.1 From 246df2bd1151d39a04ef553064144eb75ee3e980 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Tue, 13 Dec 2016 23:42:43 -0200 Subject: Adding registry endpoint authorization --- .../admin/application_settings_controller.rb | 6 +++++ .../admin/container_registry_controller.rb | 11 ++++++++ app/models/application_setting.rb | 6 +++++ app/views/admin/container_registry/show.html.haml | 31 ++++++++++++++++++++++ app/views/admin/dashboard/_head.html.haml | 4 +++ config/routes/admin.rb | 2 ++ ...egistry_access_token_to_application_settings.rb | 29 ++++++++++++++++++++ doc/administration/container_registry.md | 22 +++++++++++++-- doc/ci/docker/using_docker_build.md | 8 +++--- doc/user/project/container_registry.md | 19 +++++++------ lib/api/helpers.rb | 10 +++++++ lib/api/registry_events.rb | 2 +- 12 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 app/controllers/admin/container_registry_controller.rb create mode 100644 app/views/admin/container_registry/show.html.haml create mode 100644 db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index b0f5d4a9933..fb6df1a06d2 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -29,6 +29,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to :back end + def reset_container_registry_token + @application_setting.reset_container_registry_access_token! + flash[:notice] = 'New container registry access token has been generated!' + redirect_to :back + end + def clear_repository_check_states RepositoryCheck::ClearWorker.perform_async diff --git a/app/controllers/admin/container_registry_controller.rb b/app/controllers/admin/container_registry_controller.rb new file mode 100644 index 00000000000..265c032c67d --- /dev/null +++ b/app/controllers/admin/container_registry_controller.rb @@ -0,0 +1,11 @@ +class Admin::ContainerRegistryController < Admin::ApplicationController + def show + @access_token = container_registry_access_token + end + + private + + def container_registry_access_token + current_application_settings.container_registry_access_token + end +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 74b358d8c40..b94a71e1ea7 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -4,6 +4,7 @@ class ApplicationSetting < ActiveRecord::Base add_authentication_token_field :runners_registration_token add_authentication_token_field :health_check_access_token + add_authentication_token_field :container_registry_access_token CACHE_KEY = 'application_setting.last' DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace @@ -141,6 +142,7 @@ class ApplicationSetting < ActiveRecord::Base before_save :ensure_runners_registration_token before_save :ensure_health_check_access_token + before_save :ensure_container_registry_access_token after_commit do Rails.cache.write(CACHE_KEY, self) @@ -276,6 +278,10 @@ class ApplicationSetting < ActiveRecord::Base ensure_health_check_access_token! end + def container_registry_access_token + ensure_container_registry_access_token! + end + def sidekiq_throttling_enabled? return false unless sidekiq_throttling_column_exists? diff --git a/app/views/admin/container_registry/show.html.haml b/app/views/admin/container_registry/show.html.haml new file mode 100644 index 00000000000..8803eddda69 --- /dev/null +++ b/app/views/admin/container_registry/show.html.haml @@ -0,0 +1,31 @@ +- @no_container = true += render "admin/dashboard/head" + +%div{ class: container_class } + + %p.prepend-top-default + %span + To properly configure the Container Registry you should add the following + access token to the Docker Registry config.yml as follows: + %pre + %code + :plain + notifications: + endpoints: + - ... + headers: + X-Registry-Token: [#{@access_token}] + %br + Access token is + %code{ id: 'registry-token' } #{@access_token} + + .bs-callout.clearfix + .pull-left + %p + You can reset container registry access token by pressing the button below. + %p + = button_to reset_container_registry_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset container registry token?' } do + = icon('refresh') + Reset container registry access token diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index 7893c1dee97..dbd039547fa 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -27,3 +27,7 @@ = link_to admin_runners_path, title: 'Runners' do %span Runners + = nav_link path: 'container_registry#show' do + = link_to admin_container_registry_path, title: 'Registry' do + %span + Registry diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 8e99239f350..b09c05826a7 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -58,6 +58,7 @@ namespace :admin do resource :background_jobs, controller: 'background_jobs', only: [:show] resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } + resource :container_registry, controller: 'container_registry', only: [:show] resources :projects, only: [:index] @@ -88,6 +89,7 @@ namespace :admin do resources :services, only: [:index, :edit, :update] put :reset_runners_token put :reset_health_check_token + put :reset_container_registry_token put :clear_repository_check_states end diff --git a/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb new file mode 100644 index 00000000000..f89f9b00a5f --- /dev/null +++ b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddContainerRegistryAccessTokenToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :application_settings, :container_registry_access_token, :string + end +end diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index a6300e18dc0..14795601246 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -76,7 +76,7 @@ you modify its settings. Read the upstream documentation on how to achieve that. At the absolute minimum, make sure your [Registry configuration][registry-auth] has `container_registry` as the service and `https://gitlab.example.com/jwt/auth` -as the realm: +as the realm. ``` auth: @@ -87,6 +87,23 @@ auth: rootcertbundle: /root/certs/certbundle ``` +Also a notification endpoint must be configured with the token from +Admin Area -> Overview -> Registry (`/admin/container_registry`) like in the following sample: + +``` +notifications: + endpoints: + - name: listener + url: https://gitlab.example.com/api/v3/registry_events + headers: + X-Registry-Token: [57Cx95fc2zHFh93VTiGD] + timeout: 500ms + threshold: 5 + backoff: 1s +``` + +Check the [Registry endpoint configuration][registry-endpoint] for details. + ## Container Registry domain configuration There are two ways you can configure the Registry's external domain. @@ -477,7 +494,7 @@ configurable in future releases. **GitLab 8.8 ([source docs][8-8-docs])** - GitLab Container Registry feature was introduced. - +i [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure [restart gitlab]: restart_gitlab.md#installations-from-source [wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate @@ -487,6 +504,7 @@ configurable in future releases. [storage-config]: https://docs.docker.com/registry/configuration/#storage [registry-http-config]: https://docs.docker.com/registry/configuration/#http [registry-auth]: https://docs.docker.com/registry/configuration/#auth +[registry-endpoint]: https://docs.docker.com/registry/notifications/#/configuration [token-config]: https://docs.docker.com/registry/configuration/#token [8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md [registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 8620984d40d..6ae6269b28a 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -299,8 +299,8 @@ could look like: stage: build script: - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com - - docker build -t registry.example.com/group/project:latest . - - docker push registry.example.com/group/project:latest + - docker build -t registry.example.com/group/project/image:latest . + - docker push registry.example.com/group/project/image:latest ``` You have to use the special `gitlab-ci-token` user created for you in order to @@ -350,8 +350,8 @@ stages: - deploy variables: - CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project:$CI_BUILD_REF_NAME - CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project:latest + CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_BUILD_REF_NAME + CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest before_script: - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 91b35c73b34..eada8e04227 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -10,6 +10,7 @@ - Starting from GitLab 8.12, if you have 2FA enabled in your account, you need to pass a personal access token instead of your password in order to login to GitLab's Container Registry. +- Multiple level image names support was added in GitLab ?8.15? With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. @@ -54,26 +55,23 @@ sure that you are using the Registry URL with the namespace and project name that is hosted on GitLab: ``` -docker build -t registry.example.com/group/project . -docker push registry.example.com/group/project +docker build -t registry.example.com/group/project/image . +docker push registry.example.com/group/project/image ``` Your image will be named after the following scheme: ``` -// +/// ``` -As such, the name of the image is unique, but you can differentiate the images -using tags. - ## Use images from GitLab Container Registry To download and run a container from images hosted in GitLab Container Registry, use `docker run`: ``` -docker run [options] registry.example.com/group/project [arguments] +docker run [options] registry.example.com/group/project/image [arguments] ``` For more information on running Docker containers, visit the @@ -87,7 +85,8 @@ and click **Registry** in the project menu. This view will show you all tags in your project and will easily allow you to delete them. -![Container Registry panel](img/container_registry_panel.png) +![Container Registry panel](image-needs-update) +[//]: # (img/container_registry_panel.png) ## Build and push images using GitLab CI @@ -136,7 +135,7 @@ A user attempted to enable an S3-backed Registry. The `docker login` step went fine. However, when pushing an image, the output showed: ``` -The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test] +The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test/docker-image] dc5e59c14160: Pushing [==================================================>] 14.85 kB 03c20c1a019a: Pushing [==================================================>] 2.048 kB a08f14ef632e: Pushing [==================================================>] 2.048 kB @@ -229,7 +228,7 @@ a container image. You may need to run as root to do this. For example: ```sh docker login s3-testing.myregistry.com:4567 -docker push s3-testing.myregistry.com:4567/root/docker-test +docker push s3-testing.myregistry.com:4567/root/docker-test/docker-image ``` In the example above, we see the following trace on the mitmproxy window: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a1db2099693..0fd2b1587e3 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -111,6 +111,16 @@ module API end end + def authenticate_container_registry_access_token! + token = request.headers['X-Registry-Token'] + unless token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare( + token, + current_application_settings.container_registry_access_token + ) + unauthorized! + end + end + def authenticated_as_admin! authenticate! forbidden! unless current_user.is_admin? diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb index dc7279d2b75..e52433339eb 100644 --- a/lib/api/registry_events.rb +++ b/lib/api/registry_events.rb @@ -1,7 +1,7 @@ module API # RegistryEvents API class RegistryEvents < Grape::API - # before { authenticate! } + before { authenticate_container_registry_access_token! } content_type :json, 'application/vnd.docker.distribution.events.v1+json' -- cgit v1.2.1 From e4fa80f3b67f1ef30c262cd4df28516ccff6336a Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Fri, 16 Dec 2016 01:24:05 -0200 Subject: Fixes broken and missing tests --- .../stylesheets/pages/container_registry.scss | 6 +- .../projects/container_registry_controller.rb | 1 - app/models/container_image.rb | 4 +- app/models/namespace.rb | 8 +-- app/models/project.rb | 18 ++---- .../container_registry_authentication_service.rb | 3 +- app/services/container_images/destroy_service.rb | 1 - app/services/projects/transfer_service.rb | 6 +- .../projects/container_registry/_image.html.haml | 2 +- .../projects/container_registry/_tag.html.haml | 2 +- .../projects/container_registry/index.html.haml | 4 +- db/schema.rb | 6 ++ lib/api/registry_events.rb | 4 +- lib/container_registry/blob.rb | 4 +- lib/container_registry/registry.rb | 4 -- lib/container_registry/repository.rb | 48 -------------- spec/factories/container_images.rb | 21 +++++++ spec/features/container_registry_spec.rb | 32 +++++++--- .../security/project/internal_access_spec.rb | 3 + .../security/project/private_access_spec.rb | 3 + .../security/project/public_access_spec.rb | 3 + spec/lib/container_registry/blob_spec.rb | 15 +++-- spec/lib/container_registry/registry_spec.rb | 2 +- spec/lib/container_registry/repository_spec.rb | 65 ------------------- spec/lib/container_registry/tag_spec.rb | 11 +++- spec/lib/gitlab/import_export/all_models.yml | 3 + spec/models/ci/build_spec.rb | 2 +- spec/models/container_image_spec.rb | 73 ++++++++++++++++++++++ spec/models/namespace_spec.rb | 8 ++- spec/models/project_spec.rb | 41 +++--------- spec/services/projects/destroy_service_spec.rb | 15 +++-- spec/services/projects/transfer_service_spec.rb | 3 + 32 files changed, 211 insertions(+), 210 deletions(-) delete mode 100644 lib/container_registry/repository.rb create mode 100644 spec/factories/container_images.rb delete mode 100644 spec/lib/container_registry/repository_spec.rb create mode 100644 spec/models/container_image_spec.rb diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss index 7d68eae3c97..92543d7d714 100644 --- a/app/assets/stylesheets/pages/container_registry.scss +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -3,14 +3,14 @@ */ .container-image { - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid $white-normal; } .container-image-head { - padding: 0px 16px; + padding: 0 16px; line-height: 4; } .table.tags { - margin-bottom: 0px; + margin-bottom: 0; } diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index 54bcb5f504a..f656f86fcdb 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -20,7 +20,6 @@ class Projects::ContainerRegistryController < Projects::ApplicationController redirect_to url, alert: 'Failed to remove image' end end - end private diff --git a/app/models/container_image.rb b/app/models/container_image.rb index 7721c53a6fc..583cb977910 100644 --- a/app/models/container_image.rb +++ b/app/models/container_image.rb @@ -22,7 +22,7 @@ class ContainerImage < ActiveRecord::Base end def name_with_namespace - [container_registry_path_with_namespace, name].compact.join('/') + [container_registry_path_with_namespace, name].reject(&:blank?).join('/') end def tag(tag) @@ -55,6 +55,8 @@ class ContainerImage < ActiveRecord::Base end end + # rubocop:disable RedundantReturn + def self.split_namespace(full_path) image_name = full_path.split('/').last namespace = full_path.gsub(/(.*)(#{Regexp.escape('/' + image_name)})/, '\1') diff --git a/app/models/namespace.rb b/app/models/namespace.rb index bd0336c984a..c8e329044e0 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -118,8 +118,8 @@ class Namespace < ActiveRecord::Base end def move_dir - if any_project_has_container_registry_tags? - raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') + if any_project_has_container_registry_images? + raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has images in container registry') end # Move the namespace directory in all storages paths used by member projects @@ -154,8 +154,8 @@ class Namespace < ActiveRecord::Base end end - def any_project_has_container_registry_tags? - projects.any?(&:has_container_registry_tags?) + def any_project_has_container_registry_images? + projects.any? { |project| project.container_images.present? } end def send_update_instructions diff --git a/app/models/project.rb b/app/models/project.rb index afaf2095a4c..d4f5584f53d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -421,18 +421,12 @@ class Project < ActiveRecord::Base end end - def container_registry_repository_url + def container_registry_url if Gitlab.config.registry.enabled "#{Gitlab.config.registry.host_port}/#{container_registry_path_with_namespace}" end end - def has_container_registry_tags? - return unless container_images - - container_images.first.tags.any? - end - def commit(ref = 'HEAD') repository.commit(ref) end @@ -913,11 +907,11 @@ class Project < ActiveRecord::Base expire_caches_before_rename(old_path_with_namespace) - if has_container_registry_tags? - Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present" + if container_images.present? + Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry images are present" - # we currently doesn't support renaming repository if it contains tags in container registry - raise StandardError.new('Project cannot be renamed, because tags are present in its container registry') + # we currently doesn't support renaming repository if it contains images in container registry + raise StandardError.new('Project cannot be renamed, because images are present in its container registry') end if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) @@ -1264,7 +1258,7 @@ class Project < ActiveRecord::Base ] if container_registry_enabled? - variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_repository_url, public: true } + variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_url, public: true } end variables diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 6b83b38fa4d..5b2fcdf3b16 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -16,7 +16,8 @@ module Auth { token: authorized_token(scope).encoded } end - def self.full_access_token(names) + def self.full_access_token(*names) + names = names.flatten registry = Gitlab.config.registry token = JSONWebToken::RSAToken.new(registry.key) token.issuer = registry.issuer diff --git a/app/services/container_images/destroy_service.rb b/app/services/container_images/destroy_service.rb index bc5b53fd055..c73b6cfefba 100644 --- a/app/services/container_images/destroy_service.rb +++ b/app/services/container_images/destroy_service.rb @@ -1,6 +1,5 @@ module ContainerImages class DestroyService < BaseService - class DestroyError < StandardError; end def execute(container_image) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 20dfbddc823..3e241b9e7c0 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -36,9 +36,9 @@ module Projects raise TransferError.new("Project with same path in target namespace already exists") end - if project.has_container_registry_tags? - # we currently doesn't support renaming repository if it contains tags in container registry - raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') + unless project.container_images.empty? + # we currently doesn't support renaming repository if it contains images in container registry + raise TransferError.new('Project cannot be transferred, because images are present in its container registry') end project.expire_caches_before_rename(old_path) diff --git a/app/views/projects/container_registry/_image.html.haml b/app/views/projects/container_registry/_image.html.haml index b1d62e34a97..5845efd345a 100644 --- a/app/views/projects/container_registry/_image.html.haml +++ b/app/views/projects/container_registry/_image.html.haml @@ -10,7 +10,7 @@ = escape_once(image.name) = clipboard_button(clipboard_text: "docker pull #{image.path}") .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, image.id), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_container_registry_path(@project.namespace, @project, image.id), class: 'btn btn-remove has-tooltip', title: "Remove image", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index 00345ec26de..b35a9cb621f 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -25,5 +25,5 @@ - if can?(current_user, :update_container_image, @project) %td.content .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove tag", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index f074ce6be6d..ab6213f03d8 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -15,9 +15,9 @@ %br Then you are free to create and upload a container image with build and push commands: %pre - docker build -t #{escape_once(@project.container_registry_repository_url)} . + docker build -t #{escape_once(@project.container_registry_url)} . %br - docker push #{escape_once(@project.container_registry_repository_url)} + docker push #{escape_once(@project.container_registry_url)} - if @images.blank? .nothing-here-block No container images in Container Registry for this project. diff --git a/db/schema.rb b/db/schema.rb index 88aaa6c3c55..36df20fc8f2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -109,6 +109,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" + t.string "container_registry_access_token" t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false end @@ -392,6 +393,11 @@ ActiveRecord::Schema.define(version: 20170215200045) do add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree + create_table "container_images", force: :cascade do |t| + t.integer "project_id" + t.string "name" + end + create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb index e52433339eb..12305a49f0f 100644 --- a/lib/api/registry_events.rb +++ b/lib/api/registry_events.rb @@ -46,9 +46,7 @@ module API if project container_image = project.container_images.find_or_create_by(name: container_image_name) - if container_image.valid? - puts('Valid!') - else + unless container_image.valid? render_api_error!({ error: "Failed to create container image!" }, 400) end else diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb index eb5a2596177..8db8e483b1d 100644 --- a/lib/container_registry/blob.rb +++ b/lib/container_registry/blob.rb @@ -38,11 +38,11 @@ module ContainerRegistry end def delete - client.delete_blob(repository.name, digest) + client.delete_blob(repository.name_with_namespace, digest) end def data - @data ||= client.blob(repository.name, digest, type) + @data ||= client.blob(repository.name_with_namespace, digest, type) end end end diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb index 0e634f6b6ef..63bce655f57 100644 --- a/lib/container_registry/registry.rb +++ b/lib/container_registry/registry.rb @@ -8,10 +8,6 @@ module ContainerRegistry @client = ContainerRegistry::Client.new(uri, options) end - def repository(name) - ContainerRegistry::Repository.new(self, name) - end - private def default_path diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb deleted file mode 100644 index 0e4a7cb3cc9..00000000000 --- a/lib/container_registry/repository.rb +++ /dev/null @@ -1,48 +0,0 @@ -module ContainerRegistry - class Repository - attr_reader :registry, :name - - delegate :client, to: :registry - - def initialize(registry, name) - @registry, @name = registry, name - end - - def path - [registry.path, name].compact.join('/') - end - - def tag(tag) - ContainerRegistry::Tag.new(self, tag) - end - - def manifest - return @manifest if defined?(@manifest) - - @manifest = client.repository_tags(name) - end - - def valid? - manifest.present? - end - - def tags - return @tags if defined?(@tags) - return [] unless manifest && manifest['tags'] - - @tags = manifest['tags'].map do |tag| - ContainerRegistry::Tag.new(self, tag) - end - end - - def blob(config) - ContainerRegistry::Blob.new(self, config) - end - - def delete_tags - return unless tags - - tags.all?(&:delete) - end - end -end diff --git a/spec/factories/container_images.rb b/spec/factories/container_images.rb new file mode 100644 index 00000000000..6141a519a75 --- /dev/null +++ b/spec/factories/container_images.rb @@ -0,0 +1,21 @@ +FactoryGirl.define do + factory :container_image do + name "test_container_image" + project + + transient do + tags ['tag'] + stubbed true + end + + after(:build) do |image, evaluator| + if evaluator.stubbed + allow(Gitlab.config.registry).to receive(:enabled).and_return(true) + allow(image.client).to receive(:repository_tags).and_return({ + name: image.name_with_namespace, + tags: evaluator.tags + }) + end + end + end +end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index 203e55a36f2..862c9fbf6c0 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -2,15 +2,18 @@ require 'spec_helper' describe "Container Registry" do let(:project) { create(:empty_project) } - let(:repository) { project.container_registry_repository } + let(:registry) { project.container_registry } let(:tag_name) { 'latest' } let(:tags) { [tag_name] } + let(:container_image) { create(:container_image) } + let(:image_name) { container_image.name } before do login_as(:user) project.team << [@user, :developer] - stub_container_registry_tags(*tags) stub_container_registry_config(enabled: true) + stub_container_registry_tags(*tags) + project.container_images << container_image unless container_image.nil? allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') end @@ -19,15 +22,26 @@ describe "Container Registry" do visit namespace_project_container_registry_index_path(project.namespace, project) end - context 'when no tags' do - let(:tags) { [] } + context 'when no images' do + let(:container_image) { } + + it { expect(page).to have_content('No container images in Container Registry for this project') } + end - it { expect(page).to have_content('No images in Container Registry for this project') } + context 'when there are images' do + it { expect(page).to have_content(image_name) } end + end + + describe 'DELETE /:project/container_registry/:image_id' do + before do + visit namespace_project_container_registry_index_path(project.namespace, project) + end + + it do + expect_any_instance_of(ContainerImage).to receive(:delete_tags).and_return(true) - context 'when there are tags' do - it { expect(page).to have_content(tag_name) } - it { expect(page).to have_content('d7a513a66') } + click_on 'Remove image' end end @@ -39,7 +53,7 @@ describe "Container Registry" do it do expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true) - click_on 'Remove' + click_on 'Remove tag' end end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 24af062d763..4e7a2c0ecc0 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -429,9 +429,12 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/container_registry" do + let(:container_image) { create(:container_image) } + before do stub_container_registry_tags('latest') stub_container_registry_config(enabled: true) + project.container_images << container_image end subject { namespace_project_container_registry_index_path(project.namespace, project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index c511dcfa18e..c74cdc05593 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -418,9 +418,12 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/container_registry" do + let(:container_image) { create(:container_image) } + before do stub_container_registry_tags('latest') stub_container_registry_config(enabled: true) + project.container_images << container_image end subject { namespace_project_container_registry_index_path(project.namespace, project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index d8cc012c27e..485ef335b78 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -429,9 +429,12 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/container_registry" do + let(:container_image) { create(:container_image) } + before do stub_container_registry_tags('latest') stub_container_registry_config(enabled: true) + project.container_images << container_image end subject { namespace_project_container_registry_index_path(project.namespace, project) } diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb index bbacdc67ebd..f092449c4bd 100644 --- a/spec/lib/container_registry/blob_spec.rb +++ b/spec/lib/container_registry/blob_spec.rb @@ -9,12 +9,19 @@ describe ContainerRegistry::Blob do 'size' => 1000 } end - let(:token) { 'authorization-token' } - - let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) } - let(:repository) { registry.repository('group/test') } + let(:token) { 'token' } + + let(:group) { create(:group, name: 'group') } + let(:project) { create(:project, path: 'test', group: group) } + let(:example_host) { 'example.com' } + let(:registry_url) { 'http://' + example_host } + let(:repository) { create(:container_image, name: '', project: project) } let(:blob) { repository.blob(config) } + before do + stub_container_registry_config(enabled: true, api_url: registry_url, host_port: example_host) + end + it { expect(blob).to respond_to(:repository) } it { expect(blob).to delegate_method(:registry).to(:repository) } it { expect(blob).to delegate_method(:client).to(:repository) } diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb index 4f3f8b24fc4..4d6eea94bf0 100644 --- a/spec/lib/container_registry/registry_spec.rb +++ b/spec/lib/container_registry/registry_spec.rb @@ -10,7 +10,7 @@ describe ContainerRegistry::Registry do it { is_expected.to respond_to(:uri) } it { is_expected.to respond_to(:path) } - it { expect(subject.repository('test')).not_to be_nil } + it { expect(subject).not_to be_nil } context '#path' do subject { registry.path } diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb deleted file mode 100644 index c364e759108..00000000000 --- a/spec/lib/container_registry/repository_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'spec_helper' - -describe ContainerRegistry::Repository do - let(:registry) { ContainerRegistry::Registry.new('http://example.com') } - let(:repository) { registry.repository('group/test') } - - it { expect(repository).to respond_to(:registry) } - it { expect(repository).to delegate_method(:client).to(:registry) } - it { expect(repository.tag('test')).not_to be_nil } - - context '#path' do - subject { repository.path } - - it { is_expected.to eq('example.com/group/test') } - end - - context 'manifest processing' do - before do - stub_request(:get, 'http://example.com/v2/group/test/tags/list'). - with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }). - to_return( - status: 200, - body: JSON.dump(tags: ['test']), - headers: { 'Content-Type' => 'application/json' }) - end - - context '#manifest' do - subject { repository.manifest } - - it { is_expected.not_to be_nil } - end - - context '#valid?' do - subject { repository.valid? } - - it { is_expected.to be_truthy } - end - - context '#tags' do - subject { repository.tags } - - it { is_expected.not_to be_empty } - end - end - - context '#delete_tags' do - let(:tag) { ContainerRegistry::Tag.new(repository, 'tag') } - - before { expect(repository).to receive(:tags).twice.and_return([tag]) } - - subject { repository.delete_tags } - - context 'succeeds' do - before { expect(tag).to receive(:delete).and_return(true) } - - it { is_expected.to be_truthy } - end - - context 'any fails' do - before { expect(tag).to receive(:delete).and_return(false) } - - it { is_expected.to be_falsey } - end - end -end diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb index c5e31ae82b6..cdd0fe66bc3 100644 --- a/spec/lib/container_registry/tag_spec.rb +++ b/spec/lib/container_registry/tag_spec.rb @@ -1,11 +1,18 @@ require 'spec_helper' describe ContainerRegistry::Tag do - let(:registry) { ContainerRegistry::Registry.new('http://example.com') } - let(:repository) { registry.repository('group/test') } + let(:group) { create(:group, name: 'group') } + let(:project) { create(:project, path: 'test', group: group) } + let(:example_host) { 'example.com' } + let(:registry_url) { 'http://' + example_host } + let(:repository) { create(:container_image, name: '', project: project) } let(:tag) { repository.tag('tag') } let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } } + before do + stub_container_registry_config(enabled: true, api_url: registry_url, host_port: example_host) + end + it { expect(tag).to respond_to(:repository) } it { expect(tag).to delegate_method(:registry).to(:repository) } it { expect(tag).to delegate_method(:client).to(:repository) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 06617f3b007..9c08f41fe82 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -114,6 +114,8 @@ merge_access_levels: - protected_branch push_access_levels: - protected_branch +container_images: +- name project: - taggings - base_tags @@ -197,6 +199,7 @@ project: - project_authorizations - route - statistics +- container_images award_emoji: - awardable - user diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 2dfca8bcfce..83a2efb55b9 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1397,7 +1397,7 @@ describe Ci::Build, :models do { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } end let(:ci_registry_image) do - { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true } + { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true } end context 'and is disabled for project' do diff --git a/spec/models/container_image_spec.rb b/spec/models/container_image_spec.rb new file mode 100644 index 00000000000..e0bea737f59 --- /dev/null +++ b/spec/models/container_image_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe ContainerImage do + let(:group) { create(:group, name: 'group') } + let(:project) { create(:project, path: 'test', group: group) } + let(:example_host) { 'example.com' } + let(:registry_url) { 'http://' + example_host } + let(:container_image) { create(:container_image, name: '', project: project, stubbed: false) } + + before do + stub_container_registry_config(enabled: true, api_url: registry_url, host_port: example_host) + stub_request(:get, 'http://example.com/v2/group/test/tags/list'). + with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }). + to_return( + status: 200, + body: JSON.dump(tags: ['test']), + headers: { 'Content-Type' => 'application/json' }) + end + + it { expect(container_image).to respond_to(:project) } + it { expect(container_image).to delegate_method(:container_registry).to(:project) } + it { expect(container_image).to delegate_method(:client).to(:container_registry) } + it { expect(container_image.tag('test')).not_to be_nil } + + context '#path' do + subject { container_image.path } + + it { is_expected.to eq('example.com/group/test') } + end + + context 'manifest processing' do + context '#manifest' do + subject { container_image.manifest } + + it { is_expected.not_to be_nil } + end + + context '#valid?' do + subject { container_image.valid? } + + it { is_expected.to be_truthy } + end + + context '#tags' do + subject { container_image.tags } + + it { is_expected.not_to be_empty } + end + end + + context '#delete_tags' do + let(:tag) { ContainerRegistry::Tag.new(container_image, 'tag') } + + before do + expect(container_image).to receive(:tags).twice.and_return([tag]) + expect(tag).to receive(:digest).and_return('sha256:4c8e63ca4cb663ce6c688cb06f1c3672a172b088dac5b6d7ad7d49cd620d85cf') + end + + subject { container_image.delete_tags } + + context 'succeeds' do + before { expect(container_image.client).to receive(:delete_repository_tag).and_return(true) } + + it { is_expected.to be_truthy } + end + + context 'any fails' do + before { expect(container_image.client).to receive(:delete_repository_tag).and_return(false) } + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 35d932f1c64..aeb4eeb0b55 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -134,18 +134,20 @@ describe Namespace, models: true do expect(@namespace.move_dir).to be_truthy end - context "when any project has container tags" do + context "when any project has container images" do + let(:container_image) { create(:container_image) } + before do stub_container_registry_config(enabled: true) stub_container_registry_tags('tag') - create(:empty_project, namespace: @namespace) + create(:empty_project, namespace: @namespace, container_images: [container_image]) allow(@namespace).to receive(:path_was).and_return(@namespace.path) allow(@namespace).to receive(:path).and_return('new_path') end - it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') } + it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has images in container registry') } end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b0087a9e15d..77f2ff3d17b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1173,10 +1173,13 @@ describe Project, models: true do project.rename_repo end - context 'container registry with tags' do + context 'container registry with images' do + let(:container_image) { create(:container_image) } + before do stub_container_registry_config(enabled: true) stub_container_registry_tags('tag') + project.container_images << container_image end subject { project.rename_repo } @@ -1383,20 +1386,20 @@ describe Project, models: true do it { is_expected.to eq(project.path_with_namespace.downcase) } end - describe '#container_registry_repository' do + describe '#container_registry' do let(:project) { create(:empty_project) } before { stub_container_registry_config(enabled: true) } - subject { project.container_registry_repository } + subject { project.container_registry } it { is_expected.not_to be_nil } end - describe '#container_registry_repository_url' do + describe '#container_registry_url' do let(:project) { create(:empty_project) } - subject { project.container_registry_repository_url } + subject { project.container_registry_url } before { stub_container_registry_config(**registry_settings) } @@ -1422,34 +1425,6 @@ describe Project, models: true do end end - describe '#has_container_registry_tags?' do - let(:project) { create(:empty_project) } - - subject { project.has_container_registry_tags? } - - context 'for enabled registry' do - before { stub_container_registry_config(enabled: true) } - - context 'with tags' do - before { stub_container_registry_tags('test', 'test2') } - - it { is_expected.to be_truthy } - end - - context 'when no tags' do - before { stub_container_registry_tags } - - it { is_expected.to be_falsey } - end - end - - context 'for disabled registry' do - before { stub_container_registry_config(enabled: false) } - - it { is_expected.to be_falsey } - end - end - describe '#latest_successful_builds_for' do def create_pipeline(status = 'success') create(:ci_pipeline, project: project, diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 74bfba44dfd..270e630e70e 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -90,25 +90,30 @@ describe Projects::DestroyService, services: true do end context 'container registry' do + let(:container_image) { create(:container_image) } + before do stub_container_registry_config(enabled: true) stub_container_registry_tags('tag') + project.container_images << container_image end - context 'tags deletion succeeds' do + context 'images deletion succeeds' do it do - expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true) + expect_any_instance_of(ContainerImage).to receive(:delete_tags).and_return(true) destroy_project(project, user, {}) end end - context 'tags deletion fails' do - before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) } + context 'images deletion fails' do + before do + expect_any_instance_of(ContainerImage).to receive(:delete_tags).and_return(false) + end subject { destroy_project(project, user, {}) } - it { expect{subject}.to raise_error(Projects::DestroyService::DestroyError) } + it { expect{subject}.to raise_error(ActiveRecord::RecordNotDestroyed) } end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 5c6fbea8d0e..5e56226ff91 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -29,9 +29,12 @@ describe Projects::TransferService, services: true do end context 'disallow transfering of project with tags' do + let(:container_image) { create(:container_image) } + before do stub_container_registry_config(enabled: true) stub_container_registry_tags('tag') + project.container_images << container_image end subject { transfer_project(project, user, group) } -- cgit v1.2.1 From 164ef8a348cac86097313bc453493ccf739adffe Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Fri, 16 Dec 2016 11:12:37 -0200 Subject: Fixing typos in docs --- doc/administration/container_registry.md | 4 ++-- doc/user/project/container_registry.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 14795601246..4d1cb391e69 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -76,7 +76,7 @@ you modify its settings. Read the upstream documentation on how to achieve that. At the absolute minimum, make sure your [Registry configuration][registry-auth] has `container_registry` as the service and `https://gitlab.example.com/jwt/auth` -as the realm. +as the realm: ``` auth: @@ -494,7 +494,7 @@ configurable in future releases. **GitLab 8.8 ([source docs][8-8-docs])** - GitLab Container Registry feature was introduced. -i + [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure [restart gitlab]: restart_gitlab.md#installations-from-source [wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index eada8e04227..c5b2266ff19 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -10,7 +10,7 @@ - Starting from GitLab 8.12, if you have 2FA enabled in your account, you need to pass a personal access token instead of your password in order to login to GitLab's Container Registry. -- Multiple level image names support was added in GitLab ?8.15? +- Multiple level image names support was added in GitLab 8.15 With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. -- cgit v1.2.1 From b408a192e0fbf630d4f9a4112f6835be50a681d8 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Fri, 16 Dec 2016 12:07:20 -0200 Subject: Adding mock for full_access_token --- spec/factories/container_images.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/factories/container_images.rb b/spec/factories/container_images.rb index 6141a519a75..3693865101d 100644 --- a/spec/factories/container_images.rb +++ b/spec/factories/container_images.rb @@ -11,6 +11,7 @@ FactoryGirl.define do after(:build) do |image, evaluator| if evaluator.stubbed allow(Gitlab.config.registry).to receive(:enabled).and_return(true) + allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') allow(image.client).to receive(:repository_tags).and_return({ name: image.name_with_namespace, tags: evaluator.tags -- cgit v1.2.1 From ea17df5c4c23890c48cd51af17e2517f04f7c88b Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 25 Jan 2017 10:02:09 -0200 Subject: Fixing minor view issues --- app/views/projects/container_registry/_image.html.haml | 6 +++--- app/views/projects/container_registry/_tag.html.haml | 2 +- app/views/projects/container_registry/index.html.haml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/projects/container_registry/_image.html.haml b/app/views/projects/container_registry/_image.html.haml index 5845efd345a..72f2103b862 100644 --- a/app/views/projects/container_registry/_image.html.haml +++ b/app/views/projects/container_registry/_image.html.haml @@ -8,7 +8,7 @@ = icon("chevron-down") = escape_once(image.name) - = clipboard_button(clipboard_text: "docker pull #{image.path}") + = clipboard_button(clipboard_text: "docker pull #{image.path}") .controls.hidden-xs.pull-right = link_to namespace_project_container_registry_path(@project.namespace, @project, image.id), class: 'btn btn-remove has-tooltip', title: "Remove image", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") @@ -24,8 +24,8 @@ %table.table.tags %thead %tr - %th Name - %th Image ID + %th Tag + %th Tag ID %th Size %th Created - if can?(current_user, :update_container_image, @project) diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index b35a9cb621f..f7161e85428 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -25,5 +25,5 @@ - if can?(current_user, :update_container_image, @project) %td.content .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove tag", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove tag", data: { confirm: "Due to a Docker limitation, all tags with the same ID will also be deleted. Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index ab6213f03d8..5508a3de396 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -15,9 +15,9 @@ %br Then you are free to create and upload a container image with build and push commands: %pre - docker build -t #{escape_once(@project.container_registry_url)} . + docker build -t #{escape_once(@project.container_registry_url)}/image . %br - docker push #{escape_once(@project.container_registry_url)} + docker push #{escape_once(@project.container_registry_url)}/image - if @images.blank? .nothing-here-block No container images in Container Registry for this project. -- cgit v1.2.1 From 8294756fc110fdb84036e4ae097940410a8ad6de Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 25 Jan 2017 10:24:50 -0200 Subject: Improved readability in tag/image delete condition --- .../projects/container_registry_controller.rb | 28 +++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index f656f86fcdb..4981e57ed22 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -9,31 +9,37 @@ class Projects::ContainerRegistryController < Projects::ApplicationController end def destroy - url = namespace_project_container_registry_index_path(project.namespace, project) - if tag - delete_tag(url) + delete_tag else - if image.destroy - redirect_to url - else - redirect_to url, alert: 'Failed to remove image' - end + delete_image end end private + def registry_url + @registry_url ||= namespace_project_container_registry_index_path(project.namespace, project) + end + def verify_registry_enabled render_404 unless Gitlab.config.registry.enabled end - def delete_tag(url) + def delete_image + if image.destroy + redirect_to registry_url + else + redirect_to registry_url, alert: 'Failed to remove image' + end + end + + def delete_tag if tag.delete image.destroy if image.tags.empty? - redirect_to url + redirect_to registry_url else - redirect_to url, alert: 'Failed to remove tag' + redirect_to registry_url, alert: 'Failed to remove tag' end end -- cgit v1.2.1 From db5b4b8b1a9b8aa07c8310dde53b7c3ed391bafd Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 22 Feb 2017 11:19:23 -0300 Subject: Creates specs for destroy service and improves namespace container image query performance --- app/models/namespace.rb | 2 +- app/services/container_images/destroy_service.rb | 26 ++--------------- app/views/admin/container_registry/show.html.haml | 2 +- .../20161031013926_create_container_image.rb | 16 ---------- ...egistry_access_token_to_application_settings.rb | 16 ---------- db/schema.rb | 10 +++---- lib/api/registry_events.rb | 4 +-- .../container_images/destroy_service_spec.rb | 34 ++++++++++++++++++++++ 8 files changed, 45 insertions(+), 65 deletions(-) create mode 100644 spec/services/container_images/destroy_service_spec.rb diff --git a/app/models/namespace.rb b/app/models/namespace.rb index c8e329044e0..a803be2e780 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -155,7 +155,7 @@ class Namespace < ActiveRecord::Base end def any_project_has_container_registry_images? - projects.any? { |project| project.container_images.present? } + projects.joins(:container_images).any? end def send_update_instructions diff --git a/app/services/container_images/destroy_service.rb b/app/services/container_images/destroy_service.rb index c73b6cfefba..15dca227291 100644 --- a/app/services/container_images/destroy_service.rb +++ b/app/services/container_images/destroy_service.rb @@ -1,31 +1,9 @@ module ContainerImages class DestroyService < BaseService - class DestroyError < StandardError; end - def execute(container_image) - @container_image = container_image - - return false unless can?(current_user, :remove_project, project) - - ContainerImage.transaction do - container_image.destroy! - - unless remove_container_image_tags - raise_error('Failed to remove container image tags. Please try again or contact administrator') - end - end - - true - end - - private - - def raise_error(message) - raise DestroyError.new(message) - end + return false unless can?(current_user, :update_container_image, project) - def remove_container_image_tags - container_image.delete_tags + container_image.destroy! end end end diff --git a/app/views/admin/container_registry/show.html.haml b/app/views/admin/container_registry/show.html.haml index 8803eddda69..ffaa7736d65 100644 --- a/app/views/admin/container_registry/show.html.haml +++ b/app/views/admin/container_registry/show.html.haml @@ -17,7 +17,7 @@ X-Registry-Token: [#{@access_token}] %br Access token is - %code{ id: 'registry-token' } #{@access_token} + %code{ id: 'registry-token' }= @access_token .bs-callout.clearfix .pull-left diff --git a/db/migrate/20161031013926_create_container_image.rb b/db/migrate/20161031013926_create_container_image.rb index 85c0913b8f3..884c78880eb 100644 --- a/db/migrate/20161031013926_create_container_image.rb +++ b/db/migrate/20161031013926_create_container_image.rb @@ -7,22 +7,6 @@ class CreateContainerImage < ActiveRecord::Migration # Set this constant to true if this migration requires downtime. DOWNTIME = false - # When a migration requires downtime you **must** uncomment the following - # constant and define a short and easy to understand explanation as to why the - # migration requires downtime. - # DOWNTIME_REASON = '' - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def change create_table :container_images do |t| t.integer :project_id diff --git a/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb index f89f9b00a5f..23d87cc6d0a 100644 --- a/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb +++ b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb @@ -7,22 +7,6 @@ class AddContainerRegistryAccessTokenToApplicationSettings < ActiveRecord::Migra # Set this constant to true if this migration requires downtime. DOWNTIME = false - # When a migration requires downtime you **must** uncomment the following - # constant and define a short and easy to understand explanation as to why the - # migration requires downtime. - # DOWNTIME_REASON = '' - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def change add_column :application_settings, :container_registry_access_token, :string end diff --git a/db/schema.rb b/db/schema.rb index 36df20fc8f2..08d11546800 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -61,6 +61,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.boolean "shared_runners_enabled", default: true, null: false t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" + t.integer "max_pages_size", default: 100, null: false t.boolean "require_two_factor_authentication", default: false t.integer "two_factor_grace_period", default: 48 t.boolean "metrics_enabled", default: false @@ -107,10 +108,9 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.string "sidekiq_throttling_queues" t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true + t.string "container_registry_access_token" t.string "plantuml_url" t.boolean "plantuml_enabled" - t.string "container_registry_access_token" - t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false end @@ -586,9 +586,9 @@ ActiveRecord::Schema.define(version: 20170215200045) do end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree - add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["title"], name: "index_labels_on_title", using: :btree + add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false @@ -761,8 +761,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.boolean "lfs_enabled" t.text "description_html" + t.boolean "lfs_enabled" t.integer "parent_id" end @@ -1283,8 +1283,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "organization" t.string "incoming_email_token" + t.string "organization" t.boolean "authorized_projects_populated" t.boolean "notified_of_own_activity", default: false, null: false end diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb index 12305a49f0f..fc6fc0b97e0 100644 --- a/lib/api/registry_events.rb +++ b/lib/api/registry_events.rb @@ -39,9 +39,9 @@ module API params['events'].each do |event| repository = event['target']['repository'] - if event['action'] == 'push' and !!event['target']['tag'] + if event['action'] == 'push' && !!event['target']['tag'] namespace, container_image_name = ContainerImage::split_namespace(repository) - project = Project::find_with_namespace(namespace) + project = Project::find_by_full_path(namespace) if project container_image = project.container_images.find_or_create_by(name: container_image_name) diff --git a/spec/services/container_images/destroy_service_spec.rb b/spec/services/container_images/destroy_service_spec.rb new file mode 100644 index 00000000000..5b4dbaa7934 --- /dev/null +++ b/spec/services/container_images/destroy_service_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe ContainerImages::DestroyService, services: true do + describe '#execute' do + let(:user) { create(:user) } + let(:container_image) { create(:container_image, name: '') } + let(:project) { create(:project, path: 'test', namespace: user.namespace, container_images: [container_image]) } + let(:example_host) { 'example.com' } + let(:registry_url) { 'http://' + example_host } + + it { expect(container_image).to be_valid } + it { expect(project.container_images).not_to be_empty } + + context 'when container image has tags' do + before do + project.team << [user, :master] + end + + it 'removes all tags before destroy' do + service = described_class.new(project, user) + + expect(container_image).to receive(:delete_tags).and_return(true) + expect { service.execute(container_image) }.to change(project.container_images, :count).by(-1) + end + + it 'fails when tags are not removed' do + service = described_class.new(project, user) + + expect(container_image).to receive(:delete_tags).and_return(false) + expect { service.execute(container_image) }.to raise_error(ActiveRecord::RecordNotDestroyed) + end + end + end +end -- cgit v1.2.1 From 5852e0e0605e90949aec817293f45fabf5b116ac Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Fri, 24 Feb 2017 12:21:41 +0100 Subject: Suggest a more secure way of handling SSH host keys in docker builds --- doc/ci/ssh_keys/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 49e7ac38b26..688a69d77ba 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -38,6 +38,15 @@ following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the content of your _private_ key that you created earlier. +It is also good practice to check the server's own public key to make sure you +are not being targeted by a man-in-the-middle attack. To do this, add another +variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run +the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the +server itself), and paste its output into the `SSH_SERVER_HOSTKEY` variable. If +you need to connect to multiple servers, concatenate all the server public keys +that you collected into the **Value** of the variable. There must be one key per +line. + Next you need to modify your `.gitlab-ci.yml` with a `before_script` action. Add it to the top: @@ -59,6 +68,11 @@ before_script: # you will overwrite your user's SSH config. - mkdir -p ~/.ssh - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' + # In order to properly check the server's host key, assuming you created the + # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines + # instead. + # - mkdir -p ~/.ssh + # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts' ``` As a final step, add the _public_ key from the one you created earlier to the -- cgit v1.2.1 From 7951b8469d81f58132f69ad3a1e71fbd39ef1f49 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 24 Feb 2017 23:11:50 +0530 Subject: Document U2F limitations with multiple hostnames/FQDNs. --- .../28277-document-u2f-limitations-with-multiple-urls.yml | 4 ++++ doc/user/profile/account/two_factor_authentication.md | 11 +++++++++++ 2 files changed, 15 insertions(+) create mode 100644 changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml diff --git a/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml new file mode 100644 index 00000000000..6e3cd8a60d8 --- /dev/null +++ b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml @@ -0,0 +1,4 @@ +--- +title: Document U2F limitations with multiple URLs +merge_request: 9300 +author: diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index eaa39a0c4ea..63a3d3c472e 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -215,3 +215,14 @@ you may have cases where authorization always fails because of time differences. [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en [FreeOTP]: https://freeotp.github.io/ [YubiKey]: https://www.yubico.com/products/yubikey-hardware/ + +- The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from +multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at +the time of registration, and cannot be used for other hostnames/FQDNs. + + For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`: + + - The user logs in via `first.host.xyz` and registers their U2F key. + - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds. + - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because + the U2F key has only been registered on `first.host.xyz`. -- cgit v1.2.1 From 98a691629d93b2ff3dfbc060b3e25b8ebec6e34b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 29 Nov 2016 13:50:57 +0530 Subject: Hide form inputs for user without access --- app/views/shared/members/_member.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 8e721c9c8dd..a797e8f0799 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -24,6 +24,7 @@ = link_to source.full_name, source, class: "member-group-link" .hidden-xs.cgray + - expires_soon = member.expires_soon? - if member.request? Requested = time_ago_with_tooltip(member.requested_at) @@ -31,7 +32,7 @@ Joined #{time_ago_with_tooltip(member.created_at)} - if member.expires? · - %span{ class: ('text-warning' if member.expires_soon?) } + %span{ class: "#{"text-warning" if expires_soon} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else @@ -47,7 +48,7 @@ - current_resource = @project || @group .controls.member-controls - if show_controls && member.source == current_resource - - if user != current_user + - if user != current_user && can_admin_member = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level .member-form-control.dropdown.append-right-5 -- cgit v1.2.1 From 3e4dee01740950ae18927aba2deaec63a4ffdc08 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 30 Nov 2016 20:16:32 +0530 Subject: Changelog entry for issue #24861 --- changelogs/unreleased/24861-stringify-group-member-details.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24861-stringify-group-member-details.yml diff --git a/changelogs/unreleased/24861-stringify-group-member-details.yml b/changelogs/unreleased/24861-stringify-group-member-details.yml new file mode 100644 index 00000000000..a636d43747b --- /dev/null +++ b/changelogs/unreleased/24861-stringify-group-member-details.yml @@ -0,0 +1,4 @@ +--- +title: Hide form inputs for group member without editing rights +merge_request: 7816 +author: Kushal Pandya -- cgit v1.2.1 From 1a36b18122445ebf1014a91cf0b95299bf084ea0 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 19 Dec 2016 11:37:29 +0530 Subject: Remove author name --- changelogs/unreleased/24861-stringify-group-member-details.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/24861-stringify-group-member-details.yml b/changelogs/unreleased/24861-stringify-group-member-details.yml index a636d43747b..f56a1060862 100644 --- a/changelogs/unreleased/24861-stringify-group-member-details.yml +++ b/changelogs/unreleased/24861-stringify-group-member-details.yml @@ -1,4 +1,4 @@ --- title: Hide form inputs for group member without editing rights merge_request: 7816 -author: Kushal Pandya +author: -- cgit v1.2.1 From 7cd1b5104a21ab74f5c1a24fac697e90290ae4d9 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 28 Feb 2017 23:05:57 +0530 Subject: Remove unnecessary variable --- app/views/shared/members/_member.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index a797e8f0799..a5aa768b1b2 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -24,7 +24,6 @@ = link_to source.full_name, source, class: "member-group-link" .hidden-xs.cgray - - expires_soon = member.expires_soon? - if member.request? Requested = time_ago_with_tooltip(member.requested_at) @@ -32,7 +31,7 @@ Joined #{time_ago_with_tooltip(member.created_at)} - if member.expires? · - %span{ class: "#{"text-warning" if expires_soon} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } + %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else -- cgit v1.2.1 From 1bc5dab7b4f2650b5afb7c0e4c70e5ac9f66eba0 Mon Sep 17 00:00:00 2001 From: 3kami3 Date: Wed, 1 Mar 2017 23:16:38 +0900 Subject: Add real_ip setting to nginx example. ref) https://docs.gitlab.com/omnibus/settings/nginx.html#configuring-gitlab-trusted_proxies-and-the-nginx-real_ip-module --- lib/support/nginx/gitlab | 6 ++++++ lib/support/nginx/gitlab-ssl | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 2f7c34a3f31..78f28347d1a 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -38,6 +38,12 @@ server { ## See app/controllers/application_controller.rb for headers set + ## Real IP Module Config + ## http://nginx.org/en/docs/http/ngx_http_realip_module.html + real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol + real_ip_recursive off; ## If you enable 'on' + set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24 + ## Individual nginx logs for this GitLab vhost access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 5661394058d..1bccb1c2451 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -82,6 +82,12 @@ server { ## # ssl_dhparam /etc/ssl/certs/dhparam.pem; + ## Real IP Module Config + ## http://nginx.org/en/docs/http/ngx_http_realip_module.html + real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol + real_ip_recursive off; ## If you enable 'on' + set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24 + ## Individual nginx logs for this GitLab vhost access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; -- cgit v1.2.1 From 330d4f38041e2727a741d800b623563acef5e695 Mon Sep 17 00:00:00 2001 From: Nur Rony Date: Fri, 24 Feb 2017 22:18:08 +0600 Subject: dismissable error close is not visible enough --- app/assets/stylesheets/pages/note_form.scss | 12 ++++++++++++ .../28660-fix-dismissable-error-close-not-visible-enough.yml | 4 ++++ 2 files changed, 16 insertions(+) create mode 100644 changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index f984b469609..47e1c7902f8 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -148,6 +148,18 @@ .error-alert > .alert { margin-top: 5px; margin-bottom: 5px; + + &.alert-dismissable { + .close { + color: $white-light; + opacity: 0.85; + font-weight: normal; + + &:hover { + opacity: 1; + } + } + } } .discussion-body, diff --git a/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml new file mode 100644 index 00000000000..8b592766bf3 --- /dev/null +++ b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml @@ -0,0 +1,4 @@ +--- +title: Fixes dismissable error close is not visible enough +merge_request: 9516 +author: -- cgit v1.2.1 From 79c3ace80b690c9ccc2d6190fcf1f14f735f566c Mon Sep 17 00:00:00 2001 From: 3kami3 Date: Fri, 3 Mar 2017 22:20:29 +0900 Subject: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9623#note_24573655 Fixed issues pointed out. --- lib/support/nginx/gitlab | 3 ++- lib/support/nginx/gitlab-ssl | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 78f28347d1a..f25e66d54c8 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -42,7 +42,8 @@ server { ## http://nginx.org/en/docs/http/ngx_http_realip_module.html real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol real_ip_recursive off; ## If you enable 'on' - set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24 + ## If you have a trusted IP address, uncomment it and set it + # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24 ## Individual nginx logs for this GitLab vhost access_log /var/log/nginx/gitlab_access.log; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 1bccb1c2451..67dac676e49 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -86,7 +86,8 @@ server { ## http://nginx.org/en/docs/http/ngx_http_realip_module.html real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol real_ip_recursive off; ## If you enable 'on' - set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24 + ## If you have a trusted IP address, uncomment it and set it + # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24 ## Individual nginx logs for this GitLab vhost access_log /var/log/nginx/gitlab_access.log; -- cgit v1.2.1 From 535c0575b6809fe47938f274c636442f57483789 Mon Sep 17 00:00:00 2001 From: NeroBurner Date: Fri, 3 Mar 2017 14:24:48 +0000 Subject: Update container_registry.md: fix private-docker link --- doc/user/project/container_registry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 91b35c73b34..b6221620e58 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -249,4 +249,4 @@ Once the right permissions were set, the error will go away. [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 [docker-docs]: https://docs.docker.com/engine/userguide/intro/ -[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-docker-registry +[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry -- cgit v1.2.1 From 15b749d72d798652425dc9fc231225fed2af651e Mon Sep 17 00:00:00 2001 From: Miguel Date: Sun, 5 Mar 2017 15:34:03 +0000 Subject: Fixed typo with project service documentation link --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index 46a1ed0e148..57d85d770e7 100644 --- a/doc/README.md +++ b/doc/README.md @@ -19,7 +19,7 @@ - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. - [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. - [Profile Settings](profile/README.md) -- [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat. +- [Project Services](user/project/integrations/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. - [Snippets](user/snippets.md) Snippets allow you to create little bits of code. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. -- cgit v1.2.1 From 00119a5b6deb7c07a39b636418b18d252ff5c931 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Mon, 6 Mar 2017 17:17:11 +0000 Subject: specify where to finish database HA setup --- doc/administration/high_availability/database.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md index e4f94eb7cb6..0a08591c3ce 100644 --- a/doc/administration/high_availability/database.md +++ b/doc/administration/high_availability/database.md @@ -16,7 +16,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL: 1. Set up a `gitlab` username with a password of your choice. The `gitlab` user needs privileges to create the `gitlabhq_production` database. 1. Configure the GitLab application servers with the appropriate details. - This step is covered in [Configuring GitLab for HA](gitlab.md) + This step is covered in [Configuring GitLab for HA](gitlab.md). ## Configure using Omnibus @@ -105,6 +105,8 @@ If you use a cloud-managed service, or provide your own PostgreSQL: 1. Exit the database prompt by typing `\q` and Enter. 1. Exit the `gitlab-psql` user by running `exit` twice. 1. Run `sudo gitlab-ctl reconfigure` a final time. +1. Configure the GitLab application servers with the appropriate details. + This step is covered in [Configuring GitLab for HA](gitlab.md). --- -- cgit v1.2.1 From f482f8aaebe96fe437f63d021baaca3f30c80be7 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Mon, 6 Mar 2017 17:35:08 +0000 Subject: spell out what VIP is --- doc/administration/high_availability/load_balancer.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index dad8e956c0e..3245988fc14 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -19,8 +19,8 @@ you need to use with GitLab. ## GitLab Pages Ports If you're using GitLab Pages you will need some additional port configurations. -GitLab Pages requires a separate VIP. Configure DNS to point the -`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new VIP. See the +GitLab Pages requires a separate virtual IP address. Configure DNS to point the +`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the [GitLab Pages documentation][gitlab-pages] for more information. | LB Port | Backend Port | Protocol | @@ -32,7 +32,7 @@ GitLab Pages requires a separate VIP. Configure DNS to point the Some organizations have policies against opening SSH port 22. In this case, it may be helpful to configure an alternate SSH hostname that allows users -to use SSH on port 443. An alternate SSH hostname will require a new VIP +to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address compared to the other GitLab HTTP configuration above. Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com. -- cgit v1.2.1 From 85e0bbc4f87e5ae3176a70f82625bf4cb9408ca5 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 7 Mar 2017 02:59:12 -0600 Subject: move u2f library to webpack --- app/views/devise/sessions/two_factor.html.haml | 2 +- app/views/profiles/two_factor_auths/show.html.haml | 3 +-- config/application.rb | 1 - config/webpack.config.js | 1 + 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 951f03083bf..a039756c7e2 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -1,6 +1,6 @@ - if inject_u2f_api? - content_for :page_specific_javascripts do - = page_specific_javascript_tag('u2f.js') + = page_specific_javascript_bundle_tag('u2f') %div = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication' diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 558a1d56151..7ade5f00d47 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -4,7 +4,7 @@ - if inject_u2f_api? - content_for :page_specific_javascripts do - = page_specific_javascript_tag('u2f.js') + = page_specific_javascript_bundle_tag('u2f') .row.prepend-top-default .col-lg-3 @@ -96,4 +96,3 @@ :javascript var button = "Configure it later"; $(".flash-alert").append(button); - diff --git a/config/application.rb b/config/application.rb index cdb93e50e66..1cc092c4da1 100644 --- a/config/application.rb +++ b/config/application.rb @@ -100,7 +100,6 @@ module Gitlab config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" config.assets.precompile << "lib/ace.js" - config.assets.precompile << "u2f.js" config.assets.precompile << "vendor/assets/fonts/*" # Version of your assets, change this if you want to expire all your assets diff --git a/config/webpack.config.js b/config/webpack.config.js index 7298e7109c6..ff5f1412261 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -40,6 +40,7 @@ var config = { protected_branches: './protected_branches/protected_branches_bundle.js', snippet: './snippet/snippet_bundle.js', terminal: './terminal/terminal_bundle.js', + u2f: ['vendor/u2f'], users: './users/users_bundle.js', vue_pipelines: './vue_pipelines_index/index.js', }, -- cgit v1.2.1 From b5b448934563b0b3237b6b2e6e168c012b012097 Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Mon, 2 Jan 2017 20:51:57 +0000 Subject: Don't show links to tag a commit for non permitted users * Show tag link for users that can push code * Don't show tag link for guests and non-authenticated users --- app/views/projects/commit/_commit_box.html.haml | 5 ++-- .../26188-tag-creation-404-for-guests.yml | 4 ++++ .../projects/commit/_commit_box.html.haml_spec.rb | 28 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/26188-tag-creation-404-for-guests.yml diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 4d0b7a5ca85..d001e01609a 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -34,8 +34,9 @@ = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) %li.clearfix = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) - %li.clearfix - = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) + - if can_collaborate_with_project? + %li.clearfix + = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) %li.divider %li.dropdown-header Download diff --git a/changelogs/unreleased/26188-tag-creation-404-for-guests.yml b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml new file mode 100644 index 00000000000..fb00d46ea1f --- /dev/null +++ b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml @@ -0,0 +1,4 @@ +--- +title: Don't show links to tag a commit for users that are not permitted +merge_request: 8407 +author: diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb index e741e3cf9b6..f2919f20e85 100644 --- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb +++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb @@ -3,11 +3,13 @@ require 'spec_helper' describe 'projects/commit/_commit_box.html.haml' do include Devise::Test::ControllerHelpers + let(:user) { create(:user) } let(:project) { create(:project) } before do assign(:project, project) assign(:commit, project.commit) + allow(view).to receive(:can_collaborate_with_project?).and_return(false) end it 'shows the commit SHA' do @@ -25,4 +27,30 @@ describe 'projects/commit/_commit_box.html.haml' do expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed") end + + context 'viewing a commit' do + context 'as a developer' do + before do + expect(view).to receive(:can_collaborate_with_project?).and_return(true) + end + + it 'has a link to create a new tag' do + render + + expect(rendered).to have_link('Tag') + end + end + + context 'as a non-developer' do + before do + expect(view).to receive(:can_collaborate_with_project?).and_return(false) + end + + it 'does not have a link to create a new tag' do + render + + expect(rendered).not_to have_link('Tag') + end + end + end end -- cgit v1.2.1 From c6cd31edecdd0082b6cad865e38c6febd8cc75dd Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 7 Mar 2017 12:18:28 -0600 Subject: Change activity view wording --- app/helpers/preferences_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 710218082f2..243ef39ef61 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -36,7 +36,7 @@ module PreferencesHelper def project_view_choices [ ['Files and Readme (default)', :files], - ['Activity view', :activity] + ['Activity', :activity] ] end -- cgit v1.2.1 From ec4b03a4c66a39bed2457528509ceb9806a16084 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 7 Mar 2017 01:10:52 -0300 Subject: Fix 'Object not found - no match for id (sha)' when importing GitHub PRs --- lib/gitlab/github_import/branch_formatter.rb | 2 +- spec/lib/gitlab/github_import/branch_formatter_spec.rb | 12 ++++++------ spec/lib/gitlab/github_import/importer_spec.rb | 2 +- spec/lib/gitlab/github_import/pull_request_formatter_spec.rb | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index 0a8d05b5fe1..5d29e698b27 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -18,7 +18,7 @@ module Gitlab end def commit_exists? - project.repository.commit(sha).present? + project.repository.branch_names_contains(sha).include?(ref) end def short_id diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index 36e7d739f7e..3a31f93efa5 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -6,27 +6,27 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do let(:repo) { double } let(:raw) do { - ref: 'feature', + ref: 'branch-merged', repo: repo, sha: commit.id } end describe '#exists?' do - it 'returns true when both branch, and commit exists' do + it 'returns true when branch exists and commit is part of the branch' do branch = described_class.new(project, double(raw)) expect(branch.exists?).to eq true end - it 'returns false when branch does not exist' do - branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + it 'returns false when branch exists and commit is not part of the branch' do + branch = described_class.new(project, double(raw.merge(ref: 'feature'))) expect(branch.exists?).to eq false end - it 'returns false when commit does not exist' do - branch = described_class.new(project, double(raw.merge(sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) + it 'returns false when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) expect(branch.exists?).to eq false end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 33d83d6d2f1..3f080de99dd 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -130,7 +130,7 @@ describe Gitlab::GithubImport::Importer, lib: true do let!(:user) { create(:user, email: octocat.email) } let(:repository) { double(id: 1, fork: false) } let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } + let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) } let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } let(:pull_request) do diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index e46be18aa99..eeef23a61c6 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } - let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) } + let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) } let(:target_repo) { repository } let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } @@ -49,7 +49,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do title: 'New feature', description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, - source_branch: 'feature', + source_branch: 'branch-merged', source_branch_sha: source_sha, target_project: project, target_branch: 'master', @@ -75,7 +75,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do title: 'New feature', description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, - source_branch: 'feature', + source_branch: 'branch-merged', source_branch_sha: source_sha, target_project: project, target_branch: 'master', @@ -102,7 +102,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do title: 'New feature', description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, - source_branch: 'feature', + source_branch: 'branch-merged', source_branch_sha: source_sha, target_project: project, target_branch: 'master', @@ -194,7 +194,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:raw_data) { double(base_data) } it 'returns branch ref' do - expect(pull_request.source_branch_name).to eq 'feature' + expect(pull_request.source_branch_name).to eq 'branch-merged' end end @@ -208,7 +208,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do - context 'when source branch exists' do + context 'when target branch exists' do let(:raw_data) { double(base_data) } it 'returns branch ref' do -- cgit v1.2.1 From 5195999ea61cc8168363d784dc8e0e7d4c333804 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 7 Mar 2017 01:12:15 -0300 Subject: Add CHANGELOG --- changelogs/unreleased/fix-29093.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-29093.yml diff --git a/changelogs/unreleased/fix-29093.yml b/changelogs/unreleased/fix-29093.yml new file mode 100644 index 00000000000..791129afe93 --- /dev/null +++ b/changelogs/unreleased/fix-29093.yml @@ -0,0 +1,4 @@ +--- +title: Fix 'Object not found - no match for id (sha)' when importing GitHub Pull Requests +merge_request: +author: -- cgit v1.2.1 From 2906278f9341eb6901ad6bb0978eace35480eae2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 7 Mar 2017 21:55:30 -0600 Subject: Don't show double nav bar on project page when activity is the default project view --- app/views/projects/_activity.html.haml | 1 - app/views/projects/activity.html.haml | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index fb990dd9592..aa0cb3e1a50 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,5 +1,4 @@ - @no_container = true -= render "projects/head" %div{ class: container_class } .nav-block.activity-filter-block diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 3c0f01cbf6f..27c8e3c7fca 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1,4 +1,5 @@ - page_title "Activity" += render "projects/head" = render 'projects/last_push' -- cgit v1.2.1 From 86a3dee278e685d935e19bde64c3543e5e1437e5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 7 Mar 2017 20:12:23 -0600 Subject: Prevent visual token dropdown from opening the wrong filter dropdown --- .../javascripts/filtered_search/dropdown_utils.js | 54 ++++++++++++++-------- .../filtered_search_dropdown_manager.js | 2 +- .../filtered_search/filtered_search_manager.js | 9 ++-- .../issues/filtered_search/visual_tokens_spec.rb | 30 ++++++++++++ spec/support/filtered_search_helpers.rb | 11 +++-- 5 files changed, 81 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index b52081df646..a5a6b56a0d3 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -80,30 +80,48 @@ } // Determines the full search query (visual tokens + input) - static getSearchQuery() { - const tokensContainer = document.querySelector('.tokens-container'); + static getSearchQuery(untilInput = false) { + const tokens = [].slice.call(document.querySelectorAll('.tokens-container li')); const values = []; - [].forEach.call(tokensContainer.querySelectorAll('.js-visual-token'), (token) => { - const name = token.querySelector('.name'); - const value = token.querySelector('.value'); - const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; - let valueText = ''; + if (untilInput) { + const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token')); + // Add one to include input-token to the tokens array + tokens.splice(inputIndex + 1); + } - if (value && value.innerText) { - valueText = value.innerText; - } - - if (token.className.indexOf('filtered-search-token') !== -1) { - values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`); - } else { - values.push(name.innerText); + tokens.forEach((token) => { + if (token.classList.contains('js-visual-token')) { + const name = token.querySelector('.name'); + const value = token.querySelector('.value'); + const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; + let valueText = ''; + + if (value && value.innerText) { + valueText = value.innerText; + } + + if (token.className.indexOf('filtered-search-token') !== -1) { + values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`); + } else { + values.push(name.innerText); + } + } else if (token.classList.contains('input-token')) { + const { isLastVisualTokenValid } = + gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + const input = document.querySelector('.filtered-search'); + const inputValue = input && input.value; + + if (isLastVisualTokenValid) { + values.push(inputValue); + } else { + const previous = values.pop(); + values.push(`${previous}${inputValue}`); + } } }); - const input = document.querySelector('.filtered-search'); - values.push(input && input.value); - return values.join(' '); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 608c65c78a4..e1a97070439 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -139,7 +139,7 @@ } setDropdown() { - const query = gl.DropdownUtils.getSearchQuery(); + const query = gl.DropdownUtils.getSearchQuery(true); const { lastToken, searchToken } = this.tokenizer.processTokens(query); if (this.currentDropdown) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 58a984048de..638fe744668 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -363,10 +363,13 @@ tokenChange() { const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; - const currentDropdownRef = dropdown.reference; - this.setDropdownWrapper(); - currentDropdownRef.dispatchInputEvent(); + if (dropdown) { + const currentDropdownRef = dropdown.reference; + + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); + } } } diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index a78dbaf53ed..96e87c82d2c 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -242,6 +242,23 @@ describe 'Visual tokens', js: true, feature: true do end end + describe 'editing a search term while editing another filter token' do + before do + input_filtered_search('author assignee:', submit: false) + first('.tokens-container .filtered-search-term').double_click + end + + it 'opens hint dropdown' do + expect(page).to have_css('#js-dropdown-hint', visible: true) + end + + it 'opens author dropdown' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click + + expect(page).to have_css('#js-dropdown-author', visible: true) + end + end + describe 'add new token after editing existing token' do before do input_filtered_search('author:@root assignee:none', submit: false) @@ -319,4 +336,17 @@ describe 'Visual tokens', js: true, feature: true do expect(token.find('.name').text).to eq('Author') end end + + describe 'search using incomplete visual tokens' do + before do + input_filtered_search('author:@root assignee:none', extra_space: false) + end + + it 'tokenizes the search term to complete visual token' do + expect_tokens([ + { name: 'author', value: '@root' }, + { name: 'assignee', value: 'none' } + ]) + end + end end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index f21b85ec10b..6b009b132b6 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -4,9 +4,14 @@ module FilteredSearchHelpers end # Enables input to be set (similar to copy and paste) - def input_filtered_search(search_term, submit: true) - # Add an extra space to engage visual tokens - filtered_search.set("#{search_term} ") + def input_filtered_search(search_term, submit: true, extra_space: true) + search = search_term + if extra_space + # Add an extra space to engage visual tokens + search = "#{search_term} " + end + + filtered_search.set(search) if submit filtered_search.send_keys(:enter) -- cgit v1.2.1 From 7319460c18a6a6dedaa4aa5c564bbe0afc1ee51c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 8 Mar 2017 09:10:20 +0100 Subject: Verbosify blocked pipeline status description --- app/helpers/ci_status_helper.rb | 2 ++ lib/gitlab/ci/status/pipeline/blocked.rb | 23 +++++++++++++++++++++++ lib/gitlab/ci/status/pipeline/factory.rb | 3 ++- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 lib/gitlab/ci/status/pipeline/blocked.rb diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 2c2c408b035..a7cdca9ba2e 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -15,6 +15,8 @@ module CiStatusHelper 'passed' when 'success_with_warnings' 'passed with warnings' + when 'manual' + 'waiting for manual action' else status end diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb new file mode 100644 index 00000000000..a250c3fcb41 --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/blocked.rb @@ -0,0 +1,23 @@ +module Gitlab + module Ci + module Status + module Pipeline + class Blocked < SimpleDelegator + include Status::Extended + + def text + 'blocked' + end + + def label + 'waiting for manual action' + end + + def self.matches?(pipeline, user) + pipeline.blocked? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 13c8343b12a..17f9a75f436 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -4,7 +4,8 @@ module Gitlab module Pipeline class Factory < Status::Factory def self.extended_statuses - [Status::SuccessWarning] + [[Status::SuccessWarning, + Status::Pipeline::Blocked]] end def self.common_helpers -- cgit v1.2.1 From ccaf66b8ccbd9dc7f5d9dcd297cdcdb35fa4d8ea Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 8 Mar 2017 00:39:56 -0800 Subject: Speed up MergeRequest::RefreshService spec by combining examples --- .../merge_requests/refresh_service_spec.rb | 104 +++++++++++---------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index ff367f54d2a..92729f68e5f 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -58,16 +58,16 @@ describe MergeRequests::RefreshService, services: true do it 'executes hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). with(@merge_request, 'update', @oldrev) - end - it { expect(@merge_request.notes).not_to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey } - it { expect(@merge_request.diff_head_sha).to eq(@newrev) } - it { expect(@fork_merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@build_failed_todo).to be_done } - it { expect(@fork_build_failed_todo).to be_done } + expect(@merge_request.notes).not_to be_empty + expect(@merge_request).to be_open + expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey + expect(@merge_request.diff_head_sha).to eq(@newrev) + expect(@fork_merge_request).to be_open + expect(@fork_merge_request.notes).to be_empty + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end context 'push to origin repo target branch' do @@ -76,12 +76,14 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes.last.note).to include('merged') } - it { expect(@merge_request).to be_merged } - it { expect(@fork_merge_request).to be_merged } - it { expect(@fork_merge_request.notes.last.note).to include('merged') } - it { expect(@build_failed_todo).to be_done } - it { expect(@fork_build_failed_todo).to be_done } + it 'updates the merge state' do + expect(@merge_request.notes.last.note).to include('merged') + expect(@merge_request).to be_merged + expect(@fork_merge_request).to be_merged + expect(@fork_merge_request.notes.last.note).to include('merged') + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end context 'manual merge of source branch' do @@ -95,13 +97,15 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes.last.note).to include('merged') } - it { expect(@merge_request).to be_merged } - it { expect(@merge_request.diffs.size).to be > 0 } - it { expect(@fork_merge_request).to be_merged } - it { expect(@fork_merge_request.notes.last.note).to include('merged') } - it { expect(@build_failed_todo).to be_done } - it { expect(@fork_build_failed_todo).to be_done } + it 'updates the merge state' do + expect(@merge_request.notes.last.note).to include('merged') + expect(@merge_request).to be_merged + expect(@merge_request.diffs.size).to be > 0 + expect(@fork_merge_request).to be_merged + expect(@fork_merge_request.notes.last.note).to include('merged') + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end context 'push to fork repo source branch' do @@ -117,14 +121,14 @@ describe MergeRequests::RefreshService, services: true do it 'executes hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). with(@fork_merge_request, 'update', @oldrev) - end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') } - it { expect(@fork_merge_request).to be_open } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + expect(@merge_request.notes).to be_empty + expect(@merge_request).to be_open + expect(@fork_merge_request.notes.last.note).to include('added 28 commits') + expect(@fork_merge_request).to be_open + expect(@build_failed_todo).to be_pending + expect(@fork_build_failed_todo).to be_pending + end end context 'closed fork merge request' do @@ -139,12 +143,14 @@ describe MergeRequests::RefreshService, services: true do expect(refresh_service).not_to have_received(:execute_hooks) end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@fork_merge_request).to be_closed } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + it 'updates merge request to closed state' do + expect(@merge_request.notes).to be_empty + expect(@merge_request).to be_open + expect(@fork_merge_request.notes).to be_empty + expect(@fork_merge_request).to be_closed + expect(@build_failed_todo).to be_pending + expect(@fork_build_failed_todo).to be_pending + end end end @@ -155,12 +161,14 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@fork_merge_request).to be_open } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + it 'updates the merge request state' do + expect(@merge_request.notes).to be_empty + expect(@merge_request).to be_open + expect(@fork_merge_request.notes).to be_empty + expect(@fork_merge_request).to be_open + expect(@build_failed_todo).to be_pending + expect(@fork_build_failed_todo).to be_pending + end end describe 'merge request diff' do @@ -179,12 +187,14 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes.last.note).to include('merged') } - it { expect(@merge_request).to be_merged } - it { expect(@fork_merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@build_failed_todo).to be_done } - it { expect(@fork_build_failed_todo).to be_done } + it 'updates the merge request state' do + expect(@merge_request.notes.last.note).to include('merged') + expect(@merge_request).to be_merged + expect(@fork_merge_request).to be_open + expect(@fork_merge_request.notes).to be_empty + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end context 'push new branch that exists in a merge request' do -- cgit v1.2.1 From 850f19c02c53648b16a531a81586c05edcfa7530 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 09:24:01 +0000 Subject: Added filtered search bar to issue boards Closes #28312 --- app/assets/javascripts/boards/boards_bundle.js | 3 +++ .../javascripts/boards/filtered_search_boards.js | 5 +++++ app/assets/javascripts/boards/stores/boards_store.js | 4 ++-- app/assets/stylesheets/framework/filters.scss | 5 +++++ app/views/projects/boards/_show.html.haml | 3 ++- app/views/shared/issuable/_filter.html.haml | 18 ++---------------- app/views/shared/issuable/_search_bar.html.haml | 16 ++++++++++++++-- changelogs/unreleased/issue-boards-new-search-bar.yml | 4 ++++ 8 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 app/assets/javascripts/boards/filtered_search_boards.js create mode 100644 changelogs/unreleased/issue-boards-new-search-bar.yml diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 55d13be6e5f..951cb854ce8 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -4,6 +4,7 @@ window.Vue = require('vue'); window.Vue.use(require('vue-resource')); +import FilteredSearchBoards from './filtered_search_boards'; require('./models/issue'); require('./models/label'); require('./models/list'); @@ -26,6 +27,8 @@ $(() => { const Store = gl.issueBoards.BoardsStore; const ModalStore = gl.issueBoards.ModalStore; + new FilteredSearchBoards(); + window.gl = window.gl || {}; if (gl.IssueBoardsApp) { diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js new file mode 100644 index 00000000000..6a00d84faf1 --- /dev/null +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -0,0 +1,5 @@ +export default class FilteredSearchBoards extends gl.FilteredSearchManager { + constructor() { + super('boards'); + } +} diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 3866c6bbfc6..c902a1d8bfc 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -19,8 +19,8 @@ create () { this.state.lists = []; this.state.filters = { - author_id: gl.utils.getParameterValues('author_id')[0], - assignee_id: gl.utils.getParameterValues('assignee_id')[0], + author_username: gl.utils.getParameterValues('author_username')[0], + assignee_username: gl.utils.getParameterValues('assignee_username')[0], milestone_title: gl.utils.getParameterValues('milestone_title')[0], label_name: gl.utils.getParameterValues('label_name[]'), search: '' diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 8f2150066c7..bf0e8e2b891 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -219,6 +219,11 @@ } } +.filter-dropdown-container { + display: -webkit-flex; + display: flex; +} + .dropdown-menu .filter-dropdown-item { padding: 0; } diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 3ae78387938..a3593c9f5db 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -4,6 +4,7 @@ - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('filtered_search') = page_specific_javascript_bundle_tag('boards') = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test? @@ -12,7 +13,7 @@ = render "projects/issues/head" -= render 'shared/issuable/filter', type: :boards += render 'shared/issuable/search_bar', type: :boards #board-app.boards-app{ "v-cloak" => true, data: board_data } .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index f17ae9f28eb..f0bad69a989 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,4 +1,4 @@ -- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder +- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder - boards_page = controller.controller_name == 'boards' .issues-filters @@ -34,21 +34,7 @@ %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters .pull-right - - if boards_page - #js-boards-search.issue-boards-search - %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - - if can?(current_user, :admin_list, @project) - #js-add-issues-btn.pull-right.prepend-left-10 - .dropdown.pull-right - %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } - Add list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - - if can?(current_user, :admin_label, @project) - = render partial: "shared/issuable/label_page_create" - = dropdown_loading - - else - = render 'shared/sort_dropdown' + = render 'shared/sort_dropdown' - if @bulk_edit .issues_bulk_update.hide diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 32128f3b3dc..515c3d4258e 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -85,8 +85,20 @@ %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} - .pull-right.filter-dropdown-container - = render 'shared/sort_dropdown' + .filter-dropdown-container + - if type == :boards + - if can?(current_user, :admin_list, @project) + .dropdown.prepend-left-10 + %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } + Add list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } + - if can?(current_user, :admin_label, @project) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading + #js-add-issues-btn.prepend-left-10 + - else + = render 'shared/sort_dropdown' - if @bulk_edit .issues_bulk_update.hide diff --git a/changelogs/unreleased/issue-boards-new-search-bar.yml b/changelogs/unreleased/issue-boards-new-search-bar.yml new file mode 100644 index 00000000000..b02be70c470 --- /dev/null +++ b/changelogs/unreleased/issue-boards-new-search-bar.yml @@ -0,0 +1,4 @@ +--- +title: Added new filtered search bar to issue boards +merge_request: +author: -- cgit v1.2.1 From f89782b3f25984794f4f9752979c05d5ed6f0a96 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 11:05:37 +0000 Subject: Changed store Async updates the boards when searching --- app/assets/javascripts/boards/boards_bundle.js | 4 ++-- app/assets/javascripts/boards/components/board.js | 8 ++++---- .../javascripts/boards/filtered_search_boards.js | 9 ++++++++- app/assets/javascripts/boards/models/list.js | 20 ++++++++++++++++---- app/assets/javascripts/boards/stores/boards_store.js | 11 ++++------- .../filtered_search/filtered_search_manager.js | 8 ++++++-- 6 files changed, 40 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 951cb854ce8..6b294290f77 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -27,8 +27,6 @@ $(() => { const Store = gl.issueBoards.BoardsStore; const ModalStore = gl.issueBoards.ModalStore; - new FilteredSearchBoards(); - window.gl = window.gl || {}; if (gl.IssueBoardsApp) { @@ -62,6 +60,8 @@ $(() => { }, created () { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); + + new FilteredSearchBoards(Store.filter); }, mounted () { Store.disabled = this.disabled; diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 18324de18b3..30d3be453be 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -28,16 +28,16 @@ require('./board_list'); data () { return { detailIssue: Store.detail, - filters: Store.state.filters, + filter: Store.filter, }; }, watch: { - filters: { - handler () { + filter: { + handler() { this.list.page = 1; this.list.getIssues(true); }, - deep: true + deep: true, }, detailIssue: { handler () { diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 6a00d84faf1..0b11237b03d 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -1,5 +1,12 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { - constructor() { + constructor(store) { super('boards'); + + this.store = store; + this.destroyOnSubmit = false + } + + updateObject(path) { + this.store.path = path.substr(1); } } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index f237567208c..ae117aa3900 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -10,7 +10,7 @@ class List { this.title = obj.title; this.type = obj.list_type; this.preset = ['done', 'blank'].indexOf(this.type) > -1; - this.filters = gl.issueBoards.BoardsStore.state.filters; + this.filterPath = gl.issueBoards.BoardsStore.filter.path; this.page = 1; this.loading = true; this.loadingMore = false; @@ -65,12 +65,24 @@ class List { } getIssues (emptyIssues = true) { - const filters = this.filters; const data = { page: this.page }; + gl.issueBoards.BoardsStore.filter.path.split('&').forEach((filterParam) => { + const paramSplit = filterParam.split('='); + const paramKeyNormalized = paramSplit[0].replace('[]', ''); + const isArray = paramSplit[0].indexOf('[]'); + + if (isArray >= 0) { + if (!data[paramKeyNormalized]) { + data[paramKeyNormalized] = []; + } - Object.keys(filters).forEach((key) => { data[key] = filters[key]; }); + data[paramKeyNormalized].push(paramSplit[1]); + } else { + data[paramKeyNormalized] = paramSplit[1]; + } + }); - if (this.label) { + if (this.label && data.label_name) { data.label_name = data.label_name.filter(label => label !== this.label.title); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index c902a1d8bfc..d7e3973b327 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -8,6 +8,9 @@ gl.issueBoards.BoardsStore = { disabled: false, + filter: { + path: '', + }, state: {}, detail: { issue: {} @@ -18,13 +21,7 @@ }, create () { this.state.lists = []; - this.state.filters = { - author_username: gl.utils.getParameterValues('author_username')[0], - assignee_username: gl.utils.getParameterValues('assignee_username')[0], - milestone_title: gl.utils.getParameterValues('milestone_title')[0], - label_name: gl.utils.getParameterValues('label_name[]'), - search: '' - }; + this.filter.path = gl.utils.getUrlParamsArray().join('&'); }, addList (listObj) { const list = new List(listObj); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 58a984048de..56ff091197c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -106,7 +106,7 @@ if (!activeElements.length) { // Prevent droplab from opening dropdown - this.dropdownManager.destroyDroplab(); + //this.dropdownManager.destroyDroplab(); this.search(); } @@ -345,7 +345,11 @@ const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; - gl.utils.visitUrl(parameterizedUrl); + if (this.updateObject) { + this.updateObject(parameterizedUrl); + } else { + gl.utils.visitUrl(parameterizedUrl); + } } getUsernameParams() { -- cgit v1.2.1 From ddf71fcef5d0d7b9952d77d712007008efbb5d3f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 11:07:26 +0000 Subject: Updates the URL --- app/assets/javascripts/boards/filtered_search_boards.js | 1 + app/assets/javascripts/boards/stores/boards_store.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 0b11237b03d..ff8da88e6e8 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -8,5 +8,6 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { updateObject(path) { this.store.path = path.substr(1); + gl.issueBoards.BoardsStore.updateFiltersUrl(); } } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index d7e3973b327..28ecb322df7 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -120,7 +120,7 @@ })[0]; }, updateFiltersUrl () { - history.pushState(null, null, `?${$.param(this.state.filters)}`); + history.pushState(null, null, `?${this.filter.path}`); } }; })(); -- cgit v1.2.1 From 107c39a66e621e35f808b3a257789d78bf153894 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 14:28:50 +0000 Subject: Stop droplab from destroying itself is handled async --- app/assets/javascripts/boards/filtered_search_boards.js | 3 ++- .../javascripts/filtered_search/filtered_search_manager.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index ff8da88e6e8..43c6d9d7237 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -3,11 +3,12 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { super('boards'); this.store = store; - this.destroyOnSubmit = false + this.isHandledAsync = true; } updateObject(path) { this.store.path = path.substr(1); + gl.issueBoards.BoardsStore.updateFiltersUrl(); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 56ff091197c..652d6c9be0e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -105,8 +105,14 @@ e.preventDefault(); if (!activeElements.length) { - // Prevent droplab from opening dropdown - //this.dropdownManager.destroyDroplab(); + if (this.isHandledAsync) { + e.stopImmediatePropagation(); + this.filteredSearchInput.blur(); + this.dropdownManager.resetDropdowns(); + } else { + // Prevent droplab from opening dropdown + this.dropdownManager.destroyDroplab(); + } this.search(); } -- cgit v1.2.1 From ab7bfff08b2ba8d15f1ab5f8fa4449dc53f51bab Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 14:43:17 +0000 Subject: Make changing the URL optional - future proof ourselves for the modal window --- app/assets/javascripts/boards/boards_bundle.js | 7 ++++--- app/assets/javascripts/boards/filtered_search_boards.js | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 6b294290f77..1731f218f37 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,10 +1,11 @@ -/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, no-new */ /* global Vue */ /* global BoardService */ +import FilteredSearchBoards from './filtered_search_boards'; + window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -import FilteredSearchBoards from './filtered_search_boards'; require('./models/issue'); require('./models/label'); require('./models/list'); @@ -61,7 +62,7 @@ $(() => { created () { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); - new FilteredSearchBoards(Store.filter); + new FilteredSearchBoards(Store.filter, true); }, mounted () { Store.disabled = this.disabled; diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 43c6d9d7237..d00cb123909 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -1,14 +1,17 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { - constructor(store) { + constructor(store, updateUrl = false) { super('boards'); this.store = store; + this.updateUrl = updateUrl; this.isHandledAsync = true; } updateObject(path) { this.store.path = path.substr(1); - gl.issueBoards.BoardsStore.updateFiltersUrl(); + if (this.updateUrl) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } } } -- cgit v1.2.1 From 382fea7b5925ac7dc47ccfd79f7537284e68cd6f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 15:54:45 +0000 Subject: Handle clear search async --- app/assets/javascripts/filtered_search/filtered_search_manager.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 652d6c9be0e..3478f1130a5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -107,6 +107,7 @@ if (!activeElements.length) { if (this.isHandledAsync) { e.stopImmediatePropagation(); + this.filteredSearchInput.blur(); this.dropdownManager.resetDropdowns(); } else { @@ -205,6 +206,10 @@ this.handleInputPlaceholder(); this.dropdownManager.resetDropdowns(); + + if (this.isHandledAsync) { + this.search(); + } } handleInputVisualToken() { -- cgit v1.2.1 From 14ad75a176639ca83067ba1b45aab38ba115e5bf Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 8 Mar 2017 10:58:41 +0100 Subject: Fix `passed with warnings` stage status on MySQL --- app/models/ci/pipeline.rb | 2 +- app/models/ci/stage.rb | 6 +++--- spec/models/ci/pipeline_spec.rb | 18 ++++++++++++++++++ spec/models/ci/stage_spec.rb | 17 +++++++++++++---- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 67206415f7b..8a5a9aa4adb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -144,7 +144,7 @@ module Ci status_sql = statuses.latest.where('stage=sg.stage').status_sql - warnings_sql = statuses.latest.select('COUNT(*) > 0') + warnings_sql = statuses.latest.select('COUNT(*)') .where('stage=sg.stage').failed_but_allowed.to_sql stages_with_statuses = CommitStatus.from(stages_query, :sg) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index ca74c91b062..b60bb9c6de0 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -46,10 +46,10 @@ module Ci end def has_warnings? - if @warnings.nil? - statuses.latest.failed_but_allowed.any? + if @warnings.is_a?(Fixnum) + @warnings > 0 else - @warnings + statuses.latest.failed_but_allowed.any? end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index dd5f7098d06..3f8c24d0429 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -197,6 +197,24 @@ describe Ci::Pipeline, models: true do end end end + + context 'when there is a stage with warnings' do + before do + create(:commit_status, pipeline: pipeline, + stage: 'deploy', + name: 'prod:2', + stage_idx: 2, + status: 'failed', + allow_failure: true) + end + + it 'populates stage with correct number of warnings' do + deploy_stage = pipeline.stages.third + + expect(deploy_stage).not_to receive(:statuses) + expect(deploy_stage).to have_warnings + end + end end describe '#stages_count' do diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index c4a9743a4e2..c38faf32f7d 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -170,22 +170,31 @@ describe Ci::Stage, models: true do context 'when stage has warnings' do context 'when using memoized warnings flag' do context 'when there are warnings' do - let(:stage) { build(:ci_stage, warnings: true) } + let(:stage) { build(:ci_stage, warnings: 2) } - it 'has memoized warnings' do + it 'returns true using memoized value' do expect(stage).not_to receive(:statuses) expect(stage).to have_warnings end end context 'when there are no warnings' do - let(:stage) { build(:ci_stage, warnings: false) } + let(:stage) { build(:ci_stage, warnings: 0) } - it 'has memoized warnings' do + it 'returns false using memoized value' do expect(stage).not_to receive(:statuses) expect(stage).not_to have_warnings end end + + context 'when number of warnings is not a valid value' do + let(:stage) { build(:ci_stage, warnings: true) } + + it 'calculates statuses using database queries' do + expect(stage).to receive(:statuses).and_call_original + expect(stage).not_to have_warnings + end + end end context 'when calculating warnings from statuses' do -- cgit v1.2.1 From 4c2a6f0d9ce9b4c4e0f28c4e8a0232f8ca8a29e5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 8 Mar 2017 11:02:26 +0100 Subject: Add changelog for passed with warnings status fix --- changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml diff --git a/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml new file mode 100644 index 00000000000..6365b1a1910 --- /dev/null +++ b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml @@ -0,0 +1,4 @@ +--- +title: Fix "passed with warnings" stage status on MySQL installations +merge_request: 9802 +author: -- cgit v1.2.1 From c83be391ee03a9fb2c7d6aca552f4f230703fb30 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 8 Mar 2017 11:35:57 +0100 Subject: Adjust specs for core pipeline detailed statuses --- spec/lib/gitlab/ci/status/pipeline/factory_spec.rb | 26 ++++++++++++++++++++-- spec/models/ci/pipeline_spec.rb | 2 +- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index b10a447c27a..dd754b849b2 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -11,7 +11,8 @@ describe Gitlab::Ci::Status::Pipeline::Factory do end context 'when pipeline has a core status' do - HasStatus::AVAILABLE_STATUSES.each do |simple_status| + (HasStatus::AVAILABLE_STATUSES - [HasStatus::BLOCKED_STATUS]) + .each do |simple_status| context "when core status is #{simple_status}" do let(:pipeline) { create(:ci_pipeline, status: simple_status) } @@ -23,7 +24,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do expect(factory.core_status).to be_a expected_status end - it 'does not matche extended statuses' do + it 'does not match extended statuses' do expect(factory.extended_statuses).to be_empty end @@ -39,6 +40,27 @@ describe Gitlab::Ci::Status::Pipeline::Factory do end end end + + context "when core status is manual" do + let(:pipeline) { create(:ci_pipeline, status: :manual) } + + it "matches manual core status" do + expect(factory.core_status) + .to be_a Gitlab::Ci::Status::Manual + end + + it 'matches a correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Pipeline::Blocked] + end + + it 'extends core status with common pipeline methods' do + expect(status).to have_details + expect(status).not_to have_action + expect(status.details_path) + .to include "pipelines/#{pipeline.id}" + end + end end context 'when pipeline has warnings' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index dd5f7098d06..3ea62df62f2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -647,7 +647,7 @@ describe Ci::Pipeline, models: true do let(:pipeline) { create(:ci_pipeline, status: :manual) } it 'returns detailed status for blocked pipeline' do - expect(subject.text).to eq 'manual' + expect(subject.text).to eq 'blocked' end end -- cgit v1.2.1 From 82327ea7c68faf4bb3231c0418f277fde876ec59 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 8 Mar 2017 11:44:36 +0100 Subject: Add specs for an extended blocked pipeline status --- spec/factories/ci/pipelines.rb | 8 +++++ spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb | 42 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index 77404f46c92..b67c96bc00d 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -40,6 +40,14 @@ FactoryGirl.define do trait :invalid do config(rspec: nil) end + + trait :blocked do + status :manual + end + + trait :success do + status :success + end end end end diff --git a/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb b/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb new file mode 100644 index 00000000000..1a2b952d374 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::Blocked do + let(:pipeline) { double('pipeline') } + + subject do + described_class.new(pipeline) + end + + describe '#text' do + it 'overrides status text' do + expect(subject.text).to eq 'blocked' + end + end + + describe '#label' do + it 'overrides status label' do + expect(subject.label).to eq 'waiting for manual action' + end + end + + describe '.matches?' do + let(:user) { double('user') } + subject { described_class.matches?(pipeline, user) } + + context 'when pipeline is blocked' do + let(:pipeline) { create(:ci_pipeline, :blocked) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when pipeline is not blocked' do + let(:pipeline) { create(:ci_pipeline, :success) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end -- cgit v1.2.1 From 6bf109b74478a518b77ae7ec122dcf06f15452f8 Mon Sep 17 00:00:00 2001 From: winniehell Date: Wed, 1 Mar 2017 20:54:04 +0100 Subject: Convert Issue into ES6 class (!9636) --- app/assets/javascripts/dispatcher.js | 2 +- app/assets/javascripts/issue.js | 226 +++++++++++++++--------------- changelogs/unreleased/es6-class-issue.yml | 4 + spec/javascripts/issue_spec.js | 43 +++--- 4 files changed, 135 insertions(+), 140 deletions(-) create mode 100644 changelogs/unreleased/es6-class-issue.yml diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 546bdc9c8d7..017980271b1 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -5,7 +5,6 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make /* global ShortcutsNavigation */ /* global Build */ /* global Issuable */ -/* global Issue */ /* global ShortcutsIssuable */ /* global ZenMode */ /* global Milestone */ @@ -35,6 +34,7 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make /* global ProjectShow */ /* global Labels */ /* global Shortcuts */ +import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import GroupsList from './groups_list'; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 52457f70d90..ef4029a8623 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -5,131 +5,125 @@ require('./flash'); require('vendor/jquery.waitforimages'); require('./task_list'); -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - - this.Issue = (function() { - function Issue() { - this.submitNoteForm = bind(this.submitNoteForm, this); - if ($('a.btn-close').length) { - this.taskList = new gl.TaskList({ - dataType: 'issue', - fieldName: 'description', - selector: '.detail-page-description', - onSuccess: (result) => { - document.querySelector('#task_status').innerText = result.task_status; - document.querySelector('#task_status_short').innerText = result.task_status_short; - } - }); - this.initIssueBtnEventListeners(); - } - this.initMergeRequests(); - this.initRelatedBranches(); - this.initCanCreateBranch(); +class Issue { + constructor() { + if ($('a.btn-close').length) { + this.taskList = new gl.TaskList({ + dataType: 'issue', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: (result) => { + document.querySelector('#task_status').innerText = result.task_status; + document.querySelector('#task_status_short').innerText = result.task_status_short; + } + }); + Issue.initIssueBtnEventListeners(); } + Issue.initMergeRequests(); + Issue.initRelatedBranches(); + Issue.initCanCreateBranch(); + } - Issue.prototype.initIssueBtnEventListeners = function() { - var _this, issueFailMessage; - _this = this; - issueFailMessage = 'Unable to update this issue at this time.'; - return $('a.btn-close, a.btn-reopen').on('click', function(e) { - var $this, isClose, shouldSubmit, url; - e.preventDefault(); - e.stopImmediatePropagation(); - $this = $(this); - isClose = $this.hasClass('btn-close'); - shouldSubmit = $this.hasClass('btn-comment'); - if (shouldSubmit) { - _this.submitNoteForm($this.closest('form')); - } - $this.prop('disabled', true); - url = $this.attr('href'); - return $.ajax({ - type: 'PUT', - url: url, - error: function(jqXHR, textStatus, errorThrown) { - var issueStatus; - issueStatus = isClose ? 'close' : 'open'; - return new Flash(issueFailMessage, 'alert'); - }, - success: function(data, textStatus, jqXHR) { - if ('id' in data) { - $(document).trigger('issuable:change'); - const currentTotal = Number($('.issue_counter').text()); - if (isClose) { - $('a.btn-close').addClass('hidden'); - $('a.btn-reopen').removeClass('hidden'); - $('div.status-box-closed').removeClass('hidden'); - $('div.status-box-open').addClass('hidden'); - $('.issue_counter').text(currentTotal - 1); - } else { - $('a.btn-reopen').addClass('hidden'); - $('a.btn-close').removeClass('hidden'); - $('div.status-box-closed').addClass('hidden'); - $('div.status-box-open').removeClass('hidden'); - $('.issue_counter').text(currentTotal + 1); - } + static initIssueBtnEventListeners() { + var issueFailMessage; + issueFailMessage = 'Unable to update this issue at this time.'; + return $('a.btn-close, a.btn-reopen').on('click', function(e) { + var $this, isClose, shouldSubmit, url; + e.preventDefault(); + e.stopImmediatePropagation(); + $this = $(this); + isClose = $this.hasClass('btn-close'); + shouldSubmit = $this.hasClass('btn-comment'); + if (shouldSubmit) { + Issue.submitNoteForm($this.closest('form')); + } + $this.prop('disabled', true); + url = $this.attr('href'); + return $.ajax({ + type: 'PUT', + url: url, + error: function(jqXHR, textStatus, errorThrown) { + var issueStatus; + issueStatus = isClose ? 'close' : 'open'; + return new Flash(issueFailMessage, 'alert'); + }, + success: function(data, textStatus, jqXHR) { + if ('id' in data) { + $(document).trigger('issuable:change'); + const currentTotal = Number($('.issue_counter').text()); + if (isClose) { + $('a.btn-close').addClass('hidden'); + $('a.btn-reopen').removeClass('hidden'); + $('div.status-box-closed').removeClass('hidden'); + $('div.status-box-open').addClass('hidden'); + $('.issue_counter').text(currentTotal - 1); } else { - new Flash(issueFailMessage, 'alert'); + $('a.btn-reopen').addClass('hidden'); + $('a.btn-close').removeClass('hidden'); + $('div.status-box-closed').addClass('hidden'); + $('div.status-box-open').removeClass('hidden'); + $('.issue_counter').text(currentTotal + 1); } - return $this.prop('disabled', false); + } else { + new Flash(issueFailMessage, 'alert'); } - }); + return $this.prop('disabled', false); + } }); - }; + }); + } - Issue.prototype.submitNoteForm = function(form) { - var noteText; - noteText = form.find("textarea.js-note-text").val(); - if (noteText.trim().length > 0) { - return form.submit(); - } - }; + static submitNoteForm(form) { + var noteText; + noteText = form.find("textarea.js-note-text").val(); + if (noteText.trim().length > 0) { + return form.submit(); + } + } - Issue.prototype.initMergeRequests = function() { - var $container; - $container = $('#merge-requests'); - return $.getJSON($container.data('url')).error(function() { - return new Flash('Failed to load referenced merge requests', 'alert'); - }).success(function(data) { - if ('html' in data) { - return $container.html(data.html); - } - }); - }; + static initMergeRequests() { + var $container; + $container = $('#merge-requests'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load referenced merge requests', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + } - Issue.prototype.initRelatedBranches = function() { - var $container; - $container = $('#related-branches'); - return $.getJSON($container.data('url')).error(function() { - return new Flash('Failed to load related branches', 'alert'); - }).success(function(data) { - if ('html' in data) { - return $container.html(data.html); - } - }); - }; + static initRelatedBranches() { + var $container; + $container = $('#related-branches'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load related branches', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + } - Issue.prototype.initCanCreateBranch = function() { - var $container; - $container = $('#new-branch'); - // If the user doesn't have the required permissions the container isn't - // rendered at all. - if ($container.length === 0) { - return; + static initCanCreateBranch() { + var $container; + $container = $('#new-branch'); + // If the user doesn't have the required permissions the container isn't + // rendered at all. + if ($container.length === 0) { + return; + } + return $.getJSON($container.data('path')).error(function() { + $container.find('.unavailable').show(); + return new Flash('Failed to check if a new branch can be created.', 'alert'); + }).success(function(data) { + if (data.can_create_branch) { + $container.find('.available').show(); + } else { + return $container.find('.unavailable').show(); } - return $.getJSON($container.data('path')).error(function() { - $container.find('.unavailable').show(); - return new Flash('Failed to check if a new branch can be created.', 'alert'); - }).success(function(data) { - if (data.can_create_branch) { - $container.find('.available').show(); - } else { - return $container.find('.unavailable').show(); - } - }); - }; + }); + } +} - return Issue; - })(); -}).call(window); +export default Issue; diff --git a/changelogs/unreleased/es6-class-issue.yml b/changelogs/unreleased/es6-class-issue.yml new file mode 100644 index 00000000000..9d1c3ac7421 --- /dev/null +++ b/changelogs/unreleased/es6-class-issue.yml @@ -0,0 +1,4 @@ +--- +title: Convert Issue into ES6 class +merge_request: 9636 +author: winniehell diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index e7530f61385..8d25500b9fd 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,10 +1,9 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ -/* global Issue */ +import Issue from '~/issue'; require('~/lib/utils/text_utility'); -require('~/issue'); -(function() { +describe('Issue', function() { var INVALID_URL = 'http://goesnowhere.nothing/whereami'; var $boxClosed, $boxOpen, $btnClose, $btnReopen; @@ -59,28 +58,26 @@ require('~/issue'); expect($btnReopen).toHaveText('Reopen issue'); } - describe('Issue', function() { - describe('task lists', function() { - beforeEach(function() { - loadFixtures('issues/issue-with-task-list.html.raw'); - this.issue = new Issue(); - }); - - it('modifies the Markdown field', function() { - spyOn(jQuery, 'ajax').and.stub(); - $('input[type=checkbox]').attr('checked', true).trigger('change'); - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); - }); + describe('task lists', function() { + beforeEach(function() { + loadFixtures('issues/issue-with-task-list.html.raw'); + this.issue = new Issue(); + }); - it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PATCH'); - expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template - expect(req.data.issue.description).not.toBe(null); - }); + it('modifies the Markdown field', function() { + spyOn(jQuery, 'ajax').and.stub(); + $('input[type=checkbox]').attr('checked', true).trigger('change'); + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); - $('.js-task-list-field').trigger('tasklist:changed'); + it('submits an ajax request on tasklist:changed', function() { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PATCH'); + expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template + expect(req.data.issue.description).not.toBe(null); }); + + $('.js-task-list-field').trigger('tasklist:changed'); }); }); @@ -165,4 +162,4 @@ require('~/issue'); expect($('.issue_counter')).toHaveText(1); }); }); -}).call(window); +}); -- cgit v1.2.1 From 12b89eb88c28588e86785e8b99bf98fbfdb1139e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 8 Mar 2017 11:14:28 +0000 Subject: Apply same html and css to the action buttons in environment show view table as the ones in index table --- app/views/projects/deployments/_actions.haml | 5 ++--- app/views/projects/deployments/_deployment.html.haml | 2 +- app/views/projects/environments/show.html.haml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index a680b1ca017..506246f2ee6 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -1,9 +1,9 @@ - if can?(current_user, :create_deployment, deployment) - actions = deployment.manual_actions - if actions.present? - .inline + .btn-group .dropdown - %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } + %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } = custom_icon('icon_play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right @@ -12,4 +12,3 @@ = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = custom_icon('icon_play') %span= action.name.humanize - diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index c468202569f..260c9023daf 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -17,6 +17,6 @@ #{time_ago_with_tooltip(deployment.created_at)} %td.hidden-xs - .pull-right + .pull-right.btn-group = render 'projects/deployments/actions', deployment: deployment = render 'projects/deployments/rollback', deployment: deployment diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 29a98f23b88..f463a429f65 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -16,7 +16,7 @@ - if can?(current_user, :create_deployment, @environment) && @environment.can_stop? = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post - .deployments-container + .environments-container - if @deployments.blank? .blank-state.blank-state-no-icon %h2.blank-state-title -- cgit v1.2.1 From 5ca2005e0448eeeed2cab7390a89284eb0aef2e0 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 8 Mar 2017 12:34:45 +0100 Subject: Fix Rubocop offense in CI/CD stage code --- app/models/ci/stage.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index b60bb9c6de0..e7d6b17d445 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -46,7 +46,7 @@ module Ci end def has_warnings? - if @warnings.is_a?(Fixnum) + if @warnings.is_a?(Integer) @warnings > 0 else statuses.latest.failed_but_allowed.any? -- cgit v1.2.1 From f6ba5ba87437c9b30f787797f00ce48a6aec8b06 Mon Sep 17 00:00:00 2001 From: Alex Karnovsky Date: Wed, 8 Mar 2017 11:46:06 +0000 Subject: Update README.md I replaced "merge requests" by "commits". As far as I notice, merge requests per se don't trigger CI; commits and pushes (which are essentially adding commits) do. This is logical: an MR doesn't create anything new, so there is nothing to test. --- doc/ci/quick_start/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 2a5401ac13a..76e86f3e3c3 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -6,7 +6,7 @@ projects. GitLab offers a [continuous integration][ci] service. If you [add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository, -and configure your GitLab project to use a [Runner], then each merge request or +and configure your GitLab project to use a [Runner], then each commit or push, triggers your CI [pipeline]. The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs @@ -14,8 +14,8 @@ a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to use all three stages; stages with no jobs are simply ignored. If everything runs OK (no non-zero return values), you'll get a nice green -checkmark associated with the pushed commit or merge request. This makes it -easy to see whether a merge request caused any of the tests to fail before +checkmark associated with the commit. This makes it +easy to see whether a commit caused any of the tests to fail before you even look at the code. Most projects use GitLab's CI service to run the test suite so that -- cgit v1.2.1 From 809bba7d02b45938494f8ae471a2b27ce4a40833 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 12:17:01 +0000 Subject: Updated specs --- app/assets/javascripts/boards/boards_bundle.js | 4 +- .../boards/components/issue_card_inner.js | 23 ++-- .../javascripts/boards/filtered_search_boards.js | 14 ++ app/assets/javascripts/boards/models/list.js | 8 +- app/assets/stylesheets/framework/filters.scss | 1 - app/views/shared/issuable/_search_bar.html.haml | 2 +- spec/features/boards/add_issues_modal_spec.rb | 2 +- spec/features/boards/boards_spec.rb | 153 ++++++++------------- spec/features/issuables/default_sort_order_spec.rb | 2 +- 9 files changed, 86 insertions(+), 123 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 1731f218f37..9e9da7dfac4 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -62,7 +62,7 @@ $(() => { created () { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); - new FilteredSearchBoards(Store.filter, true); + gl.boardsFilterManager = new FilteredSearchBoards(Store.filter, true); }, mounted () { Store.disabled = this.disabled; @@ -85,7 +85,7 @@ $(() => { }); gl.IssueBoardsSearch = new Vue({ - el: document.getElementById('js-boards-search'), + el: document.getElementById('js-add-list'), data: { filters: Store.state.filters }, diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 22a8b971ff8..dce573ed6ca 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -31,29 +31,22 @@ return !this.list.label || label.id !== this.list.label.id; }, filterByLabel(label, e) { - let labelToggleText = label.title; - const labelIndex = Store.state.filters.label_name.indexOf(label.title); + const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); + const labelTitle = encodeURIComponent(label.title); + const param = `label_name[]=${labelTitle}`; + const labelIndex = filterPath.indexOf(param); $(e.currentTarget).tooltip('hide'); if (labelIndex === -1) { - Store.state.filters.label_name.push(label.title); - $('.labels-filter').prepend(``); + filterPath.push(param); } else { - Store.state.filters.label_name.splice(labelIndex, 1); - labelToggleText = Store.state.filters.label_name[0]; - $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); + filterPath.splice(labelIndex, 1); } - const selectedLabels = Store.state.filters.label_name; - if (selectedLabels.length === 0) { - labelToggleText = 'Label'; - } else if (selectedLabels.length > 1) { - labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; - } - - $('.labels-filter .dropdown-toggle-text').text(labelToggleText); + gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); Store.updateFiltersUrl(); + gl.boardsFilterManager.updateTokens(); }, labelStyle(label) { return { diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index d00cb123909..3014557c440 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -14,4 +14,18 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { gl.issueBoards.BoardsStore.updateFiltersUrl(); } } + + updateTokens() { + const tokens = document.querySelectorAll('.js-visual-token'); + + // Remove all the tokens as they will be replaced by the search manager + [].forEach.call(tokens, (el) => { + el.parentNode.removeChild(el); + }); + + this.loadSearchParamsFromURL(); + + // Get the placeholder back if search is empty + this.filteredSearchInput.dispatchEvent(new Event('input')); + } } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index ae117aa3900..b246c3c1503 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -10,7 +10,6 @@ class List { this.title = obj.title; this.type = obj.list_type; this.preset = ['done', 'blank'].indexOf(this.type) > -1; - this.filterPath = gl.issueBoards.BoardsStore.filter.path; this.page = 1; this.loading = true; this.loadingMore = false; @@ -67,18 +66,20 @@ class List { getIssues (emptyIssues = true) { const data = { page: this.page }; gl.issueBoards.BoardsStore.filter.path.split('&').forEach((filterParam) => { + if (filterParam === '') return; const paramSplit = filterParam.split('='); const paramKeyNormalized = paramSplit[0].replace('[]', ''); const isArray = paramSplit[0].indexOf('[]'); + const value = decodeURIComponent(paramSplit[1]); if (isArray >= 0) { if (!data[paramKeyNormalized]) { data[paramKeyNormalized] = []; } - data[paramKeyNormalized].push(paramSplit[1]); + data[paramKeyNormalized].push(value); } else { - data[paramKeyNormalized] = paramSplit[1]; + data[paramKeyNormalized] = value; } }); @@ -101,6 +102,7 @@ class List { } this.createIssues(data.issues); + console.log(this.issues.length); }); } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index bf0e8e2b891..dd2daa4b872 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -156,7 +156,6 @@ width: 100%; border: 1px solid $border-color; background-color: $white-light; - max-width: 87%; @media (max-width: $screen-xs-min) { -webkit-flex: 1 1 100%; diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 515c3d4258e..d73556114d8 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -88,7 +88,7 @@ .filter-dropdown-container - if type == :boards - if can?(current_user, :admin_list, @project) - .dropdown.prepend-left-10 + .dropdown.prepend-left-10#js-add-list %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } Add list .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index a3e24bb5ffa..f7f2d883d2f 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -51,7 +51,7 @@ describe 'Issue Boards add issue modal', :feature, :js do end it 'does not show tooltip on add issues button' do - button = page.find('.issue-boards-search button', text: 'Add issues') + button = page.find('.filter-dropdown-container button', text: 'Add issues') expect(button[:title]).not_to eq("Please add a list to your board first") end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index ecc356f2505..e11ba10c80c 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -359,17 +359,9 @@ describe 'Issue Boards', feature: true, js: true do context 'filtering' do it 'filters by author' do - page.within '.issues-filters' do - click_button('Author') - wait_for_ajax - - page.within '.dropdown-menu-author' do - click_link(user2.name) - end - wait_for_vue_resource - - expect(find('.js-author-search')).to have_content(user2.name) - end + set_filter("author", user2.username) + click_filter_link(user2.username) + submit_filter wait_for_vue_resource wait_for_board_cards(1, 1) @@ -377,17 +369,9 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by assignee' do - page.within '.issues-filters' do - click_button('Assignee') - wait_for_ajax - - page.within '.dropdown-menu-assignee' do - click_link(user.name) - end - wait_for_vue_resource - - expect(find('.js-assignee-search')).to have_content(user.name) - end + set_filter("assignee", user.username) + click_filter_link(user.username) + submit_filter wait_for_vue_resource @@ -396,17 +380,9 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by milestone' do - page.within '.issues-filters' do - click_button('Milestone') - wait_for_ajax - - page.within '.milestone-filter' do - click_link(milestone.title) - end - wait_for_vue_resource - - expect(find('.js-milestone-select')).to have_content(milestone.title) - end + set_filter("milestone", "\"#{milestone.title}\"") + click_filter_link(milestone.title) + submit_filter wait_for_vue_resource wait_for_board_cards(1, 1) @@ -415,16 +391,9 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by label' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter wait_for_vue_resource wait_for_board_cards(1, 1) @@ -432,19 +401,14 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by label with space after reload' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(accepting.title) - wait_for_vue_resource(spinner: false) - find('.dropdown-menu-close').click - end - end + set_filter("label", "\"#{accepting.title}\"") + click_filter_link(accepting.title) + submit_filter # Test after reload page.evaluate_script 'window.location.reload()' + wait_for_board_cards(1, 1) + wait_for_empty_boards((2..3)) wait_for_vue_resource @@ -460,26 +424,16 @@ describe 'Issue Boards', feature: true, js: true do end it 'removes filtered labels' do - wait_for_vue_resource - - page.within '.labels-filter' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource(spinner: false) - end + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter - expect(page).to have_css('input[name="label_name[]"]', visible: false) + wait_for_board_cards(1, 1) - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource(spinner: false) - end + find('.clear-search').click + submit_filter - expect(page).not_to have_css('input[name="label_name[]"]', visible: false) - end + wait_for_board_cards(1, 8) end it 'infinite scrolls list with label filter' do @@ -487,16 +441,9 @@ describe 'Issue Boards', feature: true, js: true do create(:labeled_issue, project: project, labels: [planning, testing]) end - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter wait_for_vue_resource @@ -518,18 +465,13 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by multiple labels' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax + set_filter("label", testing.title) + click_filter_link(testing.title) - page.within(find('.dropdown-menu-labels')) do - click_link(testing.title) - wait_for_vue_resource - click_link(bug.title) - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end + set_filter("label", bug.title) + click_filter_link(bug.title) + + submit_filter wait_for_vue_resource @@ -545,14 +487,14 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource end + page.within('.tokens-container') do + expect(page).to have_content(bug.title) + end + wait_for_vue_resource wait_for_board_cards(1, 1) wait_for_empty_boards((2..3)) - - page.within('.labels-filter') do - expect(find('.dropdown-toggle-text')).to have_content(bug.title) - end end it 'removes label filter by clicking label button on issue' do @@ -560,16 +502,13 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.card', match: :first)) do click_button(bug.title) end + wait_for_vue_resource expect(page).to have_selector('.card', count: 1) end wait_for_vue_resource - - page.within('.labels-filter') do - expect(find('.dropdown-toggle-text')).to have_content(bug.title) - end end end end @@ -643,4 +582,20 @@ describe 'Issue Boards', feature: true, js: true do wait_for_board_cards(board, 0) end end + + def set_filter(type, text) + find('.filtered-search').native.send_keys("#{type}:#{text}") + end + + def submit_filter + find('.filtered-search').native.send_keys(:enter) + end + + def click_filter_link(link_text) + page.within('.filtered-search-input-container') do + expect(page).to have_button(link_text) + + click_button(link_text) + end + end end diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb index 73553f97d6f..bfe43bff10f 100644 --- a/spec/features/issuables/default_sort_order_spec.rb +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -176,7 +176,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do end def selected_sort_order - find('.pull-right .dropdown button').text.downcase + find('.filter-dropdown-container .dropdown button').text.downcase end def visit_merge_requests_with_state(project, state) -- cgit v1.2.1 From a12b99a7698c851ddb5ea91916e19241fb189ced Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 12:21:07 +0000 Subject: Fixed eslint errors --- app/assets/javascripts/boards/boards_bundle.js | 2 +- app/assets/javascripts/boards/models/list.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 9e9da7dfac4..2fd1f43f02c 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,4 +1,4 @@ -/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, no-new */ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* global Vue */ /* global BoardService */ diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index b246c3c1503..c2af3bb881c 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -102,7 +102,6 @@ class List { } this.createIssues(data.issues); - console.log(this.issues.length); }); } -- cgit v1.2.1 From 9ef84008d65dcdb5a9e2d83e7a0c053044fc91f7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 14:00:28 +0000 Subject: Hides on mobile --- app/views/projects/boards/_show.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index a3593c9f5db..fa463edd526 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -13,7 +13,8 @@ = render "projects/issues/head" -= render 'shared/issuable/search_bar', type: :boards +.hidden-xs.hidden-sm + = render 'shared/issuable/search_bar', type: :boards #board-app.boards-app{ "v-cloak" => true, data: board_data } .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } -- cgit v1.2.1 From 507c2d38a059e26d4cb203babe7bf430804f26ee Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Wed, 8 Mar 2017 15:07:35 +0100 Subject: Update GITLAB_SHELL_VERSION to 5.0.0 Related to gitlab-ee#1648 --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 627a3f43a64..0062ac97180 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -4.1.1 +5.0.0 -- cgit v1.2.1 From d701b39db9d458919976249a4b7c8bb5597b3606 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 14:46:46 +0000 Subject: Fixed up boards filter spec due to CSS classes changing Also fixed issue with Vue resource encoding + in search term --- app/assets/javascripts/boards/models/list.js | 3 ++- spec/features/boards/boards_spec.rb | 14 ++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index c2af3bb881c..ad968d2120f 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -70,7 +70,8 @@ class List { const paramSplit = filterParam.split('='); const paramKeyNormalized = paramSplit[0].replace('[]', ''); const isArray = paramSplit[0].indexOf('[]'); - const value = decodeURIComponent(paramSplit[1]); + let value = decodeURIComponent(paramSplit[1]); + value = value.replace(/\+/g, ' '); if (isArray >= 0) { if (!data[paramKeyNormalized]) { diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index e11ba10c80c..f7e8b78b54d 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -29,7 +29,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'shows tooltip on add issues button' do - button = page.find('.issue-boards-search button', text: 'Add issues') + button = page.find('.filter-dropdown-container button', text: 'Add issues') expect(button[:"data-original-title"]).to eq("Please add a list to your board first") end @@ -115,9 +115,8 @@ describe 'Issue Boards', feature: true, js: true do end it 'search done list' do - page.within('#js-boards-search') do - find('.form-control').set(issue8.title) - end + find('.filtered-search').set(issue8.title) + find('.filtered-search').native.send_keys(:enter) wait_for_vue_resource @@ -127,9 +126,8 @@ describe 'Issue Boards', feature: true, js: true do end it 'search list' do - page.within('#js-boards-search') do - find('.form-control').set(issue5.title) - end + find('.filtered-search').set(issue5.title) + find('.filtered-search').native.send_keys(:enter) wait_for_vue_resource @@ -333,7 +331,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(find('.issue-boards-search')).to have_selector('.open') + expect(page).to have_css('#js-add-list.open') end it 'creates new list from a new label' do -- cgit v1.2.1 From 236d6595edd2393f4ba4faadd39529fcabe48aec Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 14:53:18 +0000 Subject: Removed previous filter code --- app/assets/javascripts/labels_select.js | 18 ++---------------- app/assets/javascripts/milestone_select.js | 8 +------- app/assets/javascripts/users_select.js | 5 ----- 3 files changed, 3 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 9e2d14c7f87..c648a0f076c 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -353,31 +353,17 @@ return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && - !$dropdown.closest('.add-issues-modal').length) { - boardsModel = gl.issueBoards.BoardsStore.state.filters; - } else if ($dropdown.closest('.add-issues-modal').length) { + if ($dropdown.closest('.add-issues-modal').length) { boardsModel = gl.issueBoards.ModalStore.store.filter; } if (boardsModel) { if (label.isAny) { boardsModel['label_name'] = []; - } - else if ($el.hasClass('is-active')) { + } else if ($el.hasClass('is-active')) { boardsModel['label_name'].push(label.title); } - else { - var filters = boardsModel['label_name']; - filters = filters.filter(function (filteredLabel) { - return filteredLabel !== label.title; - }); - boardsModel['label_name'] = filters; - } - if (!$dropdown.closest('.add-issues-modal').length) { - gl.issueBoards.BoardsStore.updateFiltersUrl(); - } e.preventDefault(); return; } diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 51fa5c828b3..4c4f94cb9f3 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -124,18 +124,12 @@ return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && - !$dropdown.closest('.add-issues-modal').length) { - boardsStore = gl.issueBoards.BoardsStore.state.filters; - } else if ($dropdown.closest('.add-issues-modal').length) { + if ($dropdown.closest('.add-issues-modal').length) { boardsStore = gl.issueBoards.ModalStore.store.filter; } if (boardsStore) { boardsStore[$dropdown.data('field-name')] = selected.name; - if (!$dropdown.closest('.add-issues-modal').length) { - gl.issueBoards.BoardsStore.updateFiltersUrl(); - } e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (selected.name != null) { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 27af859f7d8..c7a57b47834 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -217,11 +217,6 @@ } if ($el.closest('.add-issues-modal').length) { gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; - } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { - selectedId = user.id; - gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; - gl.issueBoards.BoardsStore.updateFiltersUrl(); - e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { selectedId = user.id; return Issuable.filterResults($dropdown.closest('form')); -- cgit v1.2.1 From e2a63cbdc28f4d1ba0ad71ee78cba2d5e946fadc Mon Sep 17 00:00:00 2001 From: George Andrinopoulos Date: Wed, 8 Mar 2017 16:35:49 +0200 Subject: Refactor dropdown_milestone_spec.rb --- .../29162-refactor-dropdown-milestone-spec.yml | 4 ++ .../filtered_search/dropdown_milestone_spec.rb | 58 ++++++++++++---------- 2 files changed, 35 insertions(+), 27 deletions(-) create mode 100644 changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml diff --git a/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml new file mode 100644 index 00000000000..ad0c513f525 --- /dev/null +++ b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml @@ -0,0 +1,4 @@ +--- +title: Refactor dropdown_milestone_spec.rb +merge_request: +author: George Andrinopoulos diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 0324fcad0a0..85ffffe4b6d 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -1,8 +1,7 @@ require 'rails_helper' -describe 'Dropdown milestone', js: true, feature: true do +describe 'Dropdown milestone', :feature, :js do include FilteredSearchHelpers - include WaitForAjax let!(:project) { create(:empty_project) } let!(:user) { create(:user) } @@ -15,18 +14,10 @@ describe 'Dropdown milestone', js: true, feature: true do let(:filtered_search) { find('.filtered-search') } let(:js_dropdown_milestone) { '#js-dropdown-milestone' } - - def send_keys_to_filtered_search(input) - input.split("").each do |i| - filtered_search.send_keys(i) - sleep 3 - wait_for_ajax - sleep 3 - end - end + let(:filter_dropdown) { find("#{js_dropdown_milestone} .filter-dropdown") } def dropdown_milestone_size - page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size + filter_dropdown.all('.filter-dropdown-item').size end def click_milestone(text) @@ -65,13 +56,14 @@ describe 'Dropdown milestone', js: true, feature: true do end it 'should hide loading indicator when loaded' do - send_keys_to_filtered_search('milestone:') + filtered_search.set('milestone:') - expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading') + expect(find(js_dropdown_milestone)).to have_css('.filter-dropdown-loading') + expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading') end it 'should load all the milestones when opened' do - send_keys_to_filtered_search('milestone:') + filtered_search.set('milestone:') expect(dropdown_milestone_size).to be > 0 end @@ -79,41 +71,48 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('milestone') + filtered_search.set('milestone:') + + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title) end it 'filters by name' do - send_keys_to_filtered_search(':v1') + filtered_search.send_keys('v1') expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name' do - send_keys_to_filtered_search(':V1') + filtered_search.send_keys('V1') expect(dropdown_milestone_size).to eq(1) end it 'filters by name with symbol' do - send_keys_to_filtered_search(':%v1') + filtered_search.send_keys('%v1') expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name with symbol' do - send_keys_to_filtered_search(':%V1') + filtered_search.send_keys('%V1') expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters' do - send_keys_to_filtered_search(':(+') + filtered_search.send_keys('(+') expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters with symbol' do - send_keys_to_filtered_search(':%(+') + filtered_search.send_keys('%(+') expect(dropdown_milestone_size).to eq(1) end @@ -122,6 +121,13 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'selecting from dropdown' do before do filtered_search.set('milestone:') + + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title) end it 'fills in the milestone name when the milestone has not been filled' do @@ -133,7 +139,7 @@ describe 'Dropdown milestone', js: true, feature: true do end it 'fills in the milestone name when the milestone is partially filled' do - send_keys_to_filtered_search('v') + filtered_search.send_keys('v') click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) @@ -232,16 +238,14 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'caching requests' do it 'caches requests after the first load' do - filtered_search.set('milestone') - send_keys_to_filtered_search(':') + filtered_search.set('milestone:') initial_size = dropdown_milestone_size expect(initial_size).to be > 0 create(:milestone, project: project) find('.filtered-search-input-container .clear-search').click - filtered_search.set('milestone') - send_keys_to_filtered_search(':') + filtered_search.set('milestone:') expect(dropdown_milestone_size).to eq(initial_size) end -- cgit v1.2.1 From 1b2fcdb01d0f650c336bc4ede05e9e8095e8f765 Mon Sep 17 00:00:00 2001 From: dimitrieh Date: Tue, 7 Mar 2017 00:43:24 +0100 Subject: Changed dropdown style slightly --- app/assets/stylesheets/framework/dropdowns.scss | 24 +++++++++++++--------- .../26202-change-dropdown-style-slightly.yml | 4 ++++ 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/26202-change-dropdown-style-slightly.yml diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 887ab481de4..fe8b37d2c6e 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -159,12 +159,12 @@ li { text-align: left; list-style: none; - padding: 0 8px; + padding: 0 10px; } .divider { height: 1px; - margin: 8px; + margin: 6px 10px; padding: 0; background-color: $dropdown-divider-color; } @@ -181,7 +181,7 @@ display: block; position: relative; padding: 5px 8px; - color: $dropdown-link-color; + color: $gl-text-color; line-height: initial; text-overflow: ellipsis; border-radius: 2px; @@ -218,11 +218,12 @@ } .dropdown-header { - color: $gl-text-color-secondary; + color: $gl-text-color; font-size: 13px; + font-weight: 600; line-height: 22px; text-transform: capitalize; - padding: 0 10px; + padding: 0 16px; } .separator + .dropdown-header { @@ -325,14 +326,17 @@ .dropdown-menu-selectable { a { - padding-left: 25px; + padding-left: 26px; &.is-indeterminate, &.is-active { + font-weight: 600; + color: $gl-text-color; + &::before { position: absolute; - left: 5px; - top: 8px; + left: 6px; + top: 6px; font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; @@ -354,7 +358,7 @@ .dropdown-title { position: relative; - padding: 0 25px 10px; + padding: 2px 25px 10px; margin: 0 10px 10px; font-weight: 600; line-height: 1; @@ -384,7 +388,7 @@ right: 5px; width: 20px; height: 20px; - top: -3px; + top: -1px; } .dropdown-menu-back { diff --git a/changelogs/unreleased/26202-change-dropdown-style-slightly.yml b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml new file mode 100644 index 00000000000..827224abf5a --- /dev/null +++ b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml @@ -0,0 +1,4 @@ +--- +title: Changed dropdown style slightly +merge_request: +author: -- cgit v1.2.1 From 1466b7eecc56194e2967393dec559912df20e9bb Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 16:07:26 +0000 Subject: Added discussion comments avatars to diff --- .../diff_notes/components/diff_note_avatars.js | 155 +++++++++++++++++++++ .../diff_notes/components/resolve_btn.js | 16 ++- .../javascripts/diff_notes/diff_notes_bundle.js | 10 ++ .../javascripts/diff_notes/icons/collapse_icon.svg | 1 + .../javascripts/diff_notes/models/discussion.js | 4 +- app/assets/javascripts/diff_notes/models/note.js | 13 +- .../javascripts/diff_notes/stores/comments.js | 6 +- app/assets/javascripts/files_comment_button.js | 3 + app/assets/javascripts/main.js | 6 +- app/assets/javascripts/notes.js | 105 +++++++++++--- app/assets/stylesheets/pages/diff.scss | 104 ++++++++++++++ app/views/discussions/_diff_discussion.html.haml | 2 +- app/views/projects/diffs/_line.html.haml | 18 +-- app/views/projects/diffs/_parallel_view.html.haml | 17 ++- app/views/projects/notes/_note.html.haml | 10 +- app/views/shared/icons/_collapse.svg.erb | 1 + .../merge_requests/diff_notes_avatars_spec.rb | 136 ++++++++++++++++++ spec/javascripts/diff_comments_store_spec.js | 11 +- 18 files changed, 566 insertions(+), 52 deletions(-) create mode 100644 app/assets/javascripts/diff_notes/components/diff_note_avatars.js create mode 100644 app/assets/javascripts/diff_notes/icons/collapse_icon.svg create mode 100644 app/views/shared/icons/_collapse.svg.erb create mode 100644 spec/features/merge_requests/diff_notes_avatars_spec.rb diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js new file mode 100644 index 00000000000..788daa96b3d --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -0,0 +1,155 @@ +/* global CommentsStore Cookies notes */ +import Vue from 'vue'; +import collapseIcon from '../icons/collapse_icon.svg'; + +(() => { + const DiffNoteAvatars = Vue.extend({ + props: ['discussionId'], + data() { + return { + isVisible: false, + lineType: '', + storeState: CommentsStore.state, + shownAvatars: 3, + collapseIcon, + }; + }, + template: ` +
+
+ + {{ moreText }} +
+ +
+ `, + mounted() { + this.$nextTick(() => { + this.addNoCommentClass(); + this.setDiscussionVisible(); + + this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; + }); + + $(document).on('toggle.comments', () => { + this.$nextTick(() => { + this.setDiscussionVisible(); + }); + }); + }, + destroyed() { + $(document).off('toggle.comments'); + }, + watch: { + storeState: { + handler() { + this.$nextTick(() => { + $('.has-tooltip', this.$el).tooltip('fixTitle'); + + // We need to add/remove a class to an element that is outside the Vue instance + this.addNoCommentClass(); + }); + }, + deep: true, + }, + }, + computed: { + notesSubset() { + let notes = []; + + if (this.discussion) { + notes = Object.keys(this.discussion.notes) + .slice(0, this.shownAvatars) + .map(noteId => this.discussion.notes[noteId]); + } + + return notes; + }, + extraNotesTitle() { + if (this.discussion) { + const extra = this.discussion.notesCount() - this.shownAvatars; + + return `${extra} more comment${extra > 1 ? 's' : ''}`; + } + + return ''; + }, + discussion() { + return this.storeState[this.discussionId]; + }, + notesCount() { + if (this.discussion) { + return this.discussion.notesCount(); + } + + return 0; + }, + moreText() { + const plusSign = this.notesCount < 100 ? '+' : ''; + + return `${plusSign}${this.notesCount - this.shownAvatars}`; + }, + }, + methods: { + clickedAvatar(e) { + notes.addDiffNote(e); + + // Toggle the active state of the toggle all button + this.toggleDiscussionsToggleState(); + + this.$nextTick(() => { + this.setDiscussionVisible(); + + $('.has-tooltip', this.$el).tooltip('fixTitle'); + $('.has-tooltip', this.$el).tooltip('hide'); + }); + }, + addNoCommentClass() { + const notesCount = this.notesCount; + + $(this.$el).closest('.js-avatar-container') + .toggleClass('js-no-comment-btn', notesCount > 0) + .nextUntil('.js-avatar-container') + .toggleClass('js-no-comment-btn', notesCount > 0); + }, + toggleDiscussionsToggleState() { + const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); + const $visibleNotesHolders = $notesHolders.filter(':visible'); + const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments'); + + $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length); + }, + setDiscussionVisible() { + this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); + }, + }, + }); + + Vue.component('diff-note-avatars', DiffNoteAvatars); +})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index d1873d6c7a2..fbd980f0fce 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -11,7 +11,10 @@ const Vue = require('vue'); discussionId: String, resolved: Boolean, canResolve: Boolean, - resolvedBy: String + resolvedBy: String, + authorName: String, + authorAvatar: String, + noteTruncated: String, }, data: function () { return { @@ -98,7 +101,16 @@ const Vue = require('vue'); CommentsStore.delete(this.discussionId, this.noteId); }, created: function () { - CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy); + CommentsStore.create({ + discussionId: this.discussionId, + noteId: this.noteId, + canResolve: this.canResolve, + resolved: this.resolved, + resolvedBy: this.resolvedBy, + authorName: this.authorName, + authorAvatar: this.authorAvatar, + noteTruncated: this.noteTruncated, + }); this.note = this.discussion.getNote(this.noteId); } diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index cadf8b96b87..7d8316dfd63 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -13,6 +13,7 @@ require('./components/jump_to_discussion'); require('./components/resolve_btn'); require('./components/resolve_count'); require('./components/resolve_discussion_btn'); +require('./components/diff_note_avatars'); $(() => { const projectPath = document.querySelector('.merge-request').dataset.projectPath; @@ -24,6 +25,15 @@ $(() => { window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath); gl.diffNotesCompileComponents = () => { + $('diff-note-avatars').each(function () { + const tmp = Vue.extend({ + template: $(this).get(0).outerHTML + }); + const tmpApp = new tmp().$mount(); + + $(this).replaceWith(tmpApp.$el); + }); + const $components = $(COMPONENT_SELECTOR).filter(function () { return $(this).closest('resolve-count').length !== 1; }); diff --git a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg new file mode 100644 index 00000000000..bd4b393cfaa --- /dev/null +++ b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg @@ -0,0 +1 @@ + diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js index fa518ba4d33..dce1a9b58bd 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js +++ b/app/assets/javascripts/diff_notes/models/discussion.js @@ -10,8 +10,8 @@ class DiscussionModel { this.canResolve = false; } - createNote (noteId, canResolve, resolved, resolved_by) { - Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by)); + createNote (noteObj) { + Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj)); } deleteNote (noteId) { diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js index f3a7cba5ef6..04465aa507e 100644 --- a/app/assets/javascripts/diff_notes/models/note.js +++ b/app/assets/javascripts/diff_notes/models/note.js @@ -1,12 +1,15 @@ /* eslint-disable camelcase, no-unused-vars */ class NoteModel { - constructor(discussionId, noteId, canResolve, resolved, resolved_by) { + constructor(discussionId, noteObj) { this.discussionId = discussionId; - this.id = noteId; - this.canResolve = canResolve; - this.resolved = resolved; - this.resolved_by = resolved_by; + this.id = noteObj.noteId; + this.canResolve = noteObj.canResolve; + this.resolved = noteObj.resolved; + this.resolved_by = noteObj.resolvedBy; + this.authorName = noteObj.authorName; + this.authorAvatar = noteObj.authorAvatar; + this.noteTruncated = noteObj.noteTruncated; } } diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js index c80d979b977..69c4d7a8434 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js +++ b/app/assets/javascripts/diff_notes/stores/comments.js @@ -21,10 +21,10 @@ return discussion; }, - create: function (discussionId, noteId, canResolve, resolved, resolved_by) { - const discussion = this.createDiscussion(discussionId); + create: function (noteObj) { + const discussion = this.createDiscussion(noteObj.discussionId); - discussion.createNote(noteId, canResolve, resolved, resolved_by); + discussion.createNote(noteObj); }, update: function (discussionId, noteId, resolved, resolved_by) { const discussion = this.state[discussionId]; diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 6d86888dcb8..bf84f2a0a8f 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -38,6 +38,9 @@ FilesCommentButton.prototype.render = function(e) { var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; $currentTarget = $(e.currentTarget); + + if ($currentTarget.hasClass('js-no-comment-btn')) return; + lineContentElement = this.getLineContent($currentTarget); buttonParentElement = this.getButtonParent($currentTarget); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index ae4dd64424c..79164edff0e 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -342,11 +342,11 @@ require('./zen_mode'); var notesHolders = $this.closest('.diff-file').find('.notes_holder'); $this.toggleClass('active'); if ($this.hasClass('active')) { - notesHolders.show().find('.hide').show(); + notesHolders.show().find('.hide, .content').show(); } else { - notesHolders.hide(); + notesHolders.hide().find('.content').hide(); } - $this.trigger('blur'); + $(document).trigger('toggle.comments'); return e.preventDefault(); }); $document.off('click', '.js-confirm-danger'); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index df7a7d2a459..eeab69da941 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -312,7 +312,7 @@ require('./task_list'); */ Notes.prototype.renderDiscussionNote = function(note) { - var discussionContainer, form, note_html, row; + var discussionContainer, form, note_html, row, lineType, diffAvatarContainer; if (!this.isNewNote(note)) { return; } @@ -322,6 +322,8 @@ require('./task_list'); form = $("#new-discussion-note-form-" + note.original_discussion_id); } row = form.closest("tr"); + lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; + diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); note_html = $(note.html); note_html.renderGFM(); // is this the first note of discussion? @@ -330,10 +332,26 @@ require('./task_list'); discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']"); } if (discussionContainer.length === 0) { - // insert the note and the reply button after the temp row - row.after(note.diff_discussion_html); - // remove the note (will be added again below) - row.next().find(".note").remove(); + if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { + // insert the note and the reply button after the temp row + row.after(note.diff_discussion_html); + + // remove the note (will be added again below) + row.next().find(".note").remove(); + } else { + // Merge new discussion HTML in + var $discussion = $(note.diff_discussion_html); + var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]'); + var contentContainerClass = '.' + $notes.closest('.notes_content') + .attr('class') + .split(' ') + .join('.'); + + // remove the note (will be added again below) + $notes.find('.note').remove(); + + row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); + } // Before that, the container didn't exist discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); // Add note to 'Changes' page discussions @@ -347,14 +365,40 @@ require('./task_list'); discussionContainer.append(note_html); } - if (typeof gl.diffNotesCompileComponents !== 'undefined') { + if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_id) { gl.diffNotesCompileComponents(); + this.renderDiscussionAvatar(diffAvatarContainer, note); } gl.utils.localTimeAgo($('.js-timeago'), false); return this.updateNotesCount(1); }; + Notes.prototype.getLineHolder = function(changesDiscussionContainer) { + return $(changesDiscussionContainer).closest('.notes_holder') + .prevAll('.line_holder') + .first() + .get(0); + }; + + Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) { + var commentButton = diffAvatarContainer.find('.js-add-diff-note-button'); + var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); + + if (!avatarHolder.length) { + avatarHolder = document.createElement('diff-note-avatars'); + avatarHolder.setAttribute('discussion-id', note.discussion_id); + + diffAvatarContainer.append(avatarHolder); + + gl.diffNotesCompileComponents(); + } + + if (commentButton.length) { + commentButton.remove(); + } + }; + /* Called in response the main target form has been successfully submitted. @@ -592,9 +636,14 @@ require('./task_list'); */ Notes.prototype.removeNote = function(e) { - var noteId; - noteId = $(e.currentTarget).closest(".note").attr("id"); - $(".note[id='" + noteId + "']").each((function(_this) { + var noteElId, noteId, dataNoteId, $note, lineHolder; + $note = $(e.currentTarget).closest('.note'); + noteElId = $note.attr('id'); + noteId = $note.attr('data-note-id'); + lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') + .closest('.notes_holder') + .prev('.line_holder'); + $(".note[id='" + noteElId + "']").each((function(_this) { // A same note appears in the "Discussion" and in the "Changes" tab, we have // to remove all. Using $(".note[id='noteId']") ensure we get all the notes, // where $("#noteId") would return only one. @@ -604,17 +653,26 @@ require('./task_list'); notes = note.closest(".notes"); if (typeof gl.diffNotesCompileComponents !== 'undefined') { - if (gl.diffNoteApps[noteId]) { - gl.diffNoteApps[noteId].$destroy(); + if (gl.diffNoteApps[noteElId]) { + gl.diffNoteApps[noteElId].$destroy(); } } + note.remove(); + // check if this is the last note for this line - if (notes.find(".note").length === 1) { + if (notes.find(".note").length === 0) { + var notesTr = notes.closest("tr"); + // "Discussions" tab notes.closest(".timeline-entry").remove(); - // "Changes" tab / commit view - notes.closest("tr").remove(); + + if (!_this.isParallelView() || notesTr.find('.note').length === 0) { + // "Changes" tab / commit view + notesTr.remove(); + } else { + notes.closest('.content').empty(); + } } return note.remove(); }; @@ -707,15 +765,16 @@ require('./task_list'); */ Notes.prototype.addDiffNote = function(e) { - var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent; + var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; e.preventDefault(); - $link = $(e.currentTarget); + $link = $(e.currentTarget || e.target); row = $link.closest("tr"); nextRow = row.next(); hasNotes = nextRow.is(".notes_holder"); addForm = false; notesContentSelector = ".notes_content"; rowCssToAdd = "
"; + isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar'); // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineType = $link.data("lineType"); @@ -723,7 +782,9 @@ require('./task_list'); rowCssToAdd = "
"; } notesContentSelector += " .content"; - if (hasNotes) { + notesContent = nextRow.find(notesContentSelector); + + if (hasNotes && !isDiffCommentAvatar) { nextRow.show(); notesContent = nextRow.find(notesContentSelector); if (notesContent.length) { @@ -740,13 +801,21 @@ require('./task_list'); } } } - } else { + } else if (!isDiffCommentAvatar) { // add a notes row and insert the form row.after(rowCssToAdd); nextRow = row.next(); notesContent = nextRow.find(notesContentSelector); addForm = true; + } else { + nextRow.show(); + notesContent.toggle(!notesContent.is(':visible')); + + if (!nextRow.find('.content:not(:empty)').is(':visible')) { + nextRow.hide(); + } } + if (addForm) { newForm = this.formClone.clone(); newForm.appendTo(notesContent); diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 5d0c247dea8..eab79c2a481 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -113,6 +113,10 @@ td.line_content.parallel { width: 46%; } + + .add-diff-note { + margin-left: -55px; + } } .old_line, @@ -490,3 +494,103 @@ } } } + +.diff-comment-avatar-holders { + position: absolute; + height: 19px; + width: 19px; + margin-left: -15px; + + &:hover { + .diff-comment-avatar, + .diff-comments-more-count { + @for $i from 1 through 4 { + $x-pos: 14px; + + &:nth-child(#{$i}) { + @if $i == 4 { + $x-pos: 14.5px; + } + + transform: translateX((($i * $x-pos) - $x-pos)); + + &:hover { + transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); + } + } + } + } + + .diff-comments-more-count { + padding-left: 2px; + padding-right: 2px; + width: auto; + } + } +} + +.diff-comment-avatar, +.diff-comments-more-count { + position: absolute; + left: 0; + width: 19px; + height: 19px; + margin-right: 0; + border-color: $white-light; + cursor: pointer; + transition: all .1s ease-out; + + @for $i from 1 through 4 { + &:nth-child(#{$i}) { + z-index: (4 - $i); + } + } +} + +.diff-comments-more-count { + width: 19px; + min-width: 19px; + padding-left: 0; + padding-right: 0; + overflow: hidden; +} + +.diff-comments-more-count, +.diff-notes-collapse { + background-color: $gray-darkest; + color: $white-light; + border: 1px solid $white-light; + border-radius: 1em; + font-family: $regular_font; + font-size: 9px; + line-height: 17px; + text-align: center; +} + +.diff-notes-collapse { + position: relative; + width: 19px; + height: 19px; + padding: 0; + transition: transform .1s ease-out; + + svg { + position: absolute; + left: 50%; + top: 50%; + margin-left: -5.5px; + margin-top: -5.5px; + } + + path { + fill: $white-light; + } + + &:hover { + transform: scale(1.2); + } + + &:focus { + outline: 0; + } +} diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 2deadbeeceb..ee452add394 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -2,5 +2,5 @@ %tr.notes_holder{ class: ('hide' unless expanded) } %td.notes_line{ colspan: 2 } %td.notes_content - .content + .content{ class: ('hide' unless expanded) } = render "discussions/notes", discussion: discussion diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index cd18ba2ed00..ed279cfe168 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,8 +1,11 @@ - email = local_assigns.fetch(:email, false) - plain = local_assigns.fetch(:plain, false) +- discussions = local_assigns.fetch(:discussions, nil) - type = line.type - line_code = diff_file.line_code(line) -%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } } +- if discussions && !line.meta? + - discussion = discussions[line_code] +%tr.line_holder{ class: type, id: (line_code unless plain) } - case type - when 'match' = diff_match_line line.old_pos, line.new_pos, text: line.text @@ -11,12 +14,14 @@ %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } } + %td.old_line.diff-line-num.js-avatar-container{ class: type, data: { linenumber: line.old_pos } } - link_text = type == "new" ? " " : line.old_pos - if plain = link_text - else %a{ href: "##{line_code}", data: { linenumber: link_text } } + - if discussion && !plain + %diff-note-avatars{ "discussion-id" => discussion.id } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " " : line.new_pos - if plain @@ -29,9 +34,6 @@ - else = diff_line_content(line.text) -- discussions = local_assigns.fetch(:discussions, nil) -- if discussions && !line.meta? - - discussion = discussions[line_code] - - if discussion - - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?) - = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded +- if discussion + - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?) + = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 997bf0fc560..6448748113b 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -4,6 +4,9 @@ - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] + - last_line = right.new_pos if right + - unless @diff_notes_disabled + - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) %tr.line_holder.parallel - if left - case left.type @@ -15,8 +18,10 @@ - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) - %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } + %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } + - if discussion_left + %diff-note-avatars{ "discussion-id" => discussion_left.id } %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell @@ -32,17 +37,17 @@ - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) - %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } + %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } + - if discussion_right + %diff-note-avatars{ "discussion-id" => discussion_right.id } %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel - - unless @diff_notes_disabled - - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) - - if discussion_left || discussion_right - = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right + - if discussion_left || discussion_right + = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right - if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any? - last_line = diff_file.diff_lines.last - if last_line.new_pos < total_lines diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index a73e8f345e0..a7618370a5d 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -2,7 +2,7 @@ - return if note.cross_reference_not_visible_for?(current_user) - note_editable = note_editable?(note) -%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } +%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} } .timeline-entry-inner .timeline-icon %a{ href: user_path(note.author) } @@ -30,11 +30,15 @@ - if note.resolvable? - can_resolve = can?(current_user, :resolve_note, note) - %resolve-btn{ "discussion-id" => "#{note.discussion_id}", + %resolve-btn{ "project-path" => project_path(note.project), + "discussion-id" => note.discussion_id, ":note-id" => note.id, ":resolved" => note.resolved?, ":can-resolve" => can_resolve, - "resolved-by" => "#{note.resolved_by.try(:name)}", + ":author-name" => "'#{j(note.author.name)}'", + "author-avatar" => note.author.avatar_url, + ":note-truncated" => "'#{truncate(note.note, length: 17)}'", + ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'", "v-show" => "#{can_resolve || note.resolved?}", "inline-template" => true, "ref" => "note_#{note.id}" } diff --git a/app/views/shared/icons/_collapse.svg.erb b/app/views/shared/icons/_collapse.svg.erb new file mode 100644 index 00000000000..917753fb343 --- /dev/null +++ b/app/views/shared/icons/_collapse.svg.erb @@ -0,0 +1 @@ + diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb new file mode 100644 index 00000000000..7df102067d6 --- /dev/null +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -0,0 +1,136 @@ +require 'spec_helper' + +feature 'Diff note avatars', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") } + let(:path) { "files/ruby/popen.rb" } + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 9, + diff_refs: merge_request.diff_refs + ) + end + let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) } + + before do + project.team << [user, :master] + login_as user + end + + %w(inline parallel).each do |view| + context "#{view} view" do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view) + + wait_for_ajax + end + + it 'shows note avatar' do + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(page).to have_selector('img.js-diff-comment-avatar', count: 1) + end + end + + it 'shows comment on note avatar' do + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(first('img.js-diff-comment-avatar')["title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") + end + end + + it 'toggles comments when clicking avatar' do + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + end + + expect(page).to have_selector('.notes_holder', visible: false) + + page.within find("[id='#{position.line_code(project.repository)}']") do + first('img.js-diff-comment-avatar').click + end + + expect(page).to have_selector('.notes_holder') + end + + it 'removes avatar when note is deleted' do + page.within find(".note-row-#{note.id}") do + find('.js-note-delete').click + end + + wait_for_ajax + + page.within find("[id='#{position.line_code(project.repository)}']") do + expect(page).not_to have_selector('img.js-diff-comment-avatar') + end + end + + it 'adds avatar when commenting' do + click_button 'Reply...' + + page.within '.js-discussion-note-form' do + find('.js-note-text').native.send_keys('Test') + + click_button 'Comment' + + wait_for_ajax + end + + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) + end + end + + it 'adds multiple comments' do + 3.times do + click_button 'Reply...' + + page.within '.js-discussion-note-form' do + find('.js-note-text').native.send_keys('Test') + + find('.js-comment-button').trigger 'click' + + wait_for_ajax + end + end + + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(page).to have_selector('img.js-diff-comment-avatar', count: 3) + expect(find('.diff-comments-more-count')).to have_content '+1' + end + end + + context 'multiple comments' do + before do + create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) + create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) + create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) + + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view) + + wait_for_ajax + end + + it 'shows extra comment count' do + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(find('.diff-comments-more-count')).to have_content '+1' + end + end + end + end + end +end diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/javascripts/diff_comments_store_spec.js index f956394ef53..84cf98c930a 100644 --- a/spec/javascripts/diff_comments_store_spec.js +++ b/spec/javascripts/diff_comments_store_spec.js @@ -7,7 +7,16 @@ require('~/diff_notes/stores/comments'); (() => { function createDiscussion(noteId = 1, resolved = true) { - CommentsStore.create('a', noteId, true, resolved, 'test'); + CommentsStore.create({ + discussionId: 'a', + noteId, + canResolve: true, + resolved, + resolvedBy: 'test', + authorName: 'test', + authorAvatar: 'test', + noteTruncated: 'test...', + }); } beforeEach(() => { -- cgit v1.2.1 From 3403bdc5f2aa97331ea1f4766dc08bbcd243f70d Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 8 Mar 2017 10:12:37 -0600 Subject: Fix reference to node_modules in built package Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/29192 See https://gitlab.slack.com/archives/frontend/p1488973548024005 --- fixtures/emojis/emoji-unicode-version-map.json | 2377 ++++++++++++++++++++++++ lib/gitlab/emoji.rb | 2 +- lib/tasks/gemojione.rake | 5 +- 3 files changed, 2382 insertions(+), 2 deletions(-) create mode 100644 fixtures/emojis/emoji-unicode-version-map.json diff --git a/fixtures/emojis/emoji-unicode-version-map.json b/fixtures/emojis/emoji-unicode-version-map.json new file mode 100644 index 00000000000..5164fe39426 --- /dev/null +++ b/fixtures/emojis/emoji-unicode-version-map.json @@ -0,0 +1,2377 @@ +{ + "100": "6.0", + "1234": "6.0", + "grinning": "6.1", + "grin": "6.0", + "joy": "6.0", + "rofl": "9.0", + "rolling_on_the_floor_laughing": "9.0", + "smiley": "6.0", + "smile": "6.0", + "sweat_smile": "6.0", + "laughing": "6.0", + "satisfied": "6.0", + "wink": "6.0", + "blush": "6.0", + "yum": "6.0", + "sunglasses": "6.0", + "heart_eyes": "6.0", + "kissing_heart": "6.0", + "kissing": "6.1", + "kissing_smiling_eyes": "6.1", + "kissing_closed_eyes": "6.0", + "relaxed": "1.1", + "slight_smile": "7.0", + "slightly_smiling_face": "7.0", + "hugging": "8.0", + "hugging_face": "8.0", + "thinking": "8.0", + "thinking_face": "8.0", + "neutral_face": "6.0", + "expressionless": "6.1", + "no_mouth": "6.0", + "rolling_eyes": "8.0", + "face_with_rolling_eyes": "8.0", + "smirk": "6.0", + "persevere": "6.0", + "disappointed_relieved": "6.0", + "open_mouth": "6.1", + "zipper_mouth": "8.0", + "zipper_mouth_face": "8.0", + "hushed": "6.1", + "sleepy": "6.0", + "tired_face": "6.0", + "sleeping": "6.1", + "relieved": "6.0", + "nerd": "8.0", + "nerd_face": "8.0", + "stuck_out_tongue": "6.1", + "stuck_out_tongue_winking_eye": "6.0", + "stuck_out_tongue_closed_eyes": "6.0", + "drooling_face": "9.0", + "drool": "9.0", + "unamused": "6.0", + "sweat": "6.0", + "pensive": "6.0", + "confused": "6.1", + "upside_down": "8.0", + "upside_down_face": "8.0", + "money_mouth": "8.0", + "money_mouth_face": "8.0", + "astonished": "6.0", + "frowning2": "1.1", + "white_frowning_face": "1.1", + "slight_frown": "7.0", + "slightly_frowning_face": "7.0", + "confounded": "6.0", + "disappointed": "6.0", + "worried": "6.1", + "triumph": "6.0", + "cry": "6.0", + "sob": "6.0", + "frowning": "6.1", + "anguished": "6.1", + "fearful": "6.0", + "weary": "6.0", + "grimacing": "6.1", + "cold_sweat": "6.0", + "scream": "6.0", + "flushed": "6.0", + "dizzy_face": "6.0", + "rage": "6.0", + "angry": "6.0", + "innocent": "6.0", + "cowboy": "9.0", + "face_with_cowboy_hat": "9.0", + "clown": "9.0", + "clown_face": "9.0", + "lying_face": "9.0", + "liar": "9.0", + "mask": "6.0", + "thermometer_face": "8.0", + "face_with_thermometer": "8.0", + "head_bandage": "8.0", + "face_with_head_bandage": "8.0", + "nauseated_face": "9.0", + "sick": "9.0", + "sneezing_face": "9.0", + "sneeze": "9.0", + "smiling_imp": "6.0", + "imp": "6.0", + "japanese_ogre": "6.0", + "japanese_goblin": "6.0", + "skull": "6.0", + "skeleton": "6.0", + "skull_crossbones": "1.1", + "skull_and_crossbones": "1.1", + "ghost": "6.0", + "alien": "6.0", + "space_invader": "6.0", + "robot": "8.0", + "robot_face": "8.0", + "poop": "6.0", + "shit": "6.0", + "hankey": "6.0", + "poo": "6.0", + "smiley_cat": "6.0", + "smile_cat": "6.0", + "joy_cat": "6.0", + "heart_eyes_cat": "6.0", + "smirk_cat": "6.0", + "kissing_cat": "6.0", + "scream_cat": "6.0", + "crying_cat_face": "6.0", + "pouting_cat": "6.0", + "see_no_evil": "6.0", + "hear_no_evil": "6.0", + "speak_no_evil": "6.0", + "boy": "6.0", + "boy_tone1": "8.0", + "boy_tone2": "8.0", + "boy_tone3": "8.0", + "boy_tone4": "8.0", + "boy_tone5": "8.0", + "girl": "6.0", + "girl_tone1": "8.0", + "girl_tone2": "8.0", + "girl_tone3": "8.0", + "girl_tone4": "8.0", + "girl_tone5": "8.0", + "man": "6.0", + "man_tone1": "8.0", + "man_tone2": "8.0", + "man_tone3": "8.0", + "man_tone4": "8.0", + "man_tone5": "8.0", + "woman": "6.0", + "woman_tone1": "8.0", + "woman_tone2": "8.0", + "woman_tone3": "8.0", + "woman_tone4": "8.0", + "woman_tone5": "8.0", + "older_man": "6.0", + "older_man_tone1": "8.0", + "older_man_tone2": "8.0", + "older_man_tone3": "8.0", + "older_man_tone4": "8.0", + "older_man_tone5": "8.0", + "older_woman": "6.0", + "grandma": "6.0", + "older_woman_tone1": "8.0", + "grandma_tone1": "8.0", + "older_woman_tone2": "8.0", + "grandma_tone2": "8.0", + "older_woman_tone3": "8.0", + "grandma_tone3": "8.0", + "older_woman_tone4": "8.0", + "grandma_tone4": "8.0", + "older_woman_tone5": "8.0", + "grandma_tone5": "8.0", + "baby": "6.0", + "baby_tone1": "8.0", + "baby_tone2": "8.0", + "baby_tone3": "8.0", + "baby_tone4": "8.0", + "baby_tone5": "8.0", + "angel": "6.0", + "angel_tone1": "8.0", + "angel_tone2": "8.0", + "angel_tone3": "8.0", + "angel_tone4": "8.0", + "angel_tone5": "8.0", + "cop": "6.0", + "cop_tone1": "8.0", + "cop_tone2": "8.0", + "cop_tone3": "8.0", + "cop_tone4": "8.0", + "cop_tone5": "8.0", + "spy": "7.0", + "sleuth_or_spy": "7.0", + "spy_tone1": "8.0", + "sleuth_or_spy_tone1": "8.0", + "spy_tone2": "8.0", + "sleuth_or_spy_tone2": "8.0", + "spy_tone3": "8.0", + "sleuth_or_spy_tone3": "8.0", + "spy_tone4": "8.0", + "sleuth_or_spy_tone4": "8.0", + "spy_tone5": "8.0", + "sleuth_or_spy_tone5": "8.0", + "guardsman": "6.0", + "guardsman_tone1": "8.0", + "guardsman_tone2": "8.0", + "guardsman_tone3": "8.0", + "guardsman_tone4": "8.0", + "guardsman_tone5": "8.0", + "construction_worker": "6.0", + "construction_worker_tone1": "8.0", + "construction_worker_tone2": "8.0", + "construction_worker_tone3": "8.0", + "construction_worker_tone4": "8.0", + "construction_worker_tone5": "8.0", + "man_with_turban": "6.0", + "man_with_turban_tone1": "8.0", + "man_with_turban_tone2": "8.0", + "man_with_turban_tone3": "8.0", + "man_with_turban_tone4": "8.0", + "man_with_turban_tone5": "8.0", + "person_with_blond_hair": "6.0", + "person_with_blond_hair_tone1": "8.0", + "person_with_blond_hair_tone2": "8.0", + "person_with_blond_hair_tone3": "8.0", + "person_with_blond_hair_tone4": "8.0", + "person_with_blond_hair_tone5": "8.0", + "santa": "6.0", + "santa_tone1": "8.0", + "santa_tone2": "8.0", + "santa_tone3": "8.0", + "santa_tone4": "8.0", + "santa_tone5": "8.0", + "mrs_claus": "9.0", + "mother_christmas": "9.0", + "mrs_claus_tone1": "9.0", + "mother_christmas_tone1": "9.0", + "mrs_claus_tone2": "9.0", + "mother_christmas_tone2": "9.0", + "mrs_claus_tone3": "9.0", + "mother_christmas_tone3": "9.0", + "mrs_claus_tone4": "9.0", + "mother_christmas_tone4": "9.0", + "mrs_claus_tone5": "9.0", + "mother_christmas_tone5": "9.0", + "princess": "6.0", + "princess_tone1": "8.0", + "princess_tone2": "8.0", + "princess_tone3": "8.0", + "princess_tone4": "8.0", + "princess_tone5": "8.0", + "prince": "9.0", + "prince_tone1": "9.0", + "prince_tone2": "9.0", + "prince_tone3": "9.0", + "prince_tone4": "9.0", + "prince_tone5": "9.0", + "bride_with_veil": "6.0", + "bride_with_veil_tone1": "8.0", + "bride_with_veil_tone2": "8.0", + "bride_with_veil_tone3": "8.0", + "bride_with_veil_tone4": "8.0", + "bride_with_veil_tone5": "8.0", + "man_in_tuxedo": "9.0", + "man_in_tuxedo_tone1": "9.0", + "tuxedo_tone1": "9.0", + "man_in_tuxedo_tone2": "9.0", + "tuxedo_tone2": "9.0", + "man_in_tuxedo_tone3": "9.0", + "tuxedo_tone3": "9.0", + "man_in_tuxedo_tone4": "9.0", + "tuxedo_tone4": "9.0", + "man_in_tuxedo_tone5": "9.0", + "tuxedo_tone5": "9.0", + "pregnant_woman": "9.0", + "expecting_woman": "9.0", + "pregnant_woman_tone1": "9.0", + "expecting_woman_tone1": "9.0", + "pregnant_woman_tone2": "9.0", + "expecting_woman_tone2": "9.0", + "pregnant_woman_tone3": "9.0", + "expecting_woman_tone3": "9.0", + "pregnant_woman_tone4": "9.0", + "expecting_woman_tone4": "9.0", + "pregnant_woman_tone5": "9.0", + "expecting_woman_tone5": "9.0", + "man_with_gua_pi_mao": "6.0", + "man_with_gua_pi_mao_tone1": "8.0", + "man_with_gua_pi_mao_tone2": "8.0", + "man_with_gua_pi_mao_tone3": "8.0", + "man_with_gua_pi_mao_tone4": "8.0", + "man_with_gua_pi_mao_tone5": "8.0", + "person_frowning": "6.0", + "person_frowning_tone1": "8.0", + "person_frowning_tone2": "8.0", + "person_frowning_tone3": "8.0", + "person_frowning_tone4": "8.0", + "person_frowning_tone5": "8.0", + "person_with_pouting_face": "6.0", + "person_with_pouting_face_tone1": "8.0", + "person_with_pouting_face_tone2": "8.0", + "person_with_pouting_face_tone3": "8.0", + "person_with_pouting_face_tone4": "8.0", + "person_with_pouting_face_tone5": "8.0", + "no_good": "6.0", + "no_good_tone1": "8.0", + "no_good_tone2": "8.0", + "no_good_tone3": "8.0", + "no_good_tone4": "8.0", + "no_good_tone5": "8.0", + "ok_woman": "6.0", + "ok_woman_tone1": "8.0", + "ok_woman_tone2": "8.0", + "ok_woman_tone3": "8.0", + "ok_woman_tone4": "8.0", + "ok_woman_tone5": "8.0", + "information_desk_person": "6.0", + "information_desk_person_tone1": "8.0", + "information_desk_person_tone2": "8.0", + "information_desk_person_tone3": "8.0", + "information_desk_person_tone4": "8.0", + "information_desk_person_tone5": "8.0", + "raising_hand": "6.0", + "raising_hand_tone1": "8.0", + "raising_hand_tone2": "8.0", + "raising_hand_tone3": "8.0", + "raising_hand_tone4": "8.0", + "raising_hand_tone5": "8.0", + "bow": "6.0", + "bow_tone1": "8.0", + "bow_tone2": "8.0", + "bow_tone3": "8.0", + "bow_tone4": "8.0", + "bow_tone5": "8.0", + "face_palm": "9.0", + "facepalm": "9.0", + "face_palm_tone1": "9.0", + "facepalm_tone1": "9.0", + "face_palm_tone2": "9.0", + "facepalm_tone2": "9.0", + "face_palm_tone3": "9.0", + "facepalm_tone3": "9.0", + "face_palm_tone4": "9.0", + "facepalm_tone4": "9.0", + "face_palm_tone5": "9.0", + "facepalm_tone5": "9.0", + "shrug": "9.0", + "shrug_tone1": "9.0", + "shrug_tone2": "9.0", + "shrug_tone3": "9.0", + "shrug_tone4": "9.0", + "shrug_tone5": "9.0", + "massage": "6.0", + "massage_tone1": "8.0", + "massage_tone2": "8.0", + "massage_tone3": "8.0", + "massage_tone4": "8.0", + "massage_tone5": "8.0", + "haircut": "6.0", + "haircut_tone1": "8.0", + "haircut_tone2": "8.0", + "haircut_tone3": "8.0", + "haircut_tone4": "8.0", + "haircut_tone5": "8.0", + "walking": "6.0", + "walking_tone1": "8.0", + "walking_tone2": "8.0", + "walking_tone3": "8.0", + "walking_tone4": "8.0", + "walking_tone5": "8.0", + "runner": "6.0", + "runner_tone1": "8.0", + "runner_tone2": "8.0", + "runner_tone3": "8.0", + "runner_tone4": "8.0", + "runner_tone5": "8.0", + "dancer": "6.0", + "dancer_tone1": "8.0", + "dancer_tone2": "8.0", + "dancer_tone3": "8.0", + "dancer_tone4": "8.0", + "dancer_tone5": "8.0", + "man_dancing": "9.0", + "male_dancer": "9.0", + "man_dancing_tone1": "9.0", + "male_dancer_tone1": "9.0", + "man_dancing_tone2": "9.0", + "male_dancer_tone2": "9.0", + "man_dancing_tone3": "9.0", + "male_dancer_tone3": "9.0", + "man_dancing_tone4": "9.0", + "male_dancer_tone4": "9.0", + "man_dancing_tone5": "9.0", + "male_dancer_tone5": "9.0", + "dancers": "6.0", + "levitate": "7.0", + "man_in_business_suit_levitating": "7.0", + "speaking_head": "7.0", + "speaking_head_in_silhouette": "7.0", + "bust_in_silhouette": "6.0", + "busts_in_silhouette": "6.0", + "fencer": "9.0", + "fencing": "9.0", + "horse_racing": "6.0", + "horse_racing_tone1": "8.0", + "horse_racing_tone2": "8.0", + "horse_racing_tone3": "8.0", + "horse_racing_tone4": "8.0", + "horse_racing_tone5": "8.0", + "skier": "5.2", + "snowboarder": "6.0", + "golfer": "7.0", + "surfer": "6.0", + "surfer_tone1": "8.0", + "surfer_tone2": "8.0", + "surfer_tone3": "8.0", + "surfer_tone4": "8.0", + "surfer_tone5": "8.0", + "rowboat": "6.0", + "rowboat_tone1": "8.0", + "rowboat_tone2": "8.0", + "rowboat_tone3": "8.0", + "rowboat_tone4": "8.0", + "rowboat_tone5": "8.0", + "swimmer": "6.0", + "swimmer_tone1": "8.0", + "swimmer_tone2": "8.0", + "swimmer_tone3": "8.0", + "swimmer_tone4": "8.0", + "swimmer_tone5": "8.0", + "basketball_player": "5.2", + "person_with_ball": "5.2", + "basketball_player_tone1": "8.0", + "person_with_ball_tone1": "8.0", + "basketball_player_tone2": "8.0", + "person_with_ball_tone2": "8.0", + "basketball_player_tone3": "8.0", + "person_with_ball_tone3": "8.0", + "basketball_player_tone4": "8.0", + "person_with_ball_tone4": "8.0", + "basketball_player_tone5": "8.0", + "person_with_ball_tone5": "8.0", + "lifter": "7.0", + "weight_lifter": "7.0", + "lifter_tone1": "8.0", + "weight_lifter_tone1": "8.0", + "lifter_tone2": "8.0", + "weight_lifter_tone2": "8.0", + "lifter_tone3": "8.0", + "weight_lifter_tone3": "8.0", + "lifter_tone4": "8.0", + "weight_lifter_tone4": "8.0", + "lifter_tone5": "8.0", + "weight_lifter_tone5": "8.0", + "bicyclist": "6.0", + "bicyclist_tone1": "8.0", + "bicyclist_tone2": "8.0", + "bicyclist_tone3": "8.0", + "bicyclist_tone4": "8.0", + "bicyclist_tone5": "8.0", + "mountain_bicyclist": "6.0", + "mountain_bicyclist_tone1": "8.0", + "mountain_bicyclist_tone2": "8.0", + "mountain_bicyclist_tone3": "8.0", + "mountain_bicyclist_tone4": "8.0", + "mountain_bicyclist_tone5": "8.0", + "race_car": "7.0", + "racing_car": "7.0", + "motorcycle": "7.0", + "racing_motorcycle": "7.0", + "cartwheel": "9.0", + "person_doing_cartwheel": "9.0", + "cartwheel_tone1": "9.0", + "person_doing_cartwheel_tone1": "9.0", + "cartwheel_tone2": "9.0", + "person_doing_cartwheel_tone2": "9.0", + "cartwheel_tone3": "9.0", + "person_doing_cartwheel_tone3": "9.0", + "cartwheel_tone4": "9.0", + "person_doing_cartwheel_tone4": "9.0", + "cartwheel_tone5": "9.0", + "person_doing_cartwheel_tone5": "9.0", + "wrestlers": "9.0", + "wrestling": "9.0", + "wrestlers_tone1": "9.0", + "wrestling_tone1": "9.0", + "wrestlers_tone2": "9.0", + "wrestling_tone2": "9.0", + "wrestlers_tone3": "9.0", + "wrestling_tone3": "9.0", + "wrestlers_tone4": "9.0", + "wrestling_tone4": "9.0", + "wrestlers_tone5": "9.0", + "wrestling_tone5": "9.0", + "water_polo": "9.0", + "water_polo_tone1": "9.0", + "water_polo_tone2": "9.0", + "water_polo_tone3": "9.0", + "water_polo_tone4": "9.0", + "water_polo_tone5": "9.0", + "handball": "9.0", + "handball_tone1": "9.0", + "handball_tone2": "9.0", + "handball_tone3": "9.0", + "handball_tone4": "9.0", + "handball_tone5": "9.0", + "juggling": "9.0", + "juggler": "9.0", + "juggling_tone1": "9.0", + "juggler_tone1": "9.0", + "juggling_tone2": "9.0", + "juggler_tone2": "9.0", + "juggling_tone3": "9.0", + "juggler_tone3": "9.0", + "juggling_tone4": "9.0", + "juggler_tone4": "9.0", + "juggling_tone5": "9.0", + "juggler_tone5": "9.0", + "couple": "6.0", + "two_men_holding_hands": "6.0", + "two_women_holding_hands": "6.0", + "couplekiss": "6.0", + "kiss_mm": "6.0", + "couplekiss_mm": "6.0", + "kiss_ww": "6.0", + "couplekiss_ww": "6.0", + "couple_with_heart": "6.0", + "couple_mm": "6.0", + "couple_with_heart_mm": "6.0", + "couple_ww": "6.0", + "couple_with_heart_ww": "6.0", + "family": "6.0", + "family_mwg": "6.0", + "family_mwgb": "6.0", + "family_mwbb": "6.0", + "family_mwgg": "6.0", + "family_mmb": "6.0", + "family_mmg": "6.0", + "family_mmgb": "6.0", + "family_mmbb": "6.0", + "family_mmgg": "6.0", + "family_wwb": "6.0", + "family_wwg": "6.0", + "family_wwgb": "6.0", + "family_wwbb": "6.0", + "family_wwgg": "6.0", + "tone1": "8.0", + "tone2": "8.0", + "tone3": "8.0", + "tone4": "8.0", + "tone5": "8.0", + "muscle": "6.0", + "muscle_tone1": "8.0", + "muscle_tone2": "8.0", + "muscle_tone3": "8.0", + "muscle_tone4": "8.0", + "muscle_tone5": "8.0", + "selfie": "9.0", + "selfie_tone1": "9.0", + "selfie_tone2": "9.0", + "selfie_tone3": "9.0", + "selfie_tone4": "9.0", + "selfie_tone5": "9.0", + "point_left": "6.0", + "point_left_tone1": "8.0", + "point_left_tone2": "8.0", + "point_left_tone3": "8.0", + "point_left_tone4": "8.0", + "point_left_tone5": "8.0", + "point_right": "6.0", + "point_right_tone1": "8.0", + "point_right_tone2": "8.0", + "point_right_tone3": "8.0", + "point_right_tone4": "8.0", + "point_right_tone5": "8.0", + "point_up": "1.1", + "point_up_tone1": "8.0", + "point_up_tone2": "8.0", + "point_up_tone3": "8.0", + "point_up_tone4": "8.0", + "point_up_tone5": "8.0", + "point_up_2": "6.0", + "point_up_2_tone1": "8.0", + "point_up_2_tone2": "8.0", + "point_up_2_tone3": "8.0", + "point_up_2_tone4": "8.0", + "point_up_2_tone5": "8.0", + "middle_finger": "7.0", + "reversed_hand_with_middle_finger_extended": "7.0", + "middle_finger_tone1": "8.0", + "reversed_hand_with_middle_finger_extended_tone1": "8.0", + "middle_finger_tone2": "8.0", + "reversed_hand_with_middle_finger_extended_tone2": "8.0", + "middle_finger_tone3": "8.0", + "reversed_hand_with_middle_finger_extended_tone3": "8.0", + "middle_finger_tone4": "8.0", + "reversed_hand_with_middle_finger_extended_tone4": "8.0", + "middle_finger_tone5": "8.0", + "reversed_hand_with_middle_finger_extended_tone5": "8.0", + "point_down": "6.0", + "point_down_tone1": "8.0", + "point_down_tone2": "8.0", + "point_down_tone3": "8.0", + "point_down_tone4": "8.0", + "point_down_tone5": "8.0", + "v": "1.1", + "v_tone1": "8.0", + "v_tone2": "8.0", + "v_tone3": "8.0", + "v_tone4": "8.0", + "v_tone5": "8.0", + "fingers_crossed": "9.0", + "hand_with_index_and_middle_finger_crossed": "9.0", + "fingers_crossed_tone1": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone1": "9.0", + "fingers_crossed_tone2": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone2": "9.0", + "fingers_crossed_tone3": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone3": "9.0", + "fingers_crossed_tone4": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone4": "9.0", + "fingers_crossed_tone5": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone5": "9.0", + "vulcan": "7.0", + "raised_hand_with_part_between_middle_and_ring_fingers": "7.0", + "vulcan_tone1": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone1": "8.0", + "vulcan_tone2": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone2": "8.0", + "vulcan_tone3": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone3": "8.0", + "vulcan_tone4": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone4": "8.0", + "vulcan_tone5": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone5": "8.0", + "metal": "8.0", + "sign_of_the_horns": "8.0", + "metal_tone1": "8.0", + "sign_of_the_horns_tone1": "8.0", + "metal_tone2": "8.0", + "sign_of_the_horns_tone2": "8.0", + "metal_tone3": "8.0", + "sign_of_the_horns_tone3": "8.0", + "metal_tone4": "8.0", + "sign_of_the_horns_tone4": "8.0", + "metal_tone5": "8.0", + "sign_of_the_horns_tone5": "8.0", + "call_me": "9.0", + "call_me_hand": "9.0", + "call_me_tone1": "9.0", + "call_me_hand_tone1": "9.0", + "call_me_tone2": "9.0", + "call_me_hand_tone2": "9.0", + "call_me_tone3": "9.0", + "call_me_hand_tone3": "9.0", + "call_me_tone4": "9.0", + "call_me_hand_tone4": "9.0", + "call_me_tone5": "9.0", + "call_me_hand_tone5": "9.0", + "hand_splayed": "7.0", + "raised_hand_with_fingers_splayed": "7.0", + "hand_splayed_tone1": "8.0", + "raised_hand_with_fingers_splayed_tone1": "8.0", + "hand_splayed_tone2": "8.0", + "raised_hand_with_fingers_splayed_tone2": "8.0", + "hand_splayed_tone3": "8.0", + "raised_hand_with_fingers_splayed_tone3": "8.0", + "hand_splayed_tone4": "8.0", + "raised_hand_with_fingers_splayed_tone4": "8.0", + "hand_splayed_tone5": "8.0", + "raised_hand_with_fingers_splayed_tone5": "8.0", + "raised_hand": "6.0", + "raised_hand_tone1": "8.0", + "raised_hand_tone2": "8.0", + "raised_hand_tone3": "8.0", + "raised_hand_tone4": "8.0", + "raised_hand_tone5": "8.0", + "ok_hand": "6.0", + "ok_hand_tone1": "8.0", + "ok_hand_tone2": "8.0", + "ok_hand_tone3": "8.0", + "ok_hand_tone4": "8.0", + "ok_hand_tone5": "8.0", + "thumbsup": "6.0", + "+1": "6.0", + "thumbup": "6.0", + "thumbsup_tone1": "8.0", + "+1_tone1": "8.0", + "thumbup_tone1": "8.0", + "thumbsup_tone2": "8.0", + "+1_tone2": "8.0", + "thumbup_tone2": "8.0", + "thumbsup_tone3": "8.0", + "+1_tone3": "8.0", + "thumbup_tone3": "8.0", + "thumbsup_tone4": "8.0", + "+1_tone4": "8.0", + "thumbup_tone4": "8.0", + "thumbsup_tone5": "8.0", + "+1_tone5": "8.0", + "thumbup_tone5": "8.0", + "thumbsdown": "6.0", + "-1": "6.0", + "thumbdown": "6.0", + "thumbsdown_tone1": "8.0", + "-1_tone1": "8.0", + "thumbdown_tone1": "8.0", + "thumbsdown_tone2": "8.0", + "-1_tone2": "8.0", + "thumbdown_tone2": "8.0", + "thumbsdown_tone3": "8.0", + "-1_tone3": "8.0", + "thumbdown_tone3": "8.0", + "thumbsdown_tone4": "8.0", + "-1_tone4": "8.0", + "thumbdown_tone4": "8.0", + "thumbsdown_tone5": "8.0", + "-1_tone5": "8.0", + "thumbdown_tone5": "8.0", + "fist": "6.0", + "fist_tone1": "8.0", + "fist_tone2": "8.0", + "fist_tone3": "8.0", + "fist_tone4": "8.0", + "fist_tone5": "8.0", + "punch": "6.0", + "punch_tone1": "8.0", + "punch_tone2": "8.0", + "punch_tone3": "8.0", + "punch_tone4": "8.0", + "punch_tone5": "8.0", + "left_facing_fist": "9.0", + "left_fist": "9.0", + "left_facing_fist_tone1": "9.0", + "left_fist_tone1": "9.0", + "left_facing_fist_tone2": "9.0", + "left_fist_tone2": "9.0", + "left_facing_fist_tone3": "9.0", + "left_fist_tone3": "9.0", + "left_facing_fist_tone4": "9.0", + "left_fist_tone4": "9.0", + "left_facing_fist_tone5": "9.0", + "left_fist_tone5": "9.0", + "right_facing_fist": "9.0", + "right_fist": "9.0", + "right_facing_fist_tone1": "9.0", + "right_fist_tone1": "9.0", + "right_facing_fist_tone2": "9.0", + "right_fist_tone2": "9.0", + "right_facing_fist_tone3": "9.0", + "right_fist_tone3": "9.0", + "right_facing_fist_tone4": "9.0", + "right_fist_tone4": "9.0", + "right_facing_fist_tone5": "9.0", + "right_fist_tone5": "9.0", + "raised_back_of_hand": "9.0", + "back_of_hand": "9.0", + "raised_back_of_hand_tone1": "9.0", + "back_of_hand_tone1": "9.0", + "raised_back_of_hand_tone2": "9.0", + "back_of_hand_tone2": "9.0", + "raised_back_of_hand_tone3": "9.0", + "back_of_hand_tone3": "9.0", + "raised_back_of_hand_tone4": "9.0", + "back_of_hand_tone4": "9.0", + "raised_back_of_hand_tone5": "9.0", + "back_of_hand_tone5": "9.0", + "wave": "6.0", + "wave_tone1": "8.0", + "wave_tone2": "8.0", + "wave_tone3": "8.0", + "wave_tone4": "8.0", + "wave_tone5": "8.0", + "clap": "6.0", + "clap_tone1": "8.0", + "clap_tone2": "8.0", + "clap_tone3": "8.0", + "clap_tone4": "8.0", + "clap_tone5": "8.0", + "writing_hand": "1.1", + "writing_hand_tone1": "8.0", + "writing_hand_tone2": "8.0", + "writing_hand_tone3": "8.0", + "writing_hand_tone4": "8.0", + "writing_hand_tone5": "8.0", + "open_hands": "6.0", + "open_hands_tone1": "8.0", + "open_hands_tone2": "8.0", + "open_hands_tone3": "8.0", + "open_hands_tone4": "8.0", + "open_hands_tone5": "8.0", + "raised_hands": "6.0", + "raised_hands_tone1": "8.0", + "raised_hands_tone2": "8.0", + "raised_hands_tone3": "8.0", + "raised_hands_tone4": "8.0", + "raised_hands_tone5": "8.0", + "pray": "6.0", + "pray_tone1": "8.0", + "pray_tone2": "8.0", + "pray_tone3": "8.0", + "pray_tone4": "8.0", + "pray_tone5": "8.0", + "handshake": "9.0", + "shaking_hands": "9.0", + "handshake_tone1": "9.0", + "shaking_hands_tone1": "9.0", + "handshake_tone2": "9.0", + "shaking_hands_tone2": "9.0", + "handshake_tone3": "9.0", + "shaking_hands_tone3": "9.0", + "handshake_tone4": "9.0", + "shaking_hands_tone4": "9.0", + "handshake_tone5": "9.0", + "shaking_hands_tone5": "9.0", + "nail_care": "6.0", + "nail_care_tone1": "8.0", + "nail_care_tone2": "8.0", + "nail_care_tone3": "8.0", + "nail_care_tone4": "8.0", + "nail_care_tone5": "8.0", + "ear": "6.0", + "ear_tone1": "8.0", + "ear_tone2": "8.0", + "ear_tone3": "8.0", + "ear_tone4": "8.0", + "ear_tone5": "8.0", + "nose": "6.0", + "nose_tone1": "8.0", + "nose_tone2": "8.0", + "nose_tone3": "8.0", + "nose_tone4": "8.0", + "nose_tone5": "8.0", + "footprints": "6.0", + "eyes": "6.0", + "eye": "7.0", + "eye_in_speech_bubble": "7.0", + "tongue": "6.0", + "lips": "6.0", + "kiss": "6.0", + "cupid": "6.0", + "heart": "1.1", + "heartbeat": "6.0", + "broken_heart": "6.0", + "two_hearts": "6.0", + "sparkling_heart": "6.0", + "heartpulse": "6.0", + "blue_heart": "6.0", + "green_heart": "6.0", + "yellow_heart": "6.0", + "purple_heart": "6.0", + "black_heart": "9.0", + "gift_heart": "6.0", + "revolving_hearts": "6.0", + "heart_decoration": "6.0", + "heart_exclamation": "1.1", + "heavy_heart_exclamation_mark_ornament": "1.1", + "love_letter": "6.0", + "zzz": "6.0", + "anger": "6.0", + "bomb": "6.0", + "boom": "6.0", + "sweat_drops": "6.0", + "dash": "6.0", + "dizzy": "6.0", + "speech_balloon": "6.0", + "speech_left": "7.0", + "left_speech_bubble": "7.0", + "anger_right": "7.0", + "right_anger_bubble": "7.0", + "thought_balloon": "6.0", + "hole": "7.0", + "eyeglasses": "6.0", + "dark_sunglasses": "7.0", + "necktie": "6.0", + "shirt": "6.0", + "jeans": "6.0", + "dress": "6.0", + "kimono": "6.0", + "bikini": "6.0", + "womans_clothes": "6.0", + "purse": "6.0", + "handbag": "6.0", + "pouch": "6.0", + "shopping_bags": "7.0", + "school_satchel": "6.0", + "mans_shoe": "6.0", + "athletic_shoe": "6.0", + "high_heel": "6.0", + "sandal": "6.0", + "boot": "6.0", + "crown": "6.0", + "womans_hat": "6.0", + "tophat": "6.0", + "mortar_board": "6.0", + "helmet_with_cross": "5.2", + "helmet_with_white_cross": "5.2", + "prayer_beads": "8.0", + "lipstick": "6.0", + "ring": "6.0", + "gem": "6.0", + "monkey_face": "6.0", + "monkey": "6.0", + "gorilla": "9.0", + "dog": "6.0", + "dog2": "6.0", + "poodle": "6.0", + "wolf": "6.0", + "fox": "9.0", + "fox_face": "9.0", + "cat": "6.0", + "cat2": "6.0", + "lion_face": "8.0", + "lion": "8.0", + "tiger": "6.0", + "tiger2": "6.0", + "leopard": "6.0", + "horse": "6.0", + "racehorse": "6.0", + "deer": "9.0", + "unicorn": "8.0", + "unicorn_face": "8.0", + "cow": "6.0", + "ox": "6.0", + "water_buffalo": "6.0", + "cow2": "6.0", + "pig": "6.0", + "pig2": "6.0", + "boar": "6.0", + "pig_nose": "6.0", + "ram": "6.0", + "sheep": "6.0", + "goat": "6.0", + "dromedary_camel": "6.0", + "camel": "6.0", + "elephant": "6.0", + "rhino": "9.0", + "rhinoceros": "9.0", + "mouse": "6.0", + "mouse2": "6.0", + "rat": "6.0", + "hamster": "6.0", + "rabbit": "6.0", + "rabbit2": "6.0", + "chipmunk": "7.0", + "bat": "9.0", + "bear": "6.0", + "koala": "6.0", + "panda_face": "6.0", + "feet": "6.0", + "paw_prints": "6.0", + "turkey": "8.0", + "chicken": "6.0", + "rooster": "6.0", + "hatching_chick": "6.0", + "baby_chick": "6.0", + "hatched_chick": "6.0", + "bird": "6.0", + "penguin": "6.0", + "dove": "7.0", + "dove_of_peace": "7.0", + "eagle": "9.0", + "duck": "9.0", + "owl": "9.0", + "frog": "6.0", + "crocodile": "6.0", + "turtle": "6.0", + "lizard": "9.0", + "snake": "6.0", + "dragon_face": "6.0", + "dragon": "6.0", + "whale": "6.0", + "whale2": "6.0", + "dolphin": "6.0", + "fish": "6.0", + "tropical_fish": "6.0", + "blowfish": "6.0", + "shark": "9.0", + "octopus": "6.0", + "shell": "6.0", + "crab": "8.0", + "shrimp": "9.0", + "squid": "9.0", + "butterfly": "9.0", + "snail": "6.0", + "bug": "6.0", + "ant": "6.0", + "bee": "6.0", + "beetle": "6.0", + "spider": "7.0", + "spider_web": "7.0", + "scorpion": "8.0", + "bouquet": "6.0", + "cherry_blossom": "6.0", + "white_flower": "6.0", + "rosette": "7.0", + "rose": "6.0", + "wilted_rose": "9.0", + "wilted_flower": "9.0", + "hibiscus": "6.0", + "sunflower": "6.0", + "blossom": "6.0", + "tulip": "6.0", + "seedling": "6.0", + "evergreen_tree": "6.0", + "deciduous_tree": "6.0", + "palm_tree": "6.0", + "cactus": "6.0", + "ear_of_rice": "6.0", + "herb": "6.0", + "shamrock": "4.1", + "four_leaf_clover": "6.0", + "maple_leaf": "6.0", + "fallen_leaf": "6.0", + "leaves": "6.0", + "grapes": "6.0", + "melon": "6.0", + "watermelon": "6.0", + "tangerine": "6.0", + "lemon": "6.0", + "banana": "6.0", + "pineapple": "6.0", + "apple": "6.0", + "green_apple": "6.0", + "pear": "6.0", + "peach": "6.0", + "cherries": "6.0", + "strawberry": "6.0", + "kiwi": "9.0", + "kiwifruit": "9.0", + "tomato": "6.0", + "avocado": "9.0", + "eggplant": "6.0", + "potato": "9.0", + "carrot": "9.0", + "corn": "6.0", + "hot_pepper": "7.0", + "cucumber": "9.0", + "mushroom": "6.0", + "peanuts": "9.0", + "shelled_peanut": "9.0", + "chestnut": "6.0", + "bread": "6.0", + "croissant": "9.0", + "french_bread": "9.0", + "baguette_bread": "9.0", + "pancakes": "9.0", + "cheese": "8.0", + "cheese_wedge": "8.0", + "meat_on_bone": "6.0", + "poultry_leg": "6.0", + "bacon": "9.0", + "hamburger": "6.0", + "fries": "6.0", + "pizza": "6.0", + "hotdog": "8.0", + "hot_dog": "8.0", + "taco": "8.0", + "burrito": "8.0", + "stuffed_flatbread": "9.0", + "stuffed_pita": "9.0", + "egg": "9.0", + "cooking": "6.0", + "shallow_pan_of_food": "9.0", + "paella": "9.0", + "stew": "6.0", + "salad": "9.0", + "green_salad": "9.0", + "popcorn": "8.0", + "bento": "6.0", + "rice_cracker": "6.0", + "rice_ball": "6.0", + "rice": "6.0", + "curry": "6.0", + "ramen": "6.0", + "spaghetti": "6.0", + "sweet_potato": "6.0", + "oden": "6.0", + "sushi": "6.0", + "fried_shrimp": "6.0", + "fish_cake": "6.0", + "dango": "6.0", + "icecream": "6.0", + "shaved_ice": "6.0", + "ice_cream": "6.0", + "doughnut": "6.0", + "cookie": "6.0", + "birthday": "6.0", + "cake": "6.0", + "chocolate_bar": "6.0", + "candy": "6.0", + "lollipop": "6.0", + "custard": "6.0", + "pudding": "6.0", + "flan": "6.0", + "honey_pot": "6.0", + "baby_bottle": "6.0", + "milk": "9.0", + "glass_of_milk": "9.0", + "coffee": "4.0", + "tea": "6.0", + "sake": "6.0", + "champagne": "8.0", + "bottle_with_popping_cork": "8.0", + "wine_glass": "6.0", + "cocktail": "6.0", + "tropical_drink": "6.0", + "beer": "6.0", + "beers": "6.0", + "champagne_glass": "9.0", + "clinking_glass": "9.0", + "tumbler_glass": "9.0", + "whisky": "9.0", + "fork_knife_plate": "7.0", + "fork_and_knife_with_plate": "7.0", + "fork_and_knife": "6.0", + "spoon": "9.0", + "knife": "6.0", + "amphora": "8.0", + "earth_africa": "6.0", + "earth_americas": "6.0", + "earth_asia": "6.0", + "globe_with_meridians": "6.0", + "map": "7.0", + "world_map": "7.0", + "japan": "6.0", + "mountain_snow": "7.0", + "snow_capped_mountain": "7.0", + "mountain": "5.2", + "volcano": "6.0", + "mount_fuji": "6.0", + "camping": "7.0", + "beach": "7.0", + "beach_with_umbrella": "7.0", + "desert": "7.0", + "island": "7.0", + "desert_island": "7.0", + "park": "7.0", + "national_park": "7.0", + "stadium": "7.0", + "classical_building": "7.0", + "construction_site": "7.0", + "building_construction": "7.0", + "homes": "7.0", + "house_buildings": "7.0", + "cityscape": "7.0", + "house_abandoned": "7.0", + "derelict_house_building": "7.0", + "house": "6.0", + "house_with_garden": "6.0", + "office": "6.0", + "post_office": "6.0", + "european_post_office": "6.0", + "hospital": "6.0", + "bank": "6.0", + "hotel": "6.0", + "love_hotel": "6.0", + "convenience_store": "6.0", + "school": "6.0", + "department_store": "6.0", + "factory": "6.0", + "japanese_castle": "6.0", + "european_castle": "6.0", + "wedding": "6.0", + "tokyo_tower": "6.0", + "statue_of_liberty": "6.0", + "church": "5.2", + "mosque": "8.0", + "synagogue": "8.0", + "shinto_shrine": "5.2", + "kaaba": "8.0", + "fountain": "5.2", + "tent": "5.2", + "foggy": "6.0", + "night_with_stars": "6.0", + "sunrise_over_mountains": "6.0", + "sunrise": "6.0", + "city_dusk": "6.0", + "city_sunset": "6.0", + "city_sunrise": "6.0", + "bridge_at_night": "6.0", + "hotsprings": "1.1", + "milky_way": "6.0", + "carousel_horse": "6.0", + "ferris_wheel": "6.0", + "roller_coaster": "6.0", + "barber": "6.0", + "circus_tent": "6.0", + "performing_arts": "6.0", + "frame_photo": "7.0", + "frame_with_picture": "7.0", + "art": "6.0", + "slot_machine": "6.0", + "steam_locomotive": "6.0", + "railway_car": "6.0", + "bullettrain_side": "6.0", + "bullettrain_front": "6.0", + "train2": "6.0", + "metro": "6.0", + "light_rail": "6.0", + "station": "6.0", + "tram": "6.0", + "monorail": "6.0", + "mountain_railway": "6.0", + "train": "6.0", + "bus": "6.0", + "oncoming_bus": "6.0", + "trolleybus": "6.0", + "minibus": "6.0", + "ambulance": "6.0", + "fire_engine": "6.0", + "police_car": "6.0", + "oncoming_police_car": "6.0", + "taxi": "6.0", + "oncoming_taxi": "6.0", + "red_car": "6.0", + "oncoming_automobile": "6.0", + "blue_car": "6.0", + "truck": "6.0", + "articulated_lorry": "6.0", + "tractor": "6.0", + "bike": "6.0", + "scooter": "9.0", + "motor_scooter": "9.0", + "motorbike": "9.0", + "busstop": "6.0", + "motorway": "7.0", + "railway_track": "7.0", + "railroad_track": "7.0", + "fuelpump": "5.2", + "rotating_light": "6.0", + "traffic_light": "6.0", + "vertical_traffic_light": "6.0", + "construction": "6.0", + "octagonal_sign": "9.0", + "stop_sign": "9.0", + "anchor": "4.1", + "sailboat": "5.2", + "canoe": "9.0", + "kayak": "9.0", + "speedboat": "6.0", + "cruise_ship": "7.0", + "passenger_ship": "7.0", + "ferry": "5.2", + "motorboat": "7.0", + "ship": "6.0", + "airplane": "1.1", + "airplane_small": "7.0", + "small_airplane": "7.0", + "airplane_departure": "7.0", + "airplane_arriving": "7.0", + "seat": "6.0", + "helicopter": "6.0", + "suspension_railway": "6.0", + "mountain_cableway": "6.0", + "aerial_tramway": "6.0", + "rocket": "6.0", + "satellite_orbital": "7.0", + "bellhop": "7.0", + "bellhop_bell": "7.0", + "door": "6.0", + "sleeping_accommodation": "7.0", + "bed": "7.0", + "couch": "7.0", + "couch_and_lamp": "7.0", + "toilet": "6.0", + "shower": "6.0", + "bath": "6.0", + "bath_tone1": "8.0", + "bath_tone2": "8.0", + "bath_tone3": "8.0", + "bath_tone4": "8.0", + "bath_tone5": "8.0", + "bathtub": "6.0", + "hourglass": "1.1", + "hourglass_flowing_sand": "6.0", + "watch": "1.1", + "alarm_clock": "6.0", + "stopwatch": "6.0", + "timer": "6.0", + "timer_clock": "6.0", + "clock": "7.0", + "mantlepiece_clock": "7.0", + "clock12": "6.0", + "clock1230": "6.0", + "clock1": "6.0", + "clock130": "6.0", + "clock2": "6.0", + "clock230": "6.0", + "clock3": "6.0", + "clock330": "6.0", + "clock4": "6.0", + "clock430": "6.0", + "clock5": "6.0", + "clock530": "6.0", + "clock6": "6.0", + "clock630": "6.0", + "clock7": "6.0", + "clock730": "6.0", + "clock8": "6.0", + "clock830": "6.0", + "clock9": "6.0", + "clock930": "6.0", + "clock10": "6.0", + "clock1030": "6.0", + "clock11": "6.0", + "clock1130": "6.0", + "new_moon": "6.0", + "waxing_crescent_moon": "6.0", + "first_quarter_moon": "6.0", + "waxing_gibbous_moon": "6.0", + "full_moon": "6.0", + "waning_gibbous_moon": "6.0", + "last_quarter_moon": "6.0", + "waning_crescent_moon": "6.0", + "crescent_moon": "6.0", + "new_moon_with_face": "6.0", + "first_quarter_moon_with_face": "6.0", + "last_quarter_moon_with_face": "6.0", + "thermometer": "7.0", + "sunny": "1.1", + "full_moon_with_face": "6.0", + "sun_with_face": "6.0", + "star": "5.1", + "star2": "6.0", + "stars": "6.0", + "cloud": "1.1", + "partly_sunny": "5.2", + "thunder_cloud_rain": "5.2", + "thunder_cloud_and_rain": "5.2", + "white_sun_small_cloud": "7.0", + "white_sun_with_small_cloud": "7.0", + "white_sun_cloud": "7.0", + "white_sun_behind_cloud": "7.0", + "white_sun_rain_cloud": "7.0", + "white_sun_behind_cloud_with_rain": "7.0", + "cloud_rain": "7.0", + "cloud_with_rain": "7.0", + "cloud_snow": "7.0", + "cloud_with_snow": "7.0", + "cloud_lightning": "7.0", + "cloud_with_lightning": "7.0", + "cloud_tornado": "7.0", + "cloud_with_tornado": "7.0", + "fog": "7.0", + "wind_blowing_face": "7.0", + "cyclone": "6.0", + "rainbow": "6.0", + "closed_umbrella": "6.0", + "umbrella2": "1.1", + "umbrella": "4.0", + "beach_umbrella": "5.2", + "umbrella_on_ground": "5.2", + "zap": "4.0", + "snowflake": "1.1", + "snowman2": "1.1", + "snowman": "5.2", + "comet": "1.1", + "fire": "6.0", + "flame": "6.0", + "droplet": "6.0", + "ocean": "6.0", + "jack_o_lantern": "6.0", + "christmas_tree": "6.0", + "fireworks": "6.0", + "sparkler": "6.0", + "sparkles": "6.0", + "balloon": "6.0", + "tada": "6.0", + "confetti_ball": "6.0", + "tanabata_tree": "6.0", + "bamboo": "6.0", + "dolls": "6.0", + "flags": "6.0", + "wind_chime": "6.0", + "rice_scene": "6.0", + "ribbon": "6.0", + "gift": "6.0", + "reminder_ribbon": "7.0", + "tickets": "7.0", + "admission_tickets": "7.0", + "ticket": "6.0", + "military_medal": "7.0", + "trophy": "6.0", + "medal": "7.0", + "sports_medal": "7.0", + "first_place": "9.0", + "first_place_medal": "9.0", + "second_place": "9.0", + "second_place_medal": "9.0", + "third_place": "9.0", + "third_place_medal": "9.0", + "soccer": "5.2", + "baseball": "5.2", + "basketball": "6.0", + "volleyball": "8.0", + "football": "6.0", + "rugby_football": "6.0", + "tennis": "6.0", + "8ball": "6.0", + "bowling": "6.0", + "cricket": "8.0", + "cricket_bat_ball": "8.0", + "field_hockey": "8.0", + "hockey": "8.0", + "ping_pong": "8.0", + "table_tennis": "8.0", + "badminton": "8.0", + "boxing_glove": "9.0", + "boxing_gloves": "9.0", + "martial_arts_uniform": "9.0", + "karate_uniform": "9.0", + "goal": "9.0", + "goal_net": "9.0", + "dart": "6.0", + "golf": "5.2", + "ice_skate": "5.2", + "fishing_pole_and_fish": "6.0", + "running_shirt_with_sash": "6.0", + "ski": "6.0", + "video_game": "6.0", + "joystick": "7.0", + "game_die": "6.0", + "spades": "1.1", + "hearts": "1.1", + "diamonds": "1.1", + "clubs": "1.1", + "black_joker": "6.0", + "mahjong": "5.1", + "flower_playing_cards": "6.0", + "mute": "6.0", + "speaker": "6.0", + "sound": "6.0", + "loud_sound": "6.0", + "loudspeaker": "6.0", + "mega": "6.0", + "postal_horn": "6.0", + "bell": "6.0", + "no_bell": "6.0", + "musical_score": "6.0", + "musical_note": "6.0", + "notes": "6.0", + "microphone2": "7.0", + "studio_microphone": "7.0", + "level_slider": "7.0", + "control_knobs": "7.0", + "microphone": "6.0", + "headphones": "6.0", + "radio": "6.0", + "saxophone": "6.0", + "guitar": "6.0", + "musical_keyboard": "6.0", + "trumpet": "6.0", + "violin": "6.0", + "drum": "9.0", + "drum_with_drumsticks": "9.0", + "iphone": "6.0", + "calling": "6.0", + "telephone": "1.1", + "telephone_receiver": "6.0", + "pager": "6.0", + "fax": "6.0", + "battery": "6.0", + "electric_plug": "6.0", + "computer": "6.0", + "desktop": "7.0", + "desktop_computer": "7.0", + "printer": "7.0", + "keyboard": "1.1", + "mouse_three_button": "7.0", + "three_button_mouse": "7.0", + "trackball": "7.0", + "minidisc": "6.0", + "floppy_disk": "6.0", + "cd": "6.0", + "dvd": "6.0", + "movie_camera": "6.0", + "film_frames": "7.0", + "projector": "7.0", + "film_projector": "7.0", + "clapper": "6.0", + "tv": "6.0", + "camera": "6.0", + "camera_with_flash": "7.0", + "video_camera": "6.0", + "vhs": "6.0", + "mag": "6.0", + "mag_right": "6.0", + "microscope": "6.0", + "telescope": "6.0", + "satellite": "6.0", + "candle": "7.0", + "bulb": "6.0", + "flashlight": "6.0", + "izakaya_lantern": "6.0", + "notebook_with_decorative_cover": "6.0", + "closed_book": "6.0", + "book": "6.0", + "green_book": "6.0", + "blue_book": "6.0", + "orange_book": "6.0", + "books": "6.0", + "notebook": "6.0", + "ledger": "6.0", + "page_with_curl": "6.0", + "scroll": "6.0", + "page_facing_up": "6.0", + "newspaper": "6.0", + "newspaper2": "7.0", + "rolled_up_newspaper": "7.0", + "bookmark_tabs": "6.0", + "bookmark": "6.0", + "label": "7.0", + "moneybag": "6.0", + "yen": "6.0", + "dollar": "6.0", + "euro": "6.0", + "pound": "6.0", + "money_with_wings": "6.0", + "credit_card": "6.0", + "chart": "6.0", + "currency_exchange": "6.0", + "heavy_dollar_sign": "6.0", + "envelope": "1.1", + "e-mail": "6.0", + "email": "6.0", + "incoming_envelope": "6.0", + "envelope_with_arrow": "6.0", + "outbox_tray": "6.0", + "inbox_tray": "6.0", + "package": "6.0", + "mailbox": "6.0", + "mailbox_closed": "6.0", + "mailbox_with_mail": "6.0", + "mailbox_with_no_mail": "6.0", + "postbox": "6.0", + "ballot_box": "7.0", + "ballot_box_with_ballot": "7.0", + "pencil2": "1.1", + "black_nib": "1.1", + "pen_fountain": "7.0", + "lower_left_fountain_pen": "7.0", + "pen_ballpoint": "7.0", + "lower_left_ballpoint_pen": "7.0", + "paintbrush": "7.0", + "lower_left_paintbrush": "7.0", + "crayon": "7.0", + "lower_left_crayon": "7.0", + "pencil": "6.0", + "briefcase": "6.0", + "file_folder": "6.0", + "open_file_folder": "6.0", + "dividers": "7.0", + "card_index_dividers": "7.0", + "date": "6.0", + "calendar": "6.0", + "notepad_spiral": "7.0", + "spiral_note_pad": "7.0", + "calendar_spiral": "7.0", + "spiral_calendar_pad": "7.0", + "card_index": "6.0", + "chart_with_upwards_trend": "6.0", + "chart_with_downwards_trend": "6.0", + "bar_chart": "6.0", + "clipboard": "6.0", + "pushpin": "6.0", + "round_pushpin": "6.0", + "paperclip": "6.0", + "paperclips": "7.0", + "linked_paperclips": "7.0", + "straight_ruler": "6.0", + "triangular_ruler": "6.0", + "scissors": "1.1", + "card_box": "7.0", + "card_file_box": "7.0", + "file_cabinet": "7.0", + "wastebasket": "7.0", + "lock": "6.0", + "unlock": "6.0", + "lock_with_ink_pen": "6.0", + "closed_lock_with_key": "6.0", + "key": "6.0", + "key2": "7.0", + "old_key": "7.0", + "hammer": "6.0", + "pick": "5.2", + "hammer_pick": "4.1", + "hammer_and_pick": "4.1", + "tools": "7.0", + "hammer_and_wrench": "7.0", + "dagger": "7.0", + "dagger_knife": "7.0", + "crossed_swords": "4.1", + "gun": "6.0", + "bow_and_arrow": "8.0", + "archery": "8.0", + "shield": "7.0", + "wrench": "6.0", + "nut_and_bolt": "6.0", + "gear": "4.1", + "compression": "7.0", + "alembic": "4.1", + "scales": "4.1", + "link": "6.0", + "chains": "5.2", + "syringe": "6.0", + "pill": "6.0", + "smoking": "6.0", + "coffin": "4.1", + "urn": "4.1", + "funeral_urn": "4.1", + "moyai": "6.0", + "oil": "7.0", + "oil_drum": "7.0", + "crystal_ball": "6.0", + "shopping_cart": "9.0", + "shopping_trolley": "9.0", + "atm": "6.0", + "put_litter_in_its_place": "6.0", + "potable_water": "6.0", + "wheelchair": "4.1", + "mens": "6.0", + "womens": "6.0", + "restroom": "6.0", + "baby_symbol": "6.0", + "wc": "6.0", + "passport_control": "6.0", + "customs": "6.0", + "baggage_claim": "6.0", + "left_luggage": "6.0", + "warning": "4.0", + "children_crossing": "6.0", + "no_entry": "5.2", + "no_entry_sign": "6.0", + "no_bicycles": "6.0", + "no_smoking": "6.0", + "do_not_litter": "6.0", + "non-potable_water": "6.0", + "no_pedestrians": "6.0", + "no_mobile_phones": "6.0", + "underage": "6.0", + "radioactive": "1.1", + "radioactive_sign": "1.1", + "biohazard": "1.1", + "biohazard_sign": "1.1", + "arrow_up": "4.0", + "arrow_upper_right": "1.1", + "arrow_right": "1.1", + "arrow_lower_right": "1.1", + "arrow_down": "4.0", + "arrow_lower_left": "1.1", + "arrow_left": "4.0", + "arrow_upper_left": "1.1", + "arrow_up_down": "1.1", + "left_right_arrow": "1.1", + "leftwards_arrow_with_hook": "1.1", + "arrow_right_hook": "1.1", + "arrow_heading_up": "3.2", + "arrow_heading_down": "3.2", + "arrows_clockwise": "6.0", + "arrows_counterclockwise": "6.0", + "back": "6.0", + "end": "6.0", + "on": "6.0", + "soon": "6.0", + "top": "6.0", + "place_of_worship": "8.0", + "worship_symbol": "8.0", + "atom": "4.1", + "atom_symbol": "4.1", + "om_symbol": "7.0", + "star_of_david": "1.1", + "wheel_of_dharma": "1.1", + "yin_yang": "1.1", + "cross": "1.1", + "latin_cross": "1.1", + "orthodox_cross": "1.1", + "star_and_crescent": "1.1", + "peace": "1.1", + "peace_symbol": "1.1", + "menorah": "8.0", + "six_pointed_star": "6.0", + "aries": "1.1", + "taurus": "1.1", + "gemini": "1.1", + "cancer": "1.1", + "leo": "1.1", + "virgo": "1.1", + "libra": "1.1", + "scorpius": "1.1", + "sagittarius": "1.1", + "capricorn": "1.1", + "aquarius": "1.1", + "pisces": "1.1", + "ophiuchus": "6.0", + "twisted_rightwards_arrows": "6.0", + "repeat": "6.0", + "repeat_one": "6.0", + "arrow_forward": "1.1", + "fast_forward": "6.0", + "track_next": "6.0", + "next_track": "6.0", + "play_pause": "6.0", + "arrow_backward": "1.1", + "rewind": "6.0", + "track_previous": "6.0", + "previous_track": "6.0", + "arrow_up_small": "6.0", + "arrow_double_up": "6.0", + "arrow_down_small": "6.0", + "arrow_double_down": "6.0", + "pause_button": "7.0", + "double_vertical_bar": "7.0", + "stop_button": "7.0", + "record_button": "7.0", + "eject": "4.0", + "eject_symbol": "4.0", + "cinema": "6.0", + "low_brightness": "6.0", + "high_brightness": "6.0", + "signal_strength": "6.0", + "vibration_mode": "6.0", + "mobile_phone_off": "6.0", + "recycle": "3.2", + "name_badge": "6.0", + "fleur-de-lis": "4.1", + "beginner": "6.0", + "trident": "6.0", + "o": "5.2", + "white_check_mark": "6.0", + "ballot_box_with_check": "1.1", + "heavy_check_mark": "1.1", + "heavy_multiplication_x": "1.1", + "x": "6.0", + "negative_squared_cross_mark": "6.0", + "heavy_plus_sign": "6.0", + "heavy_minus_sign": "6.0", + "heavy_division_sign": "6.0", + "curly_loop": "6.0", + "loop": "6.0", + "part_alternation_mark": "3.2", + "eight_spoked_asterisk": "1.1", + "eight_pointed_black_star": "1.1", + "sparkle": "1.1", + "bangbang": "1.1", + "interrobang": "3.0", + "question": "6.0", + "grey_question": "6.0", + "grey_exclamation": "6.0", + "exclamation": "5.2", + "wavy_dash": "1.1", + "copyright": "1.1", + "registered": "1.1", + "tm": "1.1", + "hash": "3.0", + "asterisk": "3.0", + "keycap_asterisk": "3.0", + "zero": "3.0", + "one": "3.0", + "two": "3.0", + "three": "3.0", + "four": "3.0", + "five": "3.0", + "six": "3.0", + "seven": "3.0", + "eight": "3.0", + "nine": "3.0", + "keycap_ten": "6.0", + "capital_abcd": "6.0", + "abcd": "6.0", + "symbols": "6.0", + "abc": "6.0", + "a": "6.0", + "ab": "6.0", + "b": "6.0", + "cl": "6.0", + "cool": "6.0", + "free": "6.0", + "information_source": "3.0", + "id": "6.0", + "m": "1.1", + "new": "6.0", + "ng": "6.0", + "o2": "6.0", + "ok": "6.0", + "parking": "5.2", + "sos": "6.0", + "up": "6.0", + "vs": "6.0", + "koko": "6.0", + "sa": "6.0", + "u6708": "6.0", + "u6709": "6.0", + "u6307": "5.2", + "ideograph_advantage": "6.0", + "u5272": "6.0", + "u7121": "5.2", + "u7981": "6.0", + "accept": "6.0", + "u7533": "6.0", + "u5408": "6.0", + "u7a7a": "6.0", + "congratulations": "1.1", + "secret": "1.1", + "u55b6": "6.0", + "u6e80": "6.0", + "black_small_square": "1.1", + "white_small_square": "1.1", + "white_medium_square": "3.2", + "black_medium_square": "3.2", + "white_medium_small_square": "3.2", + "black_medium_small_square": "3.2", + "black_large_square": "5.1", + "white_large_square": "5.1", + "large_orange_diamond": "6.0", + "large_blue_diamond": "6.0", + "small_orange_diamond": "6.0", + "small_blue_diamond": "6.0", + "small_red_triangle": "6.0", + "small_red_triangle_down": "6.0", + "diamond_shape_with_a_dot_inside": "6.0", + "radio_button": "6.0", + "black_square_button": "6.0", + "white_square_button": "6.0", + "white_circle": "4.1", + "black_circle": "4.1", + "red_circle": "6.0", + "blue_circle": "6.0", + "checkered_flag": "6.0", + "triangular_flag_on_post": "6.0", + "crossed_flags": "6.0", + "flag_black": "6.0", + "waving_black_flag": "6.0", + "flag_white": "6.0", + "waving_white_flag": "6.0", + "rainbow_flag": "6.0", + "gay_pride_flag": "6.0", + "flag_ac": "6.0", + "ac": "6.0", + "flag_ad": "6.0", + "ad": "6.0", + "flag_ae": "6.0", + "ae": "6.0", + "flag_af": "6.0", + "af": "6.0", + "flag_ag": "6.0", + "ag": "6.0", + "flag_ai": "6.0", + "ai": "6.0", + "flag_al": "6.0", + "al": "6.0", + "flag_am": "6.0", + "am": "6.0", + "flag_ao": "6.0", + "ao": "6.0", + "flag_aq": "6.0", + "aq": "6.0", + "flag_ar": "6.0", + "ar": "6.0", + "flag_as": "6.0", + "as": "6.0", + "flag_at": "6.0", + "at": "6.0", + "flag_au": "6.0", + "au": "6.0", + "flag_aw": "6.0", + "aw": "6.0", + "flag_ax": "6.0", + "ax": "6.0", + "flag_az": "6.0", + "az": "6.0", + "flag_ba": "6.0", + "ba": "6.0", + "flag_bb": "6.0", + "bb": "6.0", + "flag_bd": "6.0", + "bd": "6.0", + "flag_be": "6.0", + "be": "6.0", + "flag_bf": "6.0", + "bf": "6.0", + "flag_bg": "6.0", + "bg": "6.0", + "flag_bh": "6.0", + "bh": "6.0", + "flag_bi": "6.0", + "bi": "6.0", + "flag_bj": "6.0", + "bj": "6.0", + "flag_bl": "6.0", + "bl": "6.0", + "flag_bm": "6.0", + "bm": "6.0", + "flag_bn": "6.0", + "bn": "6.0", + "flag_bo": "6.0", + "bo": "6.0", + "flag_bq": "6.0", + "bq": "6.0", + "flag_br": "6.0", + "br": "6.0", + "flag_bs": "6.0", + "bs": "6.0", + "flag_bt": "6.0", + "bt": "6.0", + "flag_bv": "6.0", + "bv": "6.0", + "flag_bw": "6.0", + "bw": "6.0", + "flag_by": "6.0", + "by": "6.0", + "flag_bz": "6.0", + "bz": "6.0", + "flag_ca": "6.0", + "ca": "6.0", + "flag_cc": "6.0", + "cc": "6.0", + "flag_cd": "6.0", + "congo": "6.0", + "flag_cf": "6.0", + "cf": "6.0", + "flag_cg": "6.0", + "cg": "6.0", + "flag_ch": "6.0", + "ch": "6.0", + "flag_ci": "6.0", + "ci": "6.0", + "flag_ck": "6.0", + "ck": "6.0", + "flag_cl": "6.0", + "chile": "6.0", + "flag_cm": "6.0", + "cm": "6.0", + "flag_cn": "6.0", + "cn": "6.0", + "flag_co": "6.0", + "co": "6.0", + "flag_cp": "6.0", + "cp": "6.0", + "flag_cr": "6.0", + "cr": "6.0", + "flag_cu": "6.0", + "cu": "6.0", + "flag_cv": "6.0", + "cv": "6.0", + "flag_cw": "6.0", + "cw": "6.0", + "flag_cx": "6.0", + "cx": "6.0", + "flag_cy": "6.0", + "cy": "6.0", + "flag_cz": "6.0", + "cz": "6.0", + "flag_de": "6.0", + "de": "6.0", + "flag_dg": "6.0", + "dg": "6.0", + "flag_dj": "6.0", + "dj": "6.0", + "flag_dk": "6.0", + "dk": "6.0", + "flag_dm": "6.0", + "dm": "6.0", + "flag_do": "6.0", + "do": "6.0", + "flag_dz": "6.0", + "dz": "6.0", + "flag_ea": "6.0", + "ea": "6.0", + "flag_ec": "6.0", + "ec": "6.0", + "flag_ee": "6.0", + "ee": "6.0", + "flag_eg": "6.0", + "eg": "6.0", + "flag_eh": "6.0", + "eh": "6.0", + "flag_er": "6.0", + "er": "6.0", + "flag_es": "6.0", + "es": "6.0", + "flag_et": "6.0", + "et": "6.0", + "flag_eu": "6.0", + "eu": "6.0", + "flag_fi": "6.0", + "fi": "6.0", + "flag_fj": "6.0", + "fj": "6.0", + "flag_fk": "6.0", + "fk": "6.0", + "flag_fm": "6.0", + "fm": "6.0", + "flag_fo": "6.0", + "fo": "6.0", + "flag_fr": "6.0", + "fr": "6.0", + "flag_ga": "6.0", + "ga": "6.0", + "flag_gb": "6.0", + "gb": "6.0", + "flag_gd": "6.0", + "gd": "6.0", + "flag_ge": "6.0", + "ge": "6.0", + "flag_gf": "6.0", + "gf": "6.0", + "flag_gg": "6.0", + "gg": "6.0", + "flag_gh": "6.0", + "gh": "6.0", + "flag_gi": "6.0", + "gi": "6.0", + "flag_gl": "6.0", + "gl": "6.0", + "flag_gm": "6.0", + "gm": "6.0", + "flag_gn": "6.0", + "gn": "6.0", + "flag_gp": "6.0", + "gp": "6.0", + "flag_gq": "6.0", + "gq": "6.0", + "flag_gr": "6.0", + "gr": "6.0", + "flag_gs": "6.0", + "gs": "6.0", + "flag_gt": "6.0", + "gt": "6.0", + "flag_gu": "6.0", + "gu": "6.0", + "flag_gw": "6.0", + "gw": "6.0", + "flag_gy": "6.0", + "gy": "6.0", + "flag_hk": "6.0", + "hk": "6.0", + "flag_hm": "6.0", + "hm": "6.0", + "flag_hn": "6.0", + "hn": "6.0", + "flag_hr": "6.0", + "hr": "6.0", + "flag_ht": "6.0", + "ht": "6.0", + "flag_hu": "6.0", + "hu": "6.0", + "flag_ic": "6.0", + "ic": "6.0", + "flag_id": "6.0", + "indonesia": "6.0", + "flag_ie": "6.0", + "ie": "6.0", + "flag_il": "6.0", + "il": "6.0", + "flag_im": "6.0", + "im": "6.0", + "flag_in": "6.0", + "in": "6.0", + "flag_io": "6.0", + "io": "6.0", + "flag_iq": "6.0", + "iq": "6.0", + "flag_ir": "6.0", + "ir": "6.0", + "flag_is": "6.0", + "is": "6.0", + "flag_it": "6.0", + "it": "6.0", + "flag_je": "6.0", + "je": "6.0", + "flag_jm": "6.0", + "jm": "6.0", + "flag_jo": "6.0", + "jo": "6.0", + "flag_jp": "6.0", + "jp": "6.0", + "flag_ke": "6.0", + "ke": "6.0", + "flag_kg": "6.0", + "kg": "6.0", + "flag_kh": "6.0", + "kh": "6.0", + "flag_ki": "6.0", + "ki": "6.0", + "flag_km": "6.0", + "km": "6.0", + "flag_kn": "6.0", + "kn": "6.0", + "flag_kp": "6.0", + "kp": "6.0", + "flag_kr": "6.0", + "kr": "6.0", + "flag_kw": "6.0", + "kw": "6.0", + "flag_ky": "6.0", + "ky": "6.0", + "flag_kz": "6.0", + "kz": "6.0", + "flag_la": "6.0", + "la": "6.0", + "flag_lb": "6.0", + "lb": "6.0", + "flag_lc": "6.0", + "lc": "6.0", + "flag_li": "6.0", + "li": "6.0", + "flag_lk": "6.0", + "lk": "6.0", + "flag_lr": "6.0", + "lr": "6.0", + "flag_ls": "6.0", + "ls": "6.0", + "flag_lt": "6.0", + "lt": "6.0", + "flag_lu": "6.0", + "lu": "6.0", + "flag_lv": "6.0", + "lv": "6.0", + "flag_ly": "6.0", + "ly": "6.0", + "flag_ma": "6.0", + "ma": "6.0", + "flag_mc": "6.0", + "mc": "6.0", + "flag_md": "6.0", + "md": "6.0", + "flag_me": "6.0", + "me": "6.0", + "flag_mf": "6.0", + "mf": "6.0", + "flag_mg": "6.0", + "mg": "6.0", + "flag_mh": "6.0", + "mh": "6.0", + "flag_mk": "6.0", + "mk": "6.0", + "flag_ml": "6.0", + "ml": "6.0", + "flag_mm": "6.0", + "mm": "6.0", + "flag_mn": "6.0", + "mn": "6.0", + "flag_mo": "6.0", + "mo": "6.0", + "flag_mp": "6.0", + "mp": "6.0", + "flag_mq": "6.0", + "mq": "6.0", + "flag_mr": "6.0", + "mr": "6.0", + "flag_ms": "6.0", + "ms": "6.0", + "flag_mt": "6.0", + "mt": "6.0", + "flag_mu": "6.0", + "mu": "6.0", + "flag_mv": "6.0", + "mv": "6.0", + "flag_mw": "6.0", + "mw": "6.0", + "flag_mx": "6.0", + "mx": "6.0", + "flag_my": "6.0", + "my": "6.0", + "flag_mz": "6.0", + "mz": "6.0", + "flag_na": "6.0", + "na": "6.0", + "flag_nc": "6.0", + "nc": "6.0", + "flag_ne": "6.0", + "ne": "6.0", + "flag_nf": "6.0", + "nf": "6.0", + "flag_ng": "6.0", + "nigeria": "6.0", + "flag_ni": "6.0", + "ni": "6.0", + "flag_nl": "6.0", + "nl": "6.0", + "flag_no": "6.0", + "no": "6.0", + "flag_np": "6.0", + "np": "6.0", + "flag_nr": "6.0", + "nr": "6.0", + "flag_nu": "6.0", + "nu": "6.0", + "flag_nz": "6.0", + "nz": "6.0", + "flag_om": "6.0", + "om": "6.0", + "flag_pa": "6.0", + "pa": "6.0", + "flag_pe": "6.0", + "pe": "6.0", + "flag_pf": "6.0", + "pf": "6.0", + "flag_pg": "6.0", + "pg": "6.0", + "flag_ph": "6.0", + "ph": "6.0", + "flag_pk": "6.0", + "pk": "6.0", + "flag_pl": "6.0", + "pl": "6.0", + "flag_pm": "6.0", + "pm": "6.0", + "flag_pn": "6.0", + "pn": "6.0", + "flag_pr": "6.0", + "pr": "6.0", + "flag_ps": "6.0", + "ps": "6.0", + "flag_pt": "6.0", + "pt": "6.0", + "flag_pw": "6.0", + "pw": "6.0", + "flag_py": "6.0", + "py": "6.0", + "flag_qa": "6.0", + "qa": "6.0", + "flag_re": "6.0", + "re": "6.0", + "flag_ro": "6.0", + "ro": "6.0", + "flag_rs": "6.0", + "rs": "6.0", + "flag_ru": "6.0", + "ru": "6.0", + "flag_rw": "6.0", + "rw": "6.0", + "flag_sa": "6.0", + "saudiarabia": "6.0", + "saudi": "6.0", + "flag_sb": "6.0", + "sb": "6.0", + "flag_sc": "6.0", + "sc": "6.0", + "flag_sd": "6.0", + "sd": "6.0", + "flag_se": "6.0", + "se": "6.0", + "flag_sg": "6.0", + "sg": "6.0", + "flag_sh": "6.0", + "sh": "6.0", + "flag_si": "6.0", + "si": "6.0", + "flag_sj": "6.0", + "sj": "6.0", + "flag_sk": "6.0", + "sk": "6.0", + "flag_sl": "6.0", + "sl": "6.0", + "flag_sm": "6.0", + "sm": "6.0", + "flag_sn": "6.0", + "sn": "6.0", + "flag_so": "6.0", + "so": "6.0", + "flag_sr": "6.0", + "sr": "6.0", + "flag_ss": "6.0", + "ss": "6.0", + "flag_st": "6.0", + "st": "6.0", + "flag_sv": "6.0", + "sv": "6.0", + "flag_sx": "6.0", + "sx": "6.0", + "flag_sy": "6.0", + "sy": "6.0", + "flag_sz": "6.0", + "sz": "6.0", + "flag_ta": "6.0", + "ta": "6.0", + "flag_tc": "6.0", + "tc": "6.0", + "flag_td": "6.0", + "td": "6.0", + "flag_tf": "6.0", + "tf": "6.0", + "flag_tg": "6.0", + "tg": "6.0", + "flag_th": "6.0", + "th": "6.0", + "flag_tj": "6.0", + "tj": "6.0", + "flag_tk": "6.0", + "tk": "6.0", + "flag_tl": "6.0", + "tl": "6.0", + "flag_tm": "6.0", + "turkmenistan": "6.0", + "flag_tn": "6.0", + "tn": "6.0", + "flag_to": "6.0", + "to": "6.0", + "flag_tr": "6.0", + "tr": "6.0", + "flag_tt": "6.0", + "tt": "6.0", + "flag_tv": "6.0", + "tuvalu": "6.0", + "flag_tw": "6.0", + "tw": "6.0", + "flag_tz": "6.0", + "tz": "6.0", + "flag_ua": "6.0", + "ua": "6.0", + "flag_ug": "6.0", + "ug": "6.0", + "flag_um": "6.0", + "um": "6.0", + "flag_us": "6.0", + "us": "6.0", + "flag_uy": "6.0", + "uy": "6.0", + "flag_uz": "6.0", + "uz": "6.0", + "flag_va": "6.0", + "va": "6.0", + "flag_vc": "6.0", + "vc": "6.0", + "flag_ve": "6.0", + "ve": "6.0", + "flag_vg": "6.0", + "vg": "6.0", + "flag_vi": "6.0", + "vi": "6.0", + "flag_vn": "6.0", + "vn": "6.0", + "flag_vu": "6.0", + "vu": "6.0", + "flag_wf": "6.0", + "wf": "6.0", + "flag_ws": "6.0", + "ws": "6.0", + "flag_xk": "6.0", + "xk": "6.0", + "flag_ye": "6.0", + "ye": "6.0", + "flag_yt": "6.0", + "yt": "6.0", + "flag_za": "6.0", + "za": "6.0", + "flag_zm": "6.0", + "zm": "6.0", + "flag_zw": "6.0", + "zw": "6.0", + "regional_indicator_z": "6.0", + "regional_indicator_y": "6.0", + "regional_indicator_x": "6.0", + "regional_indicator_w": "6.0", + "regional_indicator_v": "6.0", + "regional_indicator_u": "6.0", + "regional_indicator_t": "6.0", + "regional_indicator_s": "6.0", + "regional_indicator_r": "6.0", + "regional_indicator_q": "6.0", + "regional_indicator_p": "6.0", + "regional_indicator_o": "6.0", + "regional_indicator_n": "6.0", + "regional_indicator_m": "6.0", + "regional_indicator_l": "6.0", + "regional_indicator_k": "6.0", + "regional_indicator_j": "6.0", + "regional_indicator_i": "6.0", + "regional_indicator_h": "6.0", + "regional_indicator_g": "6.0", + "regional_indicator_f": "6.0", + "regional_indicator_e": "6.0", + "regional_indicator_d": "6.0", + "regional_indicator_c": "6.0", + "regional_indicator_b": "6.0", + "regional_indicator_a": "6.0", + "large_blue_circle": "6.0", + "ten": "6.0" +} \ No newline at end of file diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb index 42703545c4f..35871fd1b7b 100644 --- a/lib/gitlab/emoji.rb +++ b/lib/gitlab/emoji.rb @@ -31,7 +31,7 @@ module Gitlab end def emoji_unicode_version(name) - @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json'))) + @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json'))) @emoji_unicode_versions_by_name[name] end diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index 1f93b5a4dd2..5293f5af12d 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -1,9 +1,12 @@ namespace :gemojione do desc 'Generates Emoji SHA256 digests' - task digests: :environment do + task digests: ['yarn:check', 'environment'] do require 'digest/sha2' require 'json' + # We don't have `node_modules` available in built versions of GitLab + FileUtils.cp_r(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json'), File.join(Rails.root, 'fixtures', 'emojis')) + dir = Gemojione.images_path resultant_emoji_map = {} -- cgit v1.2.1 From 75e78f108f850fe6c70c04a13747d2c40a511774 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 8 Mar 2017 16:45:59 +0000 Subject: The GitLab Pages external-http and external-https arguments can be specified multiple times --- config/gitlab.yml.example | 4 ++-- config/initializers/1_settings.rb | 4 ++-- doc/administration/pages/index.md | 27 ++++++++++++++++----------- features/steps/project/pages.rb | 6 +++--- lib/support/init.d/gitlab.default.example | 4 ++-- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 720df0cac2d..8d0ea603569 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -157,8 +157,8 @@ production: &base host: example.com port: 80 # Set to 443 if you serve the pages with HTTPS https: false # Set to true if you serve the pages with HTTPS - # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages - # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages + # external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages + # external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages ## Mattermost ## For enabling Add to Mattermost button diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b45d0e23080..e5e90031871 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -278,8 +278,8 @@ Settings.pages['host'] ||= "example.com" Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http" Settings.pages['url'] ||= Settings.send(:build_pages_url) -Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil? -Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil? +Settings.pages['external_http'] ||= false unless Settings.pages['external_http'].present? +Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present? # # Git LFS diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 62b0468da79..0c63b0b59a7 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -26,9 +26,9 @@ it works. --- -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 +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. @@ -65,11 +65,13 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that GitLab runs. For example, an entry would look like this: ``` -*.example.io. 1800 IN A 1.1.1.1 +*.example.io. 1800 IN A 1.1.1.1 +*.example.io. 1800 IN AAAA 2001::1 ``` where `example.io` is the domain under which GitLab Pages will be served -and `1.1.1.1` is the IP address of your GitLab instance. +and `1.1.1.1` is the IPv4 address of your GitLab instance and `2001::1` is the +IPv6 address. If you don't have IPv6, you can omit the AAAA record. > **Note:** You should not use the GitLab domain to serve user pages. For more information @@ -141,7 +143,8 @@ outside world. In addition to the wildcard domains, you can also have the option to configure GitLab Pages to work with custom domains. Again, there are two options here: support custom domains with and without TLS certificates. The easiest setup is -that without TLS certificates. +that without TLS certificates. In either case, you'll need a secondary IP. If +you have IPv6 as well as IPv4 addresses, you can use them both. ### Custom domains @@ -163,11 +166,12 @@ world. Custom domains are supported, but no TLS. pages_external_url "http://example.io" nginx['listen_addresses'] = ['1.1.1.1'] pages_nginx['enable'] = false - gitlab_pages['external_http'] = '1.1.1.2:80' + gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80'] ``` where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + `1.1.1.2` and `2001::2` are the secondary IPs the GitLab Pages daemon + listens on. If you don't have IPv6, you can omit the IPv6 address. 1. [Reconfigure GitLab][reconfigure] @@ -194,12 +198,13 @@ world. Custom domains and TLS are supported. pages_nginx['enable'] = false gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" - gitlab_pages['external_http'] = '1.1.1.2:80' - gitlab_pages['external_https'] = '1.1.1.2:443' + gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80'] + gitlab_pages['external_https'] = ['1.1.1.2:443', '[2001::2]:443'] ``` where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + `1.1.1.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon + listens on. If you don't have IPv6, you can omit the IPv6 address. 1. [Reconfigure GitLab][reconfigure] diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb index c80c6273807..4045955a8b9 100644 --- a/features/steps/project/pages.rb +++ b/features/steps/project/pages.rb @@ -53,13 +53,13 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps end step 'pages are exposed on external HTTP address' do - allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80') + allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80']) allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) end step 'pages are exposed on external HTTPS address' do - allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80') - allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443') + allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80']) + allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443']) end step 'I should be able to add a New Domain' do diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index e5797d8fe3c..f6642527639 100644 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -56,14 +56,14 @@ gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" # The value of -listen-http must be set to `gitlab.yml > pages > external_http` # as well. For example: # -# -listen-http 1.1.1.1:80 +# -listen-http 1.1.1.1:80 -listen-http [2001::1]:80 # # To enable HTTPS support for custom domains add the `-listen-https`, # `-root-cert` and `-root-key` directives in `gitlab_pages_options` below. # The value of -listen-https must be set to `gitlab.yml > pages > external_https` # as well. For example: # -# -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key +# -listen-https 1.1.1.1:443 -listen-http [2001::1]:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key # # The -pages-domain must be specified the same as in `gitlab.yml > pages > host`. # Set `gitlab_pages_enabled=true` if you want to enable the Pages feature. -- cgit v1.2.1 From 4b1dd072c63e71f19f173c0f4f0c87e5e3cf0f13 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 8 Mar 2017 17:04:44 +0000 Subject: Raise an exception if creating a test repository fails --- spec/factories/projects.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index c6f91e05d83..0db2fe04edd 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -38,7 +38,7 @@ FactoryGirl.define do trait :empty_repo do after(:create) do |project| - project.create_repository + raise "Failed to create repository!" unless project.create_repository # We delete hooks so that gitlab-shell will not try to authenticate with # an API that isn't running @@ -48,7 +48,7 @@ FactoryGirl.define do trait :broken_repo do after(:create) do |project| - project.create_repository + raise "Failed to create repository!" unless project.create_repository FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'refs')) end -- cgit v1.2.1 From ee9930145fafdd3869ecf985f0b2295081ac3c7e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Mar 2017 15:25:27 -0600 Subject: Update emojis to use harmony modules (import/export) --- app/assets/javascripts/awards_handler.js | 10 +- app/assets/javascripts/behaviors/gl_emoji.js | 206 +++---------- .../gl_emoji/is_emoji_unicode_supported.js | 121 ++++++++ .../behaviors/gl_emoji/spread_string.js | 2 +- .../behaviors/gl_emoji/unicode_support_map.js | 33 ++- app/assets/javascripts/extensions/string.js | 4 +- app/assets/javascripts/gfm_auto_complete.js | 8 +- app/assets/javascripts/main.js | 326 ++++++++++----------- config/webpack.config.js | 3 +- spec/javascripts/awards_handler_spec.js | 5 +- spec/javascripts/gl_emoji_spec.js | 23 +- 11 files changed, 375 insertions(+), 366 deletions(-) create mode 100644 app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 4667980a960..54836efdf29 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,10 +1,8 @@ /* global Cookies */ -const emojiMap = require('emoji-map'); -const emojiAliases = require('emoji-aliases'); -const glEmoji = require('./behaviors/gl_emoji'); - -const glEmojiTag = glEmoji.glEmojiTag; +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; +import { glEmojiTag } from './behaviors/gl_emoji'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const requestAnimationFrame = window.requestAnimationFrame || @@ -515,4 +513,4 @@ AwardsHandler.prototype.destroy = function destroy() { $('.emoji-menu').remove(); }; -module.exports = AwardsHandler; +export default AwardsHandler; diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index d1d98c3919f..59741cc9b1a 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -1,11 +1,13 @@ -const installCustomElements = require('document-register-element'); -const emojiMap = require('emoji-map'); -const emojiAliases = require('emoji-aliases'); -const generatedUnicodeSupportMap = require('./gl_emoji/unicode_support_map'); -const spreadString = require('./gl_emoji/spread_string'); +import installCustomElements from 'document-register-element'; +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; +import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map'; +import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported'; installCustomElements(window); +const generatedUnicodeSupportMap = getUnicodeSupportMap(); + function emojiImageTag(name, src) { return `:${name}:`; } @@ -55,163 +57,49 @@ function glEmojiTag(inputName, options) { `; } -// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ -const flagACodePoint = 127462; // parseInt('1F1E6', 16) -const flagZCodePoint = 127487; // parseInt('1F1FF', 16) -function isFlagEmoji(emojiUnicode) { - const cp = emojiUnicode.codePointAt(0); - // Length 4 because flags are made of 2 characters which are surrogate pairs - return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint; -} - -// Chrome <57 renders keycaps oddly -// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294 -// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png -function isKeycapEmoji(emojiUnicode) { - return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3'; -} - -// Check for a skin tone variation emoji which aren't always supported -const tone1 = 127995;// parseInt('1F3FB', 16) -const tone5 = 127999;// parseInt('1F3FF', 16) -function isSkinToneComboEmoji(emojiUnicode) { - return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { - const cp = char.codePointAt(0); - return cp >= tone1 && cp <= tone5; - }); -} - -// macOS supports most skin tone emoji's but -// doesn't support the skin tone versions of horse racing -const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) -function isHorceRacingSkinToneComboEmoji(emojiUnicode) { - return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && - isSkinToneComboEmoji(emojiUnicode); -} - -// Check for `family_*`, `kiss_*`, `couple_*` -// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these -const zwj = 8205; // parseInt('200D', 16) -const personStartCodePoint = 128102; // parseInt('1F466', 16) -const personEndCodePoint = 128105; // parseInt('1F469', 16) -function isPersonZwjEmoji(emojiUnicode) { - let hasPersonEmoji = false; - let hasZwj = false; - spreadString(emojiUnicode).forEach((character) => { - const cp = character.codePointAt(0); - if (cp === zwj) { - hasZwj = true; - } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) { - hasPersonEmoji = true; +function installGlEmojiElement() { + const GlEmojiElementProto = Object.create(HTMLElement.prototype); + GlEmojiElementProto.createdCallback = function createdCallback() { + const emojiUnicode = this.textContent.trim(); + const { + name, + unicodeVersion, + fallbackSrc, + fallbackSpriteClass, + } = this.dataset; + + const isEmojiUnicode = this.childNodes && Array.prototype.every.call( + this.childNodes, + childNode => childNode.nodeType === 3, + ); + const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; + const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; + + if ( + isEmojiUnicode && + !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) + ) { + // CSS sprite fallback takes precedence over image fallback + if (hasCssSpriteFalback) { + // IE 11 doesn't like adding multiple at once :( + this.classList.add('emoji-icon'); + this.classList.add(fallbackSpriteClass); + } else if (hasImageFallback) { + this.innerHTML = emojiImageTag(name, fallbackSrc); + } else { + const src = assembleFallbackImageSrc(name); + this.innerHTML = emojiImageTag(name, src); + } } - }); - - return hasPersonEmoji && hasZwj; -} - -// Helper so we don't have to run `isFlagEmoji` twice -// in `isEmojiUnicodeSupported` logic -function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { - const isFlagResult = isFlagEmoji(emojiUnicode); - return ( - (unicodeSupportMap.flag && isFlagResult) || - !isFlagResult - ); -} - -// Helper so we don't have to run `isSkinToneComboEmoji` twice -// in `isEmojiUnicodeSupported` logic -function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { - const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode); - return ( - (unicodeSupportMap.skinToneModifier && isSkinToneResult) || - !isSkinToneResult - ); -} - -// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice -// in `isEmojiUnicodeSupported` logic -function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) { - const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode); - return ( - (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || - !isHorseRacingSkinToneResult - ); -} - -// Helper so we don't have to run `isPersonZwjEmoji` twice -// in `isEmojiUnicodeSupported` logic -function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { - const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode); - return ( - (unicodeSupportMap.personZwj && isPersonZwjResult) || - !isPersonZwjResult - ); -} - -// Takes in a support map and determines whether -// the given unicode emoji is supported on the platform. -// -// Combines all the edge case tests into a one-stop shop method -function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) { - const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome && - unicodeSupportMap.meta.chromeVersion < 57; + }; - // For comments about each scenario, see the comments above each individual respective function - return unicodeSupportMap[unicodeVersion] && - !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && - checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && - checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && - checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) && - checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode); + document.registerElement('gl-emoji', { + prototype: GlEmojiElementProto, + }); } -const GlEmojiElementProto = Object.create(HTMLElement.prototype); -GlEmojiElementProto.createdCallback = function createdCallback() { - const emojiUnicode = this.textContent.trim(); - const { - name, - unicodeVersion, - fallbackSrc, - fallbackSpriteClass, - } = this.dataset; - - const isEmojiUnicode = this.childNodes && Array.prototype.every.call( - this.childNodes, - childNode => childNode.nodeType === 3, - ); - const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; - const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; - - if ( - isEmojiUnicode && - !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) - ) { - // CSS sprite fallback takes precedence over image fallback - if (hasCssSpriteFalback) { - // IE 11 doesn't like adding multiple at once :( - this.classList.add('emoji-icon'); - this.classList.add(fallbackSpriteClass); - } else if (hasImageFallback) { - this.innerHTML = emojiImageTag(name, fallbackSrc); - } else { - const src = assembleFallbackImageSrc(name); - this.innerHTML = emojiImageTag(name, src); - } - } -}; - -document.registerElement('gl-emoji', { - prototype: GlEmojiElementProto, -}); - -module.exports = { - emojiImageTag, +export { + installGlEmojiElement, glEmojiTag, - isEmojiUnicodeSupported, - isFlagEmoji, - isKeycapEmoji, - isSkinToneComboEmoji, - isHorceRacingSkinToneComboEmoji, - isPersonZwjEmoji, + emojiImageTag, }; diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js new file mode 100644 index 00000000000..5e3c45f7e92 --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js @@ -0,0 +1,121 @@ +import spreadString from './spread_string'; + +// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ +const flagACodePoint = 127462; // parseInt('1F1E6', 16) +const flagZCodePoint = 127487; // parseInt('1F1FF', 16) +function isFlagEmoji(emojiUnicode) { + const cp = emojiUnicode.codePointAt(0); + // Length 4 because flags are made of 2 characters which are surrogate pairs + return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint; +} + +// Chrome <57 renders keycaps oddly +// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294 +// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png +function isKeycapEmoji(emojiUnicode) { + return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3'; +} + +// Check for a skin tone variation emoji which aren't always supported +const tone1 = 127995;// parseInt('1F3FB', 16) +const tone5 = 127999;// parseInt('1F3FF', 16) +function isSkinToneComboEmoji(emojiUnicode) { + return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { + const cp = char.codePointAt(0); + return cp >= tone1 && cp <= tone5; + }); +} + +// macOS supports most skin tone emoji's but +// doesn't support the skin tone versions of horse racing +const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) +function isHorceRacingSkinToneComboEmoji(emojiUnicode) { + return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && + isSkinToneComboEmoji(emojiUnicode); +} + +// Check for `family_*`, `kiss_*`, `couple_*` +// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these +const zwj = 8205; // parseInt('200D', 16) +const personStartCodePoint = 128102; // parseInt('1F466', 16) +const personEndCodePoint = 128105; // parseInt('1F469', 16) +function isPersonZwjEmoji(emojiUnicode) { + let hasPersonEmoji = false; + let hasZwj = false; + spreadString(emojiUnicode).forEach((character) => { + const cp = character.codePointAt(0); + if (cp === zwj) { + hasZwj = true; + } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) { + hasPersonEmoji = true; + } + }); + + return hasPersonEmoji && hasZwj; +} + +// Helper so we don't have to run `isFlagEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isFlagResult = isFlagEmoji(emojiUnicode); + return ( + (unicodeSupportMap.flag && isFlagResult) || + !isFlagResult + ); +} + +// Helper so we don't have to run `isSkinToneComboEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { + const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode); + return ( + (unicodeSupportMap.skinToneModifier && isSkinToneResult) || + !isSkinToneResult + ); +} + +// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode); + return ( + (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || + !isHorseRacingSkinToneResult + ); +} + +// Helper so we don't have to run `isPersonZwjEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode); + return ( + (unicodeSupportMap.personZwj && isPersonZwjResult) || + !isPersonZwjResult + ); +} + +// Takes in a support map and determines whether +// the given unicode emoji is supported on the platform. +// +// Combines all the edge case tests into a one-stop shop method +function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) { + const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome && + unicodeSupportMap.meta.chromeVersion < 57; + + // For comments about each scenario, see the comments above each individual respective function + return unicodeSupportMap[unicodeVersion] && + !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && + checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && + checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && + checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) && + checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode); +} + +export { + isEmojiUnicodeSupported, + isFlagEmoji, + isKeycapEmoji, + isSkinToneComboEmoji, + isHorceRacingSkinToneComboEmoji, + isPersonZwjEmoji, +}; diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js index 2380349c4fa..327764ec6e9 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js +++ b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js @@ -47,4 +47,4 @@ function spreadString(str) { return arr; } -module.exports = spreadString; +export default spreadString; diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js index f31716d4c07..aa522e20c36 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js +++ b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js @@ -68,7 +68,7 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche // See 32px, https://i.imgur.com/htY6Zym.png // See 16px, https://i.imgur.com/FPPsIF8.png const fontSize = 16; -function testUnicodeSupportMap(testMap) { +function generateUnicodeSupportMap(testMap) { const testMapKeys = Object.keys(testMap); const numTestEntries = testMapKeys .reduce((list, testKey) => list.concat(testMap[testKey]), []).length; @@ -138,17 +138,24 @@ function testUnicodeSupportMap(testMap) { return resultMap; } -let unicodeSupportMap; -const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); -try { - unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); -} catch (err) { - // swallow -} -if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { - unicodeSupportMap = testUnicodeSupportMap(unicodeSupportTestMap); - window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); - window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); +function getUnicodeSupportMap() { + let unicodeSupportMap; + const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); + try { + unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); + } catch (err) { + // swallow + } + if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { + unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap); + window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); + window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); + } + + return unicodeSupportMap; } -module.exports = unicodeSupportMap; +export { + getUnicodeSupportMap, + generateUnicodeSupportMap, +}; diff --git a/app/assets/javascripts/extensions/string.js b/app/assets/javascripts/extensions/string.js index fe23be0bbc1..ae9662444b0 100644 --- a/app/assets/javascripts/extensions/string.js +++ b/app/assets/javascripts/extensions/string.js @@ -1,2 +1,2 @@ -require('string.prototype.codepointat'); -require('string.fromcodepoint'); +import 'string.prototype.codepointat'; +import 'string.fromcodepoint'; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 1bc04a5ad96..4f7ce1fa197 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,10 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ -const emojiMap = require('emoji-map'); -const emojiAliases = require('emoji-aliases'); -const glEmoji = require('./behaviors/gl_emoji'); - -const glEmojiTag = glEmoji.glEmojiTag; +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; +import { glEmojiTag } from '~/behaviors/gl_emoji'; // Creates the variables for setting up GFM auto-completion (function() { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 79164edff0e..689a6c3a93a 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */ +/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ /* global bp */ /* global Cookies */ /* global Flash */ @@ -13,19 +13,20 @@ import Dropzone from 'dropzone'; import Sortable from 'vendor/Sortable'; // libraries with import side-effects -require('mousetrap'); -require('mousetrap/plugins/pause/mousetrap-pause'); -require('vendor/fuzzaldrin-plus'); -require('es6-promise').polyfill(); +import 'mousetrap'; +import 'mousetrap/plugins/pause/mousetrap-pause'; +import 'vendor/fuzzaldrin-plus'; +import promisePolyfill from 'es6-promise'; // extensions -require('./extensions/string'); -require('./extensions/array'); -require('./extensions/custom_event'); -require('./extensions/element'); -require('./extensions/jquery'); -require('./extensions/object'); -require('es6-promise').polyfill(); +import './extensions/string'; +import './extensions/array'; +import './extensions/custom_event'; +import './extensions/element'; +import './extensions/jquery'; +import './extensions/object'; + +promisePolyfill.polyfill(); // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; @@ -37,174 +38,171 @@ window.Dropzone = Dropzone; window.Sortable = Sortable; // shortcuts -require('./shortcuts'); -require('./shortcuts_navigation'); -require('./shortcuts_dashboard_navigation'); -require('./shortcuts_issuable'); -require('./shortcuts_network'); +import './shortcuts'; +import './shortcuts_blob'; +import './shortcuts_dashboard_navigation'; +import './shortcuts_navigation'; +import './shortcuts_find_file'; +import './shortcuts_issuable'; +import './shortcuts_network'; // behaviors -require('./behaviors/autosize'); -require('./behaviors/details_behavior'); -require('./behaviors/quick_submit'); -require('./behaviors/requires_input'); -require('./behaviors/toggler_behavior'); -require('./behaviors/bind_in_out'); +import './behaviors/autosize'; +import './behaviors/details_behavior'; +import './behaviors/quick_submit'; +import './behaviors/requires_input'; +import './behaviors/toggler_behavior'; +import './behaviors/bind_in_out'; +import { installGlEmojiElement } from './behaviors/gl_emoji'; +installGlEmojiElement(); // blob -require('./blob/blob_ci_yaml'); -require('./blob/blob_dockerfile_selector'); -require('./blob/blob_dockerfile_selectors'); -require('./blob/blob_file_dropzone'); -require('./blob/blob_gitignore_selector'); -require('./blob/blob_gitignore_selectors'); -require('./blob/blob_license_selector'); -require('./blob/blob_license_selectors'); -require('./blob/template_selector'); +import './blob/blob_ci_yaml'; +import './blob/blob_dockerfile_selector'; +import './blob/blob_dockerfile_selectors'; +import './blob/blob_file_dropzone'; +import './blob/blob_gitignore_selector'; +import './blob/blob_gitignore_selectors'; +import './blob/blob_license_selector'; +import './blob/blob_license_selectors'; +import './blob/template_selector'; // templates -require('./templates/issuable_template_selector'); -require('./templates/issuable_template_selectors'); +import './templates/issuable_template_selector'; +import './templates/issuable_template_selectors'; // commit -require('./commit/file.js'); -require('./commit/image_file.js'); +import './commit/file'; +import './commit/image_file'; // lib/utils -require('./lib/utils/animate'); -require('./lib/utils/bootstrap_linked_tabs'); -require('./lib/utils/common_utils'); -require('./lib/utils/datetime_utility'); -require('./lib/utils/notify'); -require('./lib/utils/pretty_time'); -require('./lib/utils/text_utility'); -require('./lib/utils/type_utility'); -require('./lib/utils/url_utility'); +import './lib/utils/animate'; +import './lib/utils/bootstrap_linked_tabs'; +import './lib/utils/common_utils'; +import './lib/utils/datetime_utility'; +import './lib/utils/notify'; +import './lib/utils/pretty_time'; +import './lib/utils/text_utility'; +import './lib/utils/type_utility'; +import './lib/utils/url_utility'; // u2f -require('./u2f/authenticate'); -require('./u2f/error'); -require('./u2f/register'); -require('./u2f/util'); +import './u2f/authenticate'; +import './u2f/error'; +import './u2f/register'; +import './u2f/util'; // droplab -require('./droplab/droplab'); -require('./droplab/droplab_ajax'); -require('./droplab/droplab_ajax_filter'); -require('./droplab/droplab_filter'); +import './droplab/droplab'; +import './droplab/droplab_ajax'; +import './droplab/droplab_ajax_filter'; +import './droplab/droplab_filter'; // everything else -require('./abuse_reports'); -require('./activities'); -require('./admin'); -require('./ajax_loading_spinner'); -require('./api'); -require('./aside'); -require('./autosave'); -const AwardsHandler = require('./awards_handler'); -require('./breakpoints'); -require('./broadcast_message'); -require('./build'); -require('./build_artifacts'); -require('./build_variables'); -require('./ci_lint_editor'); -require('./commit'); -require('./commits'); -require('./compare'); -require('./compare_autocomplete'); -require('./confirm_danger_modal'); -require('./copy_as_gfm'); -require('./copy_to_clipboard'); -require('./create_label'); -require('./diff'); -require('./dispatcher'); -require('./dropzone_input'); -require('./due_date_select'); -require('./files_comment_button'); -require('./flash'); -require('./gfm_auto_complete'); -require('./gl_dropdown'); -require('./gl_field_error'); -require('./gl_field_errors'); -require('./gl_form'); -require('./group_avatar'); -require('./group_label_subscription'); -require('./groups_select'); -require('./header'); -require('./importer_status'); -require('./issuable'); -require('./issuable_context'); -require('./issuable_form'); -require('./issue'); -require('./issue_status_select'); -require('./issues_bulk_assignment'); -require('./label_manager'); -require('./labels'); -require('./labels_select'); -require('./layout_nav'); -require('./line_highlighter'); -require('./logo'); -require('./member_expiration_date'); -require('./members'); -require('./merge_request'); -require('./merge_request_tabs'); -require('./merge_request_widget'); -require('./merged_buttons'); -require('./milestone'); -require('./milestone_select'); -require('./mini_pipeline_graph_dropdown'); -require('./namespace_select'); -require('./new_branch_form'); -require('./new_commit_form'); -require('./notes'); -require('./notifications_dropdown'); -require('./notifications_form'); -require('./pager'); -require('./pipelines'); -require('./preview_markdown'); -require('./project'); -require('./project_avatar'); -require('./project_find_file'); -require('./project_fork'); -require('./project_import'); -require('./project_label_subscription'); -require('./project_new'); -require('./project_select'); -require('./project_show'); -require('./project_variables'); -require('./projects_list'); -require('./render_gfm'); -require('./render_math'); -require('./right_sidebar'); -require('./search'); -require('./search_autocomplete'); -require('./shortcuts'); -require('./shortcuts_blob'); -require('./shortcuts_dashboard_navigation'); -require('./shortcuts_find_file'); -require('./shortcuts_issuable'); -require('./shortcuts_navigation'); -require('./shortcuts_network'); -require('./signin_tabs_memoizer'); -require('./single_file_diff'); -require('./smart_interval'); -require('./snippets_list'); -require('./star'); -require('./subbable_resource'); -require('./subscription'); -require('./subscription_select'); -require('./syntax_highlight'); -require('./task_list'); -require('./todos'); -require('./tree'); -require('./user'); -require('./user_tabs'); -require('./username_validator'); -require('./users_select'); -require('./version_check_image'); -require('./visibility_select'); -require('./wikis'); -require('./zen_mode'); +import './abuse_reports'; +import './activities'; +import './admin'; +import './ajax_loading_spinner'; +import './api'; +import './aside'; +import './autosave'; +import AwardsHandler from './awards_handler'; +import './breakpoints'; +import './broadcast_message'; +import './build'; +import './build_artifacts'; +import './build_variables'; +import './ci_lint_editor'; +import './commit'; +import './commits'; +import './compare'; +import './compare_autocomplete'; +import './confirm_danger_modal'; +import './copy_as_gfm'; +import './copy_to_clipboard'; +import './create_label'; +import './diff'; +import './dispatcher'; +import './dropzone_input'; +import './due_date_select'; +import './files_comment_button'; +import './flash'; +import './gfm_auto_complete'; +import './gl_dropdown'; +import './gl_field_error'; +import './gl_field_errors'; +import './gl_form'; +import './group_avatar'; +import './group_label_subscription'; +import './groups_select'; +import './header'; +import './importer_status'; +import './issuable'; +import './issuable_context'; +import './issuable_form'; +import './issue'; +import './issue_status_select'; +import './issues_bulk_assignment'; +import './label_manager'; +import './labels'; +import './labels_select'; +import './layout_nav'; +import './line_highlighter'; +import './logo'; +import './member_expiration_date'; +import './members'; +import './merge_request'; +import './merge_request_tabs'; +import './merge_request_widget'; +import './merged_buttons'; +import './milestone'; +import './milestone_select'; +import './mini_pipeline_graph_dropdown'; +import './namespace_select'; +import './new_branch_form'; +import './new_commit_form'; +import './notes'; +import './notifications_dropdown'; +import './notifications_form'; +import './pager'; +import './pipelines'; +import './preview_markdown'; +import './project'; +import './project_avatar'; +import './project_find_file'; +import './project_fork'; +import './project_import'; +import './project_label_subscription'; +import './project_new'; +import './project_select'; +import './project_show'; +import './project_variables'; +import './projects_list'; +import './render_gfm'; +import './render_math'; +import './right_sidebar'; +import './search'; +import './search_autocomplete'; +import './signin_tabs_memoizer'; +import './single_file_diff'; +import './smart_interval'; +import './snippets_list'; +import './star'; +import './subbable_resource'; +import './subscription'; +import './subscription_select'; +import './syntax_highlight'; +import './task_list'; +import './todos'; +import './tree'; +import './user'; +import './user_tabs'; +import './username_validator'; +import './users_select'; +import './version_check_image'; +import './visibility_select'; +import './wikis'; +import './zen_mode'; (function () { document.addEventListener('beforeunload', function () { diff --git a/config/webpack.config.js b/config/webpack.config.js index 7298e7109c6..41d1e317560 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -132,8 +132,7 @@ var config = { extensions: ['.js', '.es6', '.js.es6'], alias: { '~': path.join(ROOT_PATH, 'app/assets/javascripts'), - 'emoji-map$': path.join(ROOT_PATH, 'fixtures/emojis/digests.json'), - 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), + 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'), 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index f4b1d777203..032f4836227 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,8 +1,9 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ -require('es6-promise').polyfill(); +import promisePolyfill from 'es6-promise'; +import AwardsHandler from '~/awards_handler'; -const AwardsHandler = require('~/awards_handler'); +promisePolyfill.polyfill(); (function() { var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index e94e220b19f..7ab0b37f2ec 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -1,16 +1,15 @@ +import '~/extensions/string'; +import '~/extensions/array'; -require('~/extensions/string'); -require('~/extensions/array'); - -const glEmoji = require('~/behaviors/gl_emoji'); - -const glEmojiTag = glEmoji.glEmojiTag; -const isEmojiUnicodeSupported = glEmoji.isEmojiUnicodeSupported; -const isFlagEmoji = glEmoji.isFlagEmoji; -const isKeycapEmoji = glEmoji.isKeycapEmoji; -const isSkinToneComboEmoji = glEmoji.isSkinToneComboEmoji; -const isHorceRacingSkinToneComboEmoji = glEmoji.isHorceRacingSkinToneComboEmoji; -const isPersonZwjEmoji = glEmoji.isPersonZwjEmoji; +import { glEmojiTag } from '~/behaviors/gl_emoji'; +import { + isEmojiUnicodeSupported, + isFlagEmoji, + isKeycapEmoji, + isSkinToneComboEmoji, + isHorceRacingSkinToneComboEmoji, + isPersonZwjEmoji, +} from '~/behaviors/gl_emoji/is_emoji_unicode_supported'; const emptySupportMap = { personZwj: false, -- cgit v1.2.1 From 1c2fd69696130ac06431e89a5240a52da98b424e Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 7 Mar 2017 17:22:10 -0600 Subject: Remove extra space for settings dropdown --- app/assets/stylesheets/framework/nav.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 674d3bb45aa..ea45aaa0253 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -294,7 +294,7 @@ .nav-control { @media (max-width: $screen-sm-max) { - margin-right: 75px; + margin-right: 2px; } } } -- cgit v1.2.1 From 460c2d1c5124b365d413949d5171136f1604b671 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 8 Mar 2017 12:50:19 -0500 Subject: Decrease Capybara timeout for CI environment --- features/support/capybara.rb | 2 +- spec/support/capybara.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/support/capybara.rb b/features/support/capybara.rb index 47372df152d..a5fcbb65131 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -2,7 +2,7 @@ require 'spinach/capybara' require 'capybara/poltergeist' # Give CI some extra time -timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15 +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 16d5f2bf0b8..62740ec29fd 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -3,7 +3,7 @@ require 'capybara/rspec' require 'capybara/poltergeist' # Give CI some extra time -timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| -- cgit v1.2.1 From 289dd49ef3ec6d99639b8a35d3766f424cb2e022 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Wed, 8 Mar 2017 12:14:02 -0600 Subject: Fix inconsistent deploy key documentation in UI Deploy keys were added with write access to https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7383 - We still state " Deploy keys allow read-only access to your repository." in the UI. This updates the deploy key UI information to reflect the docs https://docs.gitlab.com/ce/ssh/README.html#deploy-keys --- app/views/projects/deploy_keys/_index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 0cbe9b3275a..4cfbd9add00 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -3,7 +3,7 @@ %h4.prepend-top-0 Deploy Keys %p - Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. + Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. .col-lg-9 %h5.prepend-top-0 Create a new deploy key for this project -- cgit v1.2.1 From 26fa716dd2c1204b0cf4dc74e4c0a150f565a7ff Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Mon, 6 Mar 2017 06:21:44 +0100 Subject: Fix name colision when importing GitHub pull requests from forked repositories --- lib/gitlab/github_import/pull_request_formatter.rb | 6 +++++- spec/lib/gitlab/github_import/pull_request_formatter_spec.rb | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 4ea0200e89b..e1224d03498 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -38,7 +38,11 @@ module Gitlab def source_branch_name @source_branch_name ||= begin - source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}" + if source_branch.repo.id != target_branch.repo.id + "pull/#{number}/#{source_branch.repo.full_name}/#{source_branch_ref}" + else + source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}" + end end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index eeef23a61c6..a469821251e 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -8,9 +8,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) } + let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') } let(:target_repo) { repository } let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } + let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -205,6 +207,14 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch' end end + + context 'when source branch is from a fork' do + let(:raw_data) { double(base_data.merge(head: forked_branch)) } + + it 'prefixes branch name with pull request number and project with namespace to avoid collision' do + expect(pull_request.source_branch_name).to eq 'pull/1347/company/otherproject/master' + end + end end shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do -- cgit v1.2.1 From 4a0cf269a60945938447160a1ec0aa40889aa24d Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Mon, 6 Mar 2017 06:51:03 +0100 Subject: Changelog --- changelogs/unreleased/29034-fix-github-importer.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/29034-fix-github-importer.yml diff --git a/changelogs/unreleased/29034-fix-github-importer.yml b/changelogs/unreleased/29034-fix-github-importer.yml new file mode 100644 index 00000000000..6d08db3d55d --- /dev/null +++ b/changelogs/unreleased/29034-fix-github-importer.yml @@ -0,0 +1,4 @@ +--- +title: Fix name colision when importing GitHub pull requests from forked repositories +merge_request: 9719 +author: -- cgit v1.2.1 From ce35dcbc81116fa4803fc7bfcd35a3585695c07a Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Wed, 8 Mar 2017 19:16:39 +0100 Subject: Refactor some code --- lib/gitlab/github_import/pull_request_formatter.rb | 8 ++++++-- .../github_import/pull_request_formatter_spec.rb | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index e1224d03498..28812fd0cb9 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -38,8 +38,8 @@ module Gitlab def source_branch_name @source_branch_name ||= begin - if source_branch.repo.id != target_branch.repo.id - "pull/#{number}/#{source_branch.repo.full_name}/#{source_branch_ref}" + if cross_project? + "pull/#{number}/#{source_branch_repo.full_name}/#{source_branch_ref}" else source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}" end @@ -56,6 +56,10 @@ module Gitlab end end + def cross_project? + source_branch.repo.id != target_branch.repo.id + end + private def state diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index a469821251e..951cbea7857 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -281,6 +281,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + describe '#cross_project?' do + context 'when source and target repositories are different' do + let(:raw_data) { double(base_data.merge(head: forked_branch)) } + + it 'returns true' do + expect(pull_request.cross_project?).to eq true + end + end + + context 'when source and target repositories are the same' do + let(:raw_data) { double(base_data.merge(head: source_branch)) } + + it 'returns false' do + expect(pull_request.cross_project?).to eq false + end + end + end + describe '#url' do let(:raw_data) { double(base_data) } -- cgit v1.2.1 From 46214d5e7b0142af67e2f8a5fa4b54c423d3b3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Wed, 8 Mar 2017 13:15:05 -0300 Subject: Improve storage validation after configuration structure update Besides improving the error message to specify what exactly you need to do to solve the error, we now don't skip all storage validations on the test environment, so that you also get a nice error message if you're running tests. Now if conditions are met to skip valitaions (test env or env variable) we still make sure the settings _look_ sane, we just skip verifying the paths exists and meet the given conditions. --- config/initializers/6_validations.rb | 16 +++--- spec/initializers/6_validations_spec.rb | 92 +++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb index abe570f430c..9e24f42d284 100644 --- a/config/initializers/6_validations.rb +++ b/config/initializers/6_validations.rb @@ -13,24 +13,27 @@ def storage_validation_error(message) raise "#{message}. Please fix this in your gitlab.yml before starting GitLab." end -def validate_storages +def validate_storages_config storage_validation_error('No repository storage path defined') if Gitlab.config.repositories.storages.empty? Gitlab.config.repositories.storages.each do |name, repository_storage| storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name) if repository_storage.is_a?(String) - error = "#{name} is not a valid storage, because it has no `path` key. " \ + raise "#{name} is not a valid storage, because it has no `path` key. " \ "It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \ - "Refer to gitlab.yml.example for an updated example" - - storage_validation_error(error) + "For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\n" \ + "If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n" end if !repository_storage.is_a?(Hash) || repository_storage['path'].nil? storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example") end + end +end +def validate_storages_paths + Gitlab.config.repositories.storages.each do |name, repository_storage| parent_name, _parent_path = find_parent_path(name, repository_storage['path']) if parent_name storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages") @@ -38,4 +41,5 @@ def validate_storages end end -validate_storages unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true' +validate_storages_config +validate_storages_paths unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true' diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb index cf182e6d221..374517fec37 100644 --- a/spec/initializers/6_validations_spec.rb +++ b/spec/initializers/6_validations_spec.rb @@ -12,63 +12,77 @@ describe '6_validations', lib: true do FileUtils.rm_rf('tmp/tests/paths') end - context 'with correct settings' do - before do - mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' }) - end + describe 'validate_storages_config' do + context 'with correct settings' do + before do + mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' }) + end - it 'passes through' do - expect { validate_storages }.not_to raise_error + it 'passes through' do + expect { validate_storages_config }.not_to raise_error + end end - end - context 'with invalid storage names' do - before do - mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' }) - end + context 'with invalid storage names' do + before do + mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' }) + end - it 'throws an error' do - expect { validate_storages }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.') + it 'throws an error' do + expect { validate_storages_config }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.') + end end - end - context 'with nested storage paths' do - before do - mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' }) - end + context 'with incomplete settings' do + before do + mock_storages('foo' => {}) + end - it 'throws an error' do - expect { validate_storages }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.') + it 'throws an error suggesting the user to update its settings' do + expect { validate_storages_config }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.') + end end - end - context 'with similar but un-nested storage paths' do - before do - mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' }) - end + context 'with deprecated settings structure' do + before do + mock_storages('foo' => 'tmp/tests/paths/a/b/c') + end - it 'passes through' do - expect { validate_storages }.not_to raise_error + it 'throws an error suggesting the user to update its settings' do + expect { validate_storages_config }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nFor source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\nIf you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n") + end end end - context 'with incomplete settings' do - before do - mock_storages('foo' => {}) - end + describe 'validate_storages_paths' do + context 'with correct settings' do + before do + mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' }) + end - it 'throws an error suggesting the user to update its settings' do - expect { validate_storages }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.') + it 'passes through' do + expect { validate_storages_paths }.not_to raise_error + end end - end - context 'with deprecated settings structure' do - before do - mock_storages('foo' => 'tmp/tests/paths/a/b/c') + context 'with nested storage paths' do + before do + mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' }) + end + + it 'throws an error' do + expect { validate_storages_paths }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.') + end end - it 'throws an error suggesting the user to update its settings' do - expect { validate_storages }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nRefer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.") + context 'with similar but un-nested storage paths' do + before do + mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' }) + end + + it 'passes through' do + expect { validate_storages_paths }.not_to raise_error + end end end -- cgit v1.2.1 From cd0825f6121e2139e8843e22016797aa2bdd5f07 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 8 Mar 2017 14:19:21 -0500 Subject: Get view type based on diff_view Cookie This is because we need to persist the same view type across tabs --- app/assets/javascripts/notes.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index eeab69da941..47cc34e7a20 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,12 +1,14 @@ /* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */ /* global Flash */ /* global Autosave */ +/* global Cookies */ /* global ResolveService */ /* global mrRefreshWidgetUrl */ require('./autosave'); window.autosize = require('vendor/autosize'); window.Dropzone = require('dropzone'); +window.Cookies = require('js-cookie'); require('./dropzone_input'); require('./gfm_auto_complete'); require('vendor/jquery.caret'); // required by jquery.atwho @@ -42,7 +44,6 @@ require('./task_list'); this.notes_url = notes_url; this.note_ids = note_ids; this.last_fetched_at = last_fetched_at; - this.view = view; this.noteable_url = document.URL; this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge")); this.basePollingInterval = 15000; @@ -57,6 +58,7 @@ require('./task_list'); selector: '.notes' }); this.collapseLongCommitList(); + this.setViewType(view); // We are in the Merge Requests page so we need another edit form for Changes tab if (gl.utils.getPagePath(1) === 'merge_requests') { @@ -65,6 +67,10 @@ require('./task_list'); } } + Notes.prototype.setViewType = function(view) { + this.view = Cookies.get('diff_view') || view; + }; + Notes.prototype.addBinding = function() { // add note to UI after creation $(document).on("ajax:success", ".js-main-target-form", this.addNote); @@ -302,7 +308,7 @@ require('./task_list'); }; Notes.prototype.isParallelView = function() { - return this.view === 'parallel'; + return Cookies.get('diff_view') === 'parallel'; }; /* -- cgit v1.2.1 From f92a43c95867e054ef9fe7c6c8f7399261c7bf51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=83=20Winnie=20=F0=9F=92=83?= Date: Wed, 8 Mar 2017 20:26:22 +0000 Subject: Remove timeout from awards_handler_spec.js --- spec/javascripts/awards_handler_spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index f4b1d777203..d8517e4d3d1 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -46,9 +46,6 @@ const AwardsHandler = require('~/awards_handler'); isEmojiMenuBuilt = true; resolve(); }); - - // Fail after 1 second - setTimeout(reject, 1000); } }); }; -- cgit v1.2.1 From 08c141b954af5d83cffe9891ff0d8f62c646ae34 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Mon, 27 Feb 2017 13:05:50 +1100 Subject: respect offset and limit query params for infinite lists --- app/assets/javascripts/lib/utils/url_utility.js | 7 ++ app/assets/javascripts/pager.js | 8 ++- changelogs/unreleased/28030-infinite-offset.yml | 4 ++ spec/javascripts/pager_spec.js | 90 +++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/28030-infinite-offset.yml create mode 100644 spec/javascripts/pager_spec.js diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 1bc81d2e4a4..09c4261b318 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -66,6 +66,13 @@ return results; })()).join('&'); }; + w.gl.utils.removeParams = (params) => { + const url = new URL(window.location.href); + params.forEach((param) => { + url.search = w.gl.utils.removeParamQueryString(url.search, param); + }); + return url.href; + }; w.gl.utils.getLocationHash = function(url) { var hashIndex; if (typeof url === 'undefined') { diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index e35cf6d295e..5f6bc902cf8 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,11 +1,15 @@ +require('~/lib/utils/common_utils'); +require('~/lib/utils/url_utility'); + (() => { const ENDLESS_SCROLL_BOTTOM_PX = 400; const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; const Pager = { init(limit = 0, preload = false, disable = false, callback = $.noop) { + this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']); this.limit = limit; - this.offset = this.limit; + this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit; this.disable = disable; this.callback = callback; this.loading = $('.loading').first(); @@ -20,7 +24,7 @@ this.loading.show(); $.ajax({ type: 'GET', - url: $('.content_list').data('href') || window.location.href, + url: this.url, data: `limit=${this.limit}&offset=${this.offset}`, dataType: 'json', error: () => this.loading.hide(), diff --git a/changelogs/unreleased/28030-infinite-offset.yml b/changelogs/unreleased/28030-infinite-offset.yml new file mode 100644 index 00000000000..6f4082d7684 --- /dev/null +++ b/changelogs/unreleased/28030-infinite-offset.yml @@ -0,0 +1,4 @@ +--- +title: allow offset query parameter for infinite list pages +merge_request: +author: diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js new file mode 100644 index 00000000000..d966226909b --- /dev/null +++ b/spec/javascripts/pager_spec.js @@ -0,0 +1,90 @@ +/* global fixture */ + +require('~/pager'); + +describe('pager', () => { + const Pager = window.Pager; + + it('is defined on window', () => { + expect(window.Pager).toBeDefined(); + }); + + describe('init', () => { + const originalHref = window.location.href; + + beforeEach(() => { + setFixtures('
'); + spyOn($, 'ajax'); + }); + + afterEach(() => { + window.history.replaceState({}, null, originalHref); + }); + + it('should use data-href attribute from list element', () => { + const href = `${gl.TEST_HOST}/some_list.json`; + setFixtures(`
`); + Pager.init(); + expect(Pager.url).toBe(href); + }); + + it('should use current url if data-href attribute not provided', () => { + const href = `${gl.TEST_HOST}/some_list`; + spyOn(gl.utils, 'removeParams').and.returnValue(href); + Pager.init(); + expect(Pager.url).toBe(href); + }); + + it('should get initial offset from query parameter', () => { + window.history.replaceState({}, null, '?offset=100'); + Pager.init(); + expect(Pager.offset).toBe(100); + }); + + it('keeps extra query parameters from url', () => { + window.history.replaceState({}, null, '?filter=test&offset=100'); + const href = `${gl.TEST_HOST}/some_list?filter=test`; + spyOn(gl.utils, 'removeParams').and.returnValue(href); + Pager.init(); + expect(gl.utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']); + expect(Pager.url).toEqual(href); + }); + }); + + describe('getOld', () => { + beforeEach(() => { + setFixtures('
'); + Pager.init(); + }); + + it('shows loader while loading next page', () => { + spyOn(Pager.loading, 'show'); + Pager.getOld(); + expect(Pager.loading.show).toHaveBeenCalled(); + }); + + it('hides loader on success', () => { + spyOn($, 'ajax').and.callFake(options => options.success({})); + spyOn(Pager.loading, 'hide'); + Pager.getOld(); + expect(Pager.loading.hide).toHaveBeenCalled(); + }); + + it('hides loader on error', () => { + spyOn($, 'ajax').and.callFake(options => options.error()); + spyOn(Pager.loading, 'hide'); + Pager.getOld(); + expect(Pager.loading.hide).toHaveBeenCalled(); + }); + + it('sends request to url with offset and limit params', () => { + spyOn($, 'ajax'); + Pager.offset = 100; + Pager.limit = 20; + Pager.getOld(); + const [{ data, url }] = $.ajax.calls.argsFor(0); + expect(data).toBe('limit=20&offset=100'); + expect(url).toBe('/some_list'); + }); + }); +}); -- cgit v1.2.1 From b6b780cbf4c75183e349f63371b42117127fc4d8 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 8 Mar 2017 21:31:29 +0000 Subject: Remove hidden text from subscribe button --- app/views/dashboard/issues.html.haml | 4 +--- spec/features/dashboard_issues_spec.rb | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 9a4e423f896..10867140d4f 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -6,10 +6,8 @@ .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - = link_to params.merge(rss_url_options), class: 'btn' do + = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do = icon('rss') - %span.icon-label - Subscribe = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb index aa75e1140f6..8c61cdebc4b 100644 --- a/spec/features/dashboard_issues_spec.rb +++ b/spec/features/dashboard_issues_spec.rb @@ -48,7 +48,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do it 'updates atom feed link' do visit_issues(milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') + link = find('.nav-controls a[title="Subscribe"]') params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) -- cgit v1.2.1 From 7e1e43405d60d8949556c8964419fb2afec0402f Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 8 Mar 2017 14:49:59 -0800 Subject: Flush out the update guide from 8.17 - 9.0 And bump the stable branch version in the installation from source guide --- doc/install/installation.md | 4 +- doc/update/8.17-to-9.0.md | 235 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+), 2 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index bb4141c6cd3..b6fb7a14a3a 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -288,9 +288,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-17-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-0-stable gitlab -**Note:** You can change `8-17-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `9-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 4cc8be752c4..277d24f989f 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -1,3 +1,163 @@ +# From 8.17 to 9.0 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +We will continue supporting Ruby < 2.3 for the time being but we recommend you +upgrade to Ruby 2.3 if you're running a source installation, as this is the same +version that ships with our Omnibus package. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz +echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +cd ruby-2.3.3 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + + + + +Since GitLab 8.17, GitLab requires the use of yarn yarn >= v0.17.0 to manage +javascript dependencies. + + # install yarn + curl --location https://yarnpkg.com/install.sh | bash - + +More information can be found on the yarnpkg.com website. + + + +### 5. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-0-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-0-stable-ee +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Install/update frontend asset dependencies +sudo -u git -H npm install --production + +# Clean up assets and cache +sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 7. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` + +### 8. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v5.0.0 +``` + +### 9. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/8-17-stable:config/gitlab.yml.example origin/9-0-stable:config/gitlab.yml.example +``` + #### Configuration changes for repository storages This version introduces a new configuration structure for repository storages. @@ -85,3 +245,78 @@ via [/etc/default/gitlab]. [Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache [/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 10. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 11. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.17) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.16 to 8.17](8.16-to-8.17.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/gitlab.yml.example -- cgit v1.2.1 From 65f6abffc4349131ebb75f8ded0f417933c4a50e Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 8 Mar 2017 15:16:47 -0800 Subject: Update the gitignore and gitlab-ci templates for 9.0 --- vendor/gitignore/Android.gitignore | 3 +- vendor/gitignore/Global/Eclipse.gitignore | 5 ++ vendor/gitignore/Global/JetBrains.gitignore | 1 + vendor/gitignore/Global/SBT.gitignore | 3 + vendor/gitignore/Java.gitignore | 1 + vendor/gitignore/Maven.gitignore | 2 +- vendor/gitignore/Node.gitignore | 2 + vendor/gitignore/Objective-C.gitignore | 4 +- vendor/gitignore/PlayFramework.gitignore | 1 + vendor/gitignore/Python.gitignore | 3 + vendor/gitignore/Scala.gitignore | 21 ----- vendor/gitignore/Swift.gitignore | 2 +- vendor/gitignore/Symfony.gitignore | 4 - vendor/gitignore/TeX.gitignore | 9 +-- vendor/gitignore/VisualStudio.gitignore | 11 ++- vendor/gitlab-ci-yml/Android.gitlab-ci.yml | 51 ++++++++++++ vendor/gitlab-ci-yml/Bash.gitlab-ci.yml | 35 ++++++++ vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml | 1 - vendor/gitlab-ci-yml/Django.gitlab-ci.yml | 34 ++++++++ vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml | 7 ++ vendor/gitlab-ci-yml/LICENSE | 2 +- vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml | 78 ++++++++++++++++++ vendor/gitlab-ci-yml/Maven.gitlab-ci.yml | 5 +- vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml | 92 ++++++++++++++++++++++ vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml | 92 ---------------------- vendor/gitlab-ci-yml/PHP.gitlab-ci.yml | 33 ++++++++ vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml | 6 ++ vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml | 14 ++-- .../autodeploy/Kubernetes.gitlab-ci.yml | 3 +- .../autodeploy/OpenShift.gitlab-ci.yml | 3 +- 30 files changed, 388 insertions(+), 140 deletions(-) create mode 100644 vendor/gitlab-ci-yml/Android.gitlab-ci.yml create mode 100644 vendor/gitlab-ci-yml/Bash.gitlab-ci.yml create mode 100644 vendor/gitlab-ci-yml/Django.gitlab-ci.yml create mode 100644 vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml create mode 100644 vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml delete mode 100644 vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml create mode 100644 vendor/gitlab-ci-yml/PHP.gitlab-ci.yml diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore index a1a65c2d72e..520a86352f7 100644 --- a/vendor/gitignore/Android.gitignore +++ b/vendor/gitignore/Android.gitignore @@ -37,6 +37,7 @@ captures/ .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml +.idea/dictionaries .idea/libraries # Keystore files @@ -48,7 +49,7 @@ captures/ # Google Services (e.g. APIs or Firebase) google-services.json -#Freeline +# Freeline freeline.py freeline/ freeline_project_description.json diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore index 31c9fb31167..4f88399d2d8 100644 --- a/vendor/gitignore/Global/Eclipse.gitignore +++ b/vendor/gitignore/Global/Eclipse.gitignore @@ -49,3 +49,8 @@ local.properties # Code Recommenders .recommenders/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore index 401fee15748..ec7e95c6ab5 100644 --- a/vendor/gitignore/Global/JetBrains.gitignore +++ b/vendor/gitignore/Global/JetBrains.gitignore @@ -4,6 +4,7 @@ # User-specific stuff: .idea/**/workspace.xml .idea/**/tasks.xml +.idea/dictionaries # Sensitive or high-churn files: .idea/**/dataSources/ diff --git a/vendor/gitignore/Global/SBT.gitignore b/vendor/gitignore/Global/SBT.gitignore index 970d897c75c..5ed6acb6576 100644 --- a/vendor/gitignore/Global/SBT.gitignore +++ b/vendor/gitignore/Global/SBT.gitignore @@ -1,9 +1,12 @@ # Simple Build Tool # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control +dist/* target/ lib_managed/ src_managed/ project/boot/ +project/plugins/project/ .history .cache +.lib/ diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index dbb4a2dfa1a..6143e53f9e3 100644 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore @@ -1,3 +1,4 @@ +# Compiled class file *.class # Log file diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore index 9af45b175ae..5f2dbe11df9 100644 --- a/vendor/gitignore/Maven.gitignore +++ b/vendor/gitignore/Maven.gitignore @@ -8,5 +8,5 @@ dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties -# Exclude maven wrapper +# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) !/.mvn/wrapper/maven-wrapper.jar diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore index 38ac77e405e..00cbbdf53f6 100644 --- a/vendor/gitignore/Node.gitignore +++ b/vendor/gitignore/Node.gitignore @@ -2,6 +2,8 @@ logs *.log npm-debug.log* +yarn-debug.log* +yarn-error.log* # Runtime data pids diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore index af90c007a3f..09dfede4814 100644 --- a/vendor/gitignore/Objective-C.gitignore +++ b/vendor/gitignore/Objective-C.gitignore @@ -45,10 +45,10 @@ Carthage/Build # fastlane # -# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: -# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md +# https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html diff --git a/vendor/gitignore/PlayFramework.gitignore b/vendor/gitignore/PlayFramework.gitignore index 6d67f119175..ae5ec9fe1d9 100644 --- a/vendor/gitignore/PlayFramework.gitignore +++ b/vendor/gitignore/PlayFramework.gitignore @@ -5,6 +5,7 @@ bin/ /lib/ /logs/ /modules +/project/project /project/target /target tmp/ diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore index cf3102d6b00..62c1e736924 100644 --- a/vendor/gitignore/Python.gitignore +++ b/vendor/gitignore/Python.gitignore @@ -76,6 +76,9 @@ target/ # celery beat schedule file celerybeat-schedule +# SageMath parsed files +*.sage.py + # dotenv .env diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore index 006a7b247fe..9c07d4ae988 100644 --- a/vendor/gitignore/Scala.gitignore +++ b/vendor/gitignore/Scala.gitignore @@ -1,23 +1,2 @@ *.class *.log - -# sbt specific -.cache -.history -.lib/ -dist/* -target/ -lib_managed/ -src_managed/ -project/boot/ -project/plugins/project/ - -# Scala-IDE specific -.ensime -.ensime_cache/ -.scala_dependencies -.worksheet - -# ENSIME specific -.ensime_cache/ -.ensime diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore index 099d22ae2f4..d5340449396 100644 --- a/vendor/gitignore/Swift.gitignore +++ b/vendor/gitignore/Swift.gitignore @@ -59,7 +59,7 @@ Carthage/Build # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: -# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md +# https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore index ed4d3c6c28d..6c224e024e9 100644 --- a/vendor/gitignore/Symfony.gitignore +++ b/vendor/gitignore/Symfony.gitignore @@ -25,7 +25,6 @@ /bin/* !bin/console !bin/symfony_requirements -/vendor/ # Assets and user uploads /web/bundles/ @@ -38,8 +37,5 @@ # Build data /build/ -# Composer PHAR -/composer.phar - # Backup entities generated with doctrine:generate:entities command **/Entity/*~ diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index 69bfb1eec3e..57ed9f5d972 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -28,7 +28,6 @@ *.blg *-blx.aux *-blx.bib -*.brf *.run.xml ## Build tool auxiliary files: @@ -77,8 +76,6 @@ acs-*.bib *.t[1-9] *.t[1-9][0-9] *.tfm -*.[1-9] -*.[1-9][0-9] #(r)(e)ledmac/(r)(e)ledpar *.end @@ -134,6 +131,9 @@ acs-*.bib *.mlf *.mlt *.mtc[0-9]* +*.slf[0-9]* +*.slt[0-9]* +*.stc[0-9]* # minted _minted* @@ -142,9 +142,6 @@ _minted* # morewrites *.mw -# mylatexformat -*.fmt - # nomencl *.nlo diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 8054980d742..a752eacca7d 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -166,7 +166,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files +# NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets @@ -276,3 +276,12 @@ __pycache__/ # Cake - Uncomment if you are using it # tools/** # !tools/packages.config + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs \ No newline at end of file diff --git a/vendor/gitlab-ci-yml/Android.gitlab-ci.yml b/vendor/gitlab-ci-yml/Android.gitlab-ci.yml new file mode 100644 index 00000000000..5f9d54ff574 --- /dev/null +++ b/vendor/gitlab-ci-yml/Android.gitlab-ci.yml @@ -0,0 +1,51 @@ +# Read more about this script on this blog post https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/, by Greyson Parrelli +image: openjdk:8-jdk + +variables: + ANDROID_COMPILE_SDK: "25" + ANDROID_BUILD_TOOLS: "24.0.0" + ANDROID_SDK_TOOLS: "24.4.1" + +before_script: + - apt-get --quiet update --yes + - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 + - wget --quiet --output-document=android-sdk.tgz https://dl.google.com/android/android-sdk_r${ANDROID_SDK_TOOLS}-linux.tgz + - tar --extract --gzip --file=android-sdk.tgz + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter android-${ANDROID_COMPILE_SDK} + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter platform-tools + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter build-tools-${ANDROID_BUILD_TOOLS} + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-android-m2repository + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-google_play_services + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-m2repository + - export ANDROID_HOME=$PWD/android-sdk-linux + - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ + - chmod +x ./gradlew + +stages: + - build + - test + +build: + stage: build + script: + - ./gradlew assembleDebug + artifacts: + paths: + - app/build/outputs/ + +unitTests: + stage: test + script: + - ./gradlew test + +functionalTests: + stage: test + script: + - wget --quiet --output-document=android-wait-for-emulator https://raw.githubusercontent.com/travis-ci/travis-cookbooks/0f497eb71291b52a703143c5cd63a217c8766dc9/community-cookbooks/android-sdk/files/default/android-wait-for-emulator + - chmod +x android-wait-for-emulator + - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter sys-img-x86-google_apis-${ANDROID_COMPILE_SDK} + - echo no | android-sdk-linux/tools/android create avd -n test -t android-${ANDROID_COMPILE_SDK} --abi google_apis/x86 + - android-sdk-linux/tools/emulator64-x86 -avd test -no-window -no-audio & + - ./android-wait-for-emulator + - adb shell input keyevent 82 + - ./gradlew cAT diff --git a/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml new file mode 100644 index 00000000000..27537689b80 --- /dev/null +++ b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml @@ -0,0 +1,35 @@ +# see https://docs.gitlab.com/ce/ci/yaml/README.html for all available options + +# you can delete this line if you're not using Docker +image: busybox:latest + +before_script: + - echo "Before script section" + - echo "For example you might run an update here or install a build dependency" + - echo "Or perhaps you might print out some debugging details" + +after_script: + - echo "After script section" + - echo "For example you might do some cleanup here" + +build1: + stage: build + script: + - echo "Do your build here" + +test1: + stage: test + script: + - echo "Do a test here" + - echo "For example run a test suite" + +test2: + stage: test + script: + - echo "Do another parallel test here" + - echo "For example run a lint test" + +deploy1: + stage: deploy + script: + - echo "Do your deploy here" \ No newline at end of file diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml index e8da49a935e..37e44735f7c 100644 --- a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml @@ -1,4 +1,3 @@ -# This file is a template, and might need editing before it works on your project. # Official language image. Look for the different tagged releases at: # https://hub.docker.com/r/crystallang/crystal/ image: "crystallang/crystal:latest" diff --git a/vendor/gitlab-ci-yml/Django.gitlab-ci.yml b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml new file mode 100644 index 00000000000..b3106863cca --- /dev/null +++ b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml @@ -0,0 +1,34 @@ +# Official framework image. Look for the different tagged releases at: +# https://hub.docker.com/r/library/python +image: python:latest + +# Pick zero or more services to be used on all builds. +# Only needed when using a docker container to run your tests in. +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +services: + - mysql:latest + - postgres:latest + +variables: + POSTGRES_DB: database_name + +# This folder is cached between builds +# http://docs.gitlab.com/ce/ci/yaml/README.html#cache +cache: + paths: + - ~/.cache/pip/ + +# This is a basic example for a gem or script which doesn't use +# services such as redis or postgres +before_script: + - python -V # Print out python version for debugging + # Uncomment next line if your Django app needs a JS runtime: + # - apt-get update -q && apt-get install nodejs -yqq + - pip install -r requirements.txt + +test: + variables: + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB" + script: + - python manage.py migrate + - python manage.py test diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml index 98d3039ad06..a65e48a3389 100644 --- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml @@ -6,6 +6,13 @@ # https://github.com/gradle/gradle image: java:8 +# Disable the Gradle daemon for Continuous Integration servers as correctness +# is usually a priority over speed in CI environments. Using a fresh +# runtime for each build is more reliable since the runtime is completely +# isolated from any previous builds. +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + # Make the gradle wrapper executable. This essentially downloads a copy of # Gradle to build the project with. # https://docs.gradle.org/current/userguide/gradle_wrapper.html diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE index 80f7b87b6c0..d6c93c6fcf7 100644 --- a/vendor/gitlab-ci-yml/LICENSE +++ b/vendor/gitlab-ci-yml/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 GitLab.org +Copyright (c) 2016-2017 GitLab.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml new file mode 100644 index 00000000000..0d6a6eddc97 --- /dev/null +++ b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml @@ -0,0 +1,78 @@ +# Official framework image. Look for the different tagged releases at: +# https://hub.docker.com/r/library/php +image: php:latest + +# Pick zero or more services to be used on all builds. +# Only needed when using a docker container to run your tests in. +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +services: + - mysql:latest + +variables: + MYSQL_DATABASE: project_name + MYSQL_ROOT_PASSWORD: secret + +# This folder is cached between builds +# http://docs.gitlab.com/ce/ci/yaml/README.html#cache +cache: + paths: + - vendor/ + - node_modules/ + +# This is a basic example for a gem or script which doesn't use +# services such as redis or postgres +before_script: + # Update packages + - apt-get update -yqq + + # Upgrade to Node 7 + - curl -sL https://deb.nodesource.com/setup_7.x | bash - + + # Install dependencies + - apt-get install git nodejs libcurl4-gnutls-dev libicu-dev libmcrypt-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libpq-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev -yqq + + # Install php extensions + - docker-php-ext-install mbstring mcrypt pdo_mysql curl json intl gd xml zip bz2 opcache + + # Install Composer and project dependencies. + - curl -sS https://getcomposer.org/installer | php + - php composer.phar install + + # Install Node dependencies. + # comment this out if you don't have a node dependency + - npm install + + # Copy over testing configuration. + # Don't forget to set the database config in .env.testing correctly + # DB_HOST=mysql + # DB_DATABASE=project_name + # DB_USERNAME=root + # DB_PASSWORD=secret + - cp .env.testing .env + + # Run npm build + # comment this out if you don't have a frontend build + # you can change this to to your frontend building script like + # npm run build + - npm run dev + + # Generate an application key. Re-cache. + - php artisan key:generate + - php artisan config:cache + + # Run database migrations. + - php artisan migrate + + # Run database seed + - php artisan db:seed + +test: + script: + # run laravel tests + - php vendor/bin/phpunit --coverage-text --colors=never + + # run frontend tests + # if you have any task for testing frontend + # set it in your package.json script + # comment this out if you don't have a frontend test + - npm test diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml index 1678a47f9ac..b75f0665bee 100644 --- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml @@ -17,16 +17,17 @@ variables: # This will supress any download for dependencies and plugins or upload messages which would clutter the console log. # `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work. - MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true" + MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true" # As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used # when running from the command line. # `installAtEnd` and `deployAtEnd`are only effective with recent version of the corresponding plugins. MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true" # Cache downloaded dependencies and plugins between builds. +# To keep cache across branches add 'key: "$CI_BUILD_REF_NAME"' cache: paths: - - /root/.m2/repository/ + - .m2/repository # This will only validate and compile stuff and run e.g. maven-enforcer-plugin. # Because some enforcer rules might check dependency convergence and class duplications diff --git a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml new file mode 100644 index 00000000000..6b6c405a507 --- /dev/null +++ b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml @@ -0,0 +1,92 @@ +image: ayufan/openshift-cli + +stages: + - test + - review + - staging + - production + - cleanup + +variables: + OPENSHIFT_SERVER: openshift.default.svc.cluster.local + # OPENSHIFT_DOMAIN: apps.example.com + # Configure this variable in Secure Variables: + # OPENSHIFT_TOKEN: my.openshift.token + +test1: + stage: test + before_script: [] + script: + - echo run tests + +test2: + stage: test + before_script: [] + script: + - echo run tests + +.deploy: &deploy + before_script: + - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify + - oc project "$CI_PROJECT_NAME-$CI_PROJECT_ID" 2> /dev/null || oc new-project "$CI_PROJECT_NAME-$CI_PROJECT_ID" + script: + - "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker" + - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow" + - "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST" + +review: + <<: *deploy + stage: review + variables: + APP: $CI_BUILD_REF_NAME + APP_HOST: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN + environment: + name: review/$CI_BUILD_REF_NAME + url: http://$CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN + on_stop: stop-review + only: + - branches + except: + - master + +stop-review: + <<: *deploy + stage: cleanup + script: + - oc delete all -l "app=$APP" + when: manual + variables: + APP: $CI_BUILD_REF_NAME + GIT_STRATEGY: none + environment: + name: review/$CI_BUILD_REF_NAME + action: stop + only: + - branches + except: + - master + +staging: + <<: *deploy + stage: staging + variables: + APP: staging + APP_HOST: $CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN + environment: + name: staging + url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN + only: + - master + +production: + <<: *deploy + stage: production + variables: + APP: production + APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN + when: manual + environment: + name: production + url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN + only: + - master diff --git a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml deleted file mode 100644 index 2ba5cad9682..00000000000 --- a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml +++ /dev/null @@ -1,92 +0,0 @@ -# This file is a template, and might need editing before it works on your project. -image: ayufan/openshift-cli - -stages: - - test - - review - - staging - - production - -variables: - OPENSHIFT_SERVER: openshift.default.svc.cluster.local - # OPENSHIFT_DOMAIN: apps.example.com - # Configure this variable in Secure Variables: - # OPENSHIFT_TOKEN: my.openshift.token - -test1: - stage: test - before_script: [] - script: - - echo run tests - -test2: - stage: test - before_script: [] - script: - - echo run tests - -.deploy: &deploy - before_script: - - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify - - oc project "$CI_PROJECT_NAME" 2> /dev/null || oc new-project "$CI_PROJECT_NAME" - script: - - "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker" - - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow" - - "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST" - -review: - <<: *deploy - stage: review - variables: - APP: $CI_BUILD_REF_NAME - APP_HOST: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN - environment: - name: review/$CI_BUILD_REF_NAME - url: http://$CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN - on_stop: stop-review - only: - - branches - except: - - master - -stop-review: - <<: *deploy - stage: review - script: - - oc delete all -l "app=$APP" - when: manual - variables: - APP: $CI_BUILD_REF_NAME - GIT_STRATEGY: none - environment: - name: review/$CI_BUILD_REF_NAME - action: stop - only: - - branches - except: - - master - -staging: - <<: *deploy - stage: staging - variables: - APP: staging - APP_HOST: $CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN - environment: - name: staging - url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN - only: - - master - -production: - <<: *deploy - stage: production - variables: - APP: production - APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN - when: manual - environment: - name: production - url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN - only: - - master diff --git a/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml new file mode 100644 index 00000000000..bb8caa49d6b --- /dev/null +++ b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml @@ -0,0 +1,33 @@ +# Select image from https://hub.docker.com/_/php/ +image: php:7.1.1 + +# Select what we should cache between builds +cache: + paths: + - vendor/ + +before_script: +- apt-get update -yqq +- apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev +# Install PHP extensions +- docker-php-ext-install mbstring mcrypt pdo_pgsql curl json intl gd xml zip bz2 opcache +# Install and run Composer +- curl -sS https://getcomposer.org/installer | php +- php composer.phar install + +# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service +# See http://docs.gitlab.com/ce/ci/services/README.html for examples. +services: + - mysql:5.7 + +# Set any variables we need +variables: + # Configure mysql environment variables (https://hub.docker.com/r/_/mysql/) + MYSQL_DATABASE: mysql_database + MYSQL_ROOT_PASSWORD: mysql_strong_password + +# Run our tests +# If Xdebug was installed you can generate a coverage report and see code coverage metrics. +test: + script: + - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never \ No newline at end of file diff --git a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml index 45df6975259..a72b8281401 100644 --- a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml @@ -9,3 +9,9 @@ pages: - public only: - master + +test: + script: + - hugo + except: + - master diff --git a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml index 36918fc005a..d98cf94d635 100644 --- a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml @@ -1,11 +1,15 @@ -# Full project: https://gitlab.com/pages/jekyll +# Template project: https://gitlab.com/pages/jekyll +# Docs: https://docs.gitlab.com/ce/pages/ +# Jekyll version: 3.4.0 image: ruby:2.3 +before_script: +- bundle install + test: stage: test script: - - gem install jekyll - - jekyll build -d test + - bundle exec jekyll build -d test artifacts: paths: - test @@ -15,10 +19,10 @@ test: pages: stage: deploy script: - - gem install jekyll - - jekyll build -d public + - bundle exec jekyll build -d public artifacts: paths: - public only: - master + \ No newline at end of file diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml index 7298ea73bab..574f9365f14 100644 --- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml @@ -12,6 +12,7 @@ stages: - review - staging - production + - cleanup build: stage: build @@ -61,7 +62,7 @@ review: - master stop_review: - stage: review + stage: cleanup variables: GIT_STRATEGY: none script: diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml index 249adbc9f4a..4d6f4e00ebb 100644 --- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml @@ -12,6 +12,7 @@ stages: - review - staging - production + - cleanup build: stage: build @@ -61,7 +62,7 @@ review: - master stop_review: - stage: review + stage: cleanup variables: GIT_STRATEGY: none script: -- cgit v1.2.1 From e4551749c09883ff5a9cd793a77fb3bbfa08f409 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 7 Mar 2017 15:45:03 -0600 Subject: Only check new migrations in 'rake down_timecheck' --- lib/tasks/gitlab/db.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index ecf6b6e068b..5476438b8fa 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -62,7 +62,7 @@ namespace :gitlab do ref = Shellwords.escape(args[:ref]) - migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines + migrations = `git diff #{ref}.. --diff-filter=A --name-only -- db/migrate`.lines .map { |file| Rails.root.join(file.strip).to_s } .select { |file| File.file?(file) } -- cgit v1.2.1 From e3b640cab1df6cf7289b01c431d0e3bacc3d127c Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 8 Mar 2017 15:53:19 -0800 Subject: Add the generated license.csv for 9.0 --- licenses.csv | 945 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 945 insertions(+) create mode 100644 licenses.csv diff --git a/licenses.csv b/licenses.csv new file mode 100644 index 00000000000..a2cbef126ad --- /dev/null +++ b/licenses.csv @@ -0,0 +1,945 @@ +RedCloth,4.3.2,MIT +abbrev,1.0.9,ISC +accepts,1.3.3,MIT +ace-rails-ap,4.1.0,MIT +acorn,4.0.4,MIT +acorn-dynamic-import,2.0.1,MIT +acorn-jsx,3.0.1,MIT +actionmailer,4.2.8,MIT +actionpack,4.2.8,MIT +actionview,4.2.8,MIT +activejob,4.2.8,MIT +activemodel,4.2.8,MIT +activerecord,4.2.8,MIT +activesupport,4.2.8,MIT +acts-as-taggable-on,4.0.0,MIT +addressable,2.3.8,Apache 2.0 +after,0.8.2,MIT +after_commit_queue,1.3.0,MIT +ajv,4.11.2,MIT +ajv-keywords,1.5.1,MIT +akismet,2.0.0,MIT +align-text,0.1.4,MIT +allocations,1.0.5,MIT +amdefine,1.0.1,BSD-3-Clause OR MIT +ansi-escapes,1.4.0,MIT +ansi-html,0.0.7,Apache 2.0 +ansi-regex,2.1.1,MIT +ansi-styles,2.2.1,MIT +anymatch,1.3.0,ISC +append-transform,0.4.0,MIT +aproba,1.1.0,ISC +are-we-there-yet,1.1.2,ISC +arel,6.0.4,MIT +argparse,1.0.9,MIT +arr-diff,2.0.0,MIT +arr-flatten,1.0.1,MIT +array-find,1.0.0,MIT +array-flatten,1.1.1,MIT +array-slice,0.2.3,MIT +array-union,1.0.2,MIT +array-uniq,1.0.3,MIT +array-unique,0.2.1,MIT +arraybuffer.slice,0.0.6,MIT +arrify,1.0.1,MIT +asana,0.4.0,MIT +asciidoctor,1.5.3,MIT +asciidoctor-plantuml,0.0.7,MIT +asn1,0.2.3,MIT +asn1.js,4.9.1,MIT +assert,1.4.1,MIT +assert-plus,0.2.0,MIT +async,0.2.10,MIT +async-each,1.0.1,MIT +asynckit,0.4.0,MIT +attr_encrypted,3.0.3,MIT +attr_required,1.0.0,MIT +autoparse,0.3.3,Apache 2.0 +autoprefixer-rails,6.2.3,MIT +aws-sign2,0.6.0,Apache 2.0 +aws4,1.6.0,MIT +axiom-types,0.1.1,MIT +babel-code-frame,6.22.0,MIT +babel-core,6.23.1,MIT +babel-generator,6.23.0,MIT +babel-helper-bindify-decorators,6.22.0,MIT +babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT +babel-helper-call-delegate,6.22.0,MIT +babel-helper-define-map,6.23.0,MIT +babel-helper-explode-assignable-expression,6.22.0,MIT +babel-helper-explode-class,6.22.0,MIT +babel-helper-function-name,6.23.0,MIT +babel-helper-get-function-arity,6.22.0,MIT +babel-helper-hoist-variables,6.22.0,MIT +babel-helper-optimise-call-expression,6.23.0,MIT +babel-helper-regex,6.22.0,MIT +babel-helper-remap-async-to-generator,6.22.0,MIT +babel-helper-replace-supers,6.23.0,MIT +babel-helpers,6.23.0,MIT +babel-loader,6.2.10,MIT +babel-messages,6.23.0,MIT +babel-plugin-check-es2015-constants,6.22.0,MIT +babel-plugin-istanbul,4.0.0,New BSD +babel-plugin-syntax-async-functions,6.13.0,MIT +babel-plugin-syntax-async-generators,6.13.0,MIT +babel-plugin-syntax-class-properties,6.13.0,MIT +babel-plugin-syntax-decorators,6.13.0,MIT +babel-plugin-syntax-dynamic-import,6.18.0,MIT +babel-plugin-syntax-exponentiation-operator,6.13.0,MIT +babel-plugin-syntax-object-rest-spread,6.13.0,MIT +babel-plugin-syntax-trailing-function-commas,6.22.0,MIT +babel-plugin-transform-async-generator-functions,6.22.0,MIT +babel-plugin-transform-async-to-generator,6.22.0,MIT +babel-plugin-transform-class-properties,6.23.0,MIT +babel-plugin-transform-decorators,6.22.0,MIT +babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT +babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT +babel-plugin-transform-es2015-block-scoping,6.23.0,MIT +babel-plugin-transform-es2015-classes,6.23.0,MIT +babel-plugin-transform-es2015-computed-properties,6.22.0,MIT +babel-plugin-transform-es2015-destructuring,6.23.0,MIT +babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT +babel-plugin-transform-es2015-for-of,6.23.0,MIT +babel-plugin-transform-es2015-function-name,6.22.0,MIT +babel-plugin-transform-es2015-literals,6.22.0,MIT +babel-plugin-transform-es2015-modules-amd,6.22.0,MIT +babel-plugin-transform-es2015-modules-commonjs,6.23.0,MIT +babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT +babel-plugin-transform-es2015-modules-umd,6.23.0,MIT +babel-plugin-transform-es2015-object-super,6.22.0,MIT +babel-plugin-transform-es2015-parameters,6.23.0,MIT +babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT +babel-plugin-transform-es2015-spread,6.22.0,MIT +babel-plugin-transform-es2015-sticky-regex,6.22.0,MIT +babel-plugin-transform-es2015-template-literals,6.22.0,MIT +babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT +babel-plugin-transform-es2015-unicode-regex,6.22.0,MIT +babel-plugin-transform-exponentiation-operator,6.22.0,MIT +babel-plugin-transform-object-rest-spread,6.23.0,MIT +babel-plugin-transform-regenerator,6.22.0,MIT +babel-plugin-transform-strict-mode,6.22.0,MIT +babel-preset-es2015,6.22.0,MIT +babel-preset-stage-2,6.22.0,MIT +babel-preset-stage-3,6.22.0,MIT +babel-register,6.23.0,MIT +babel-runtime,6.22.0,MIT +babel-template,6.23.0,MIT +babel-traverse,6.23.1,MIT +babel-types,6.23.0,MIT +babosa,1.0.2,MIT +babylon,6.15.0,MIT +backo2,1.0.2,MIT +balanced-match,0.4.2,MIT +base32,0.3.2,MIT +base64-arraybuffer,0.1.5,MIT +base64-js,1.2.0,MIT +base64id,1.0.0,MIT +batch,0.5.3,MIT +bcrypt,3.1.11,MIT +bcrypt-pbkdf,1.0.1,New BSD +better-assert,1.0.2,MIT +big.js,3.1.3,MIT +binary-extensions,1.8.0,MIT +bindata,2.3.5,ruby +blob,0.0.4,unknown +block-stream,0.0.9,ISC +bluebird,3.4.7,MIT +bn.js,4.11.6,MIT +body-parser,1.16.0,MIT +boom,2.10.1,New BSD +bootstrap-sass,3.3.6,MIT +brace-expansion,1.1.6,MIT +braces,1.8.5,MIT +brorand,1.0.7,MIT +browser,2.2.0,MIT +browserify-aes,1.0.6,MIT +browserify-cipher,1.0.0,MIT +browserify-des,1.0.0,MIT +browserify-rsa,4.0.1,MIT +browserify-sign,4.0.0,ISC +browserify-zlib,0.1.4,MIT +buffer,4.9.1,MIT +buffer-shims,1.0.0,MIT +buffer-xor,1.0.3,MIT +builder,3.2.3,MIT +builtin-modules,1.1.1,MIT +builtin-status-codes,3.0.0,MIT +bytes,2.4.0,MIT +caller-path,0.1.0,MIT +callsite,1.0.0,unknown +callsites,0.2.0,MIT +camelcase,1.2.1,MIT +carrierwave,0.11.2,MIT +caseless,0.11.0,Apache 2.0 +cause,0.1,MIT +center-align,0.1.3,MIT +chalk,1.1.3,MIT +charlock_holmes,0.7.3,MIT +chokidar,1.6.1,MIT +chronic,0.10.2,MIT +chronic_duration,0.10.6,MIT +chunky_png,1.3.5,MIT +cipher-base,1.0.3,MIT +circular-json,0.3.1,MIT +cli-cursor,1.0.2,MIT +cli-width,2.1.0,ISC +cliui,2.1.0,ISC +clone,1.0.2,MIT +co,4.6.0,MIT +code-point-at,1.1.0,MIT +coercible,1.0.0,MIT +coffee-rails,4.1.1,MIT +coffee-script,2.4.1,MIT +coffee-script-source,1.10.0,MIT +colors,1.1.2,MIT +combine-lists,1.0.1,MIT +combined-stream,1.0.5,MIT +commander,2.9.0,MIT +commondir,1.0.1,MIT +component-bind,1.0.0,unknown +component-emitter,1.2.1,MIT +component-inherit,0.0.3,unknown +compressible,2.0.9,MIT +compression,1.6.2,MIT +compression-webpack-plugin,0.3.2,MIT +concat-map,0.0.1,MIT +concat-stream,1.6.0,MIT +concurrent-ruby,1.0.4,MIT +connect,3.5.0,MIT +connect-history-api-fallback,1.3.0,MIT +connection_pool,2.2.1,MIT +console-browserify,1.1.0,MIT +console-control-strings,1.1.0,ISC +constants-browserify,1.0.0,MIT +contains-path,0.1.0,MIT +content-disposition,0.5.2,MIT +content-type,1.0.2,MIT +convert-source-map,1.3.0,MIT +cookie,0.3.1,MIT +cookie-signature,1.0.6,MIT +core-js,2.4.1,MIT +core-util-is,1.0.2,MIT +crack,0.4.3,MIT +create-ecdh,4.0.0,MIT +create-hash,1.1.2,MIT +create-hmac,1.1.4,MIT +creole,0.5.0,ruby +cryptiles,2.0.5,New BSD +crypto-browserify,3.11.0,MIT +css_parser,1.4.1,MIT +custom-event,1.0.1,MIT +d,0.1.1,MIT +d3,3.5.11,New BSD +d3_rails,3.5.11,MIT +dashdash,1.14.1,MIT +date-now,0.1.4,MIT +debug,2.6.0,MIT +decamelize,1.2.0,MIT +deckar01-task_list,1.0.6,MIT +deep-extend,0.4.1,MIT +deep-is,0.1.3,MIT +default-require-extensions,1.0.0,MIT +default_value_for,3.0.2,MIT +defaults,1.0.3,MIT +del,2.2.2,MIT +delayed-stream,1.0.0,MIT +delegates,1.0.0,MIT +depd,1.1.0,MIT +des.js,1.0.0,MIT +descendants_tracker,0.0.4,MIT +destroy,1.0.4,MIT +detect-indent,4.0.0,MIT +devise,4.2.0,MIT +devise-two-factor,3.0.0,MIT +di,0.0.1,MIT +diff-lcs,1.2.5,"MIT,Perl Artistic v2,GNU GPL v2" +diffie-hellman,5.0.2,MIT +diffy,3.1.0,MIT +doctrine,1.5.0,BSD +document-register-element,1.3.0,MIT +dom-serialize,2.2.1,MIT +domain-browser,1.1.7,MIT +domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0" +doorkeeper,4.2.0,MIT +doorkeeper-openid_connect,1.1.2,MIT +dropzone,4.2.0,MIT +dropzonejs-rails,0.7.2,MIT +duplexer,0.1.1,MIT +ecc-jsbn,0.1.1,MIT +ee-first,1.1.1,MIT +ejs,2.5.6,Apache 2.0 +elliptic,6.3.3,MIT +email_reply_trimmer,0.1.6,MIT +emoji-unicode-version,0.2.1,MIT +emojis-list,2.1.0,MIT +encodeurl,1.0.1,MIT +encryptor,3.0.0,MIT +engine.io,1.8.2,MIT +engine.io-client,1.8.2,MIT +engine.io-parser,1.3.2,MIT +enhanced-resolve,3.1.0,MIT +ent,2.2.0,MIT +equalizer,0.0.11,MIT +errno,0.1.4,MIT +error-ex,1.3.0,MIT +erubis,2.7.0,MIT +es5-ext,0.10.12,MIT +es6-iterator,2.0.0,MIT +es6-map,0.1.4,MIT +es6-promise,4.0.5,MIT +es6-set,0.1.4,MIT +es6-symbol,3.1.0,MIT +es6-weak-map,2.0.1,MIT +escape-html,1.0.3,MIT +escape-string-regexp,1.0.5,MIT +escape_utils,1.1.1,MIT +escodegen,1.8.1,Simplified BSD +escope,3.6.0,Simplified BSD +eslint,3.15.0,MIT +eslint-config-airbnb-base,10.0.1,MIT +eslint-import-resolver-node,0.2.3,MIT +eslint-import-resolver-webpack,0.8.1,MIT +eslint-module-utils,2.0.0,MIT +eslint-plugin-filenames,1.1.0,MIT +eslint-plugin-import,2.2.0,MIT +eslint-plugin-jasmine,2.2.0,MIT +espree,3.4.0,Simplified BSD +esprima,3.1.3,Simplified BSD +esrecurse,4.1.0,Simplified BSD +estraverse,4.1.1,Simplified BSD +esutils,2.0.2,BSD +etag,1.7.0,MIT +eve-raphael,0.5.0,Apache 2.0 +event-emitter,0.3.4,MIT +eventemitter3,1.2.0,MIT +events,1.1.1,MIT +eventsource,0.1.6,MIT +evp_bytestokey,1.0.0,MIT +excon,0.52.0,MIT +execjs,2.6.0,MIT +exit-hook,1.1.1,MIT +expand-braces,0.1.2,MIT +expand-brackets,0.1.5,MIT +expand-range,1.8.2,MIT +express,4.14.1,MIT +expression_parser,0.9.0,MIT +extend,3.0.0,MIT +extglob,0.3.2,MIT +extlib,0.9.16,MIT +extract-zip,1.5.0,Simplified BSD +extsprintf,1.0.2,MIT +faraday,0.9.2,MIT +faraday_middleware,0.10.0,MIT +faraday_middleware-multi_json,0.0.6,MIT +fast-levenshtein,2.0.6,MIT +faye-websocket,0.10.0,MIT +fd-slicer,1.0.1,MIT +ffi,1.9.10,BSD +figures,1.7.0,MIT +file-entry-cache,2.0.0,MIT +filename-regex,2.0.0,MIT +fileset,2.0.3,MIT +filesize,3.5.4,New BSD +fill-range,2.2.3,MIT +finalhandler,0.5.1,MIT +find-cache-dir,0.1.1,MIT +find-root,0.1.2,MIT +find-up,2.1.0,MIT +flat-cache,1.2.2,MIT +flowdock,0.7.1,MIT +fog-aws,0.11.0,MIT +fog-core,1.42.0,MIT +fog-google,0.5.0,MIT +fog-json,1.0.2,MIT +fog-local,0.3.0,MIT +fog-openstack,0.1.6,MIT +fog-rackspace,0.1.1,MIT +fog-xml,0.1.2,MIT +font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License" +for-in,0.1.6,MIT +for-own,0.1.4,MIT +forever-agent,0.6.1,Apache 2.0 +form-data,2.1.2,MIT +formatador,0.2.5,MIT +forwarded,0.1.0,MIT +fresh,0.3.0,MIT +fs-extra,1.0.0,MIT +fs.realpath,1.0.0,ISC +fsevents,,unknown +fstream,1.0.10,ISC +fstream-ignore,1.0.5,ISC +function-bind,1.1.0,MIT +gauge,2.7.2,ISC +gemnasium-gitlab-service,0.2.6,MIT +gemojione,3.0.1,MIT +generate-function,2.0.0,MIT +generate-object-property,1.2.0,MIT +get-caller-file,1.0.2,ISC +get_process_mem,0.2.0,MIT +getpass,0.1.6,MIT +gitaly,0.2.1,MIT +github-linguist,4.7.6,MIT +github-markup,1.4.0,MIT +gitlab-flowdock-git-hook,1.0.1,MIT +gitlab-grit,2.8.1,MIT +gitlab-markup,1.5.1,MIT +gitlab_omniauth-ldap,1.2.1,MIT +glob,7.1.1,ISC +glob-base,0.3.0,MIT +glob-parent,2.0.0,ISC +globalid,0.3.7,MIT +globals,9.14.0,MIT +globby,5.0.0,MIT +gollum-grit_adapter,1.0.1,MIT +gollum-lib,4.2.1,MIT +gollum-rugged_adapter,0.4.2,MIT +gon,6.1.0,MIT +google-api-client,0.8.7,Apache 2.0 +google-protobuf,3.2.0,New BSD +googleauth,0.5.1,Apache 2.0 +graceful-fs,4.1.11,ISC +graceful-readlink,1.0.1,MIT +grape,0.19.1,MIT +grape-entity,0.6.0,MIT +grpc,1.1.2,New BSD +gzip-size,3.0.0,MIT +hamlit,2.6.1,MIT +handle-thing,1.2.5,MIT +handlebars,4.0.6,MIT +har-validator,2.0.6,ISC +has,1.0.1,MIT +has-ansi,2.0.0,MIT +has-binary,0.1.7,MIT +has-cors,1.1.0,MIT +has-flag,1.0.0,MIT +has-unicode,2.0.1,ISC +hash.js,1.0.3,MIT +hasha,2.2.0,MIT +hashie,3.5.5,MIT +hawk,3.1.3,New BSD +health_check,2.6.0,MIT +hipchat,1.5.2,MIT +hoek,2.16.3,New BSD +home-or-tmp,2.0.0,MIT +hosted-git-info,2.2.0,ISC +hpack.js,2.1.6,MIT +html-entities,1.2.0,MIT +html-pipeline,1.11.0,MIT +html2text,0.2.0,MIT +htmlentities,4.3.4,MIT +http,0.9.8,MIT +http-cookie,1.0.3,MIT +http-deceiver,1.2.7,MIT +http-errors,1.5.1,MIT +http-form_data,1.0.1,MIT +http-proxy,1.16.2,MIT +http-proxy-middleware,0.17.3,MIT +http-signature,1.1.1,MIT +http_parser.rb,0.6.0,MIT +httparty,0.13.7,MIT +httpclient,2.8.2,ruby +https-browserify,0.0.1,MIT +i18n,0.8.1,MIT +ice_nine,0.11.2,MIT +iconv-lite,0.4.15,MIT +ieee754,1.1.8,New BSD +ignore,3.2.2,MIT +imurmurhash,0.1.4,MIT +indexof,0.0.1,unknown +inflight,1.0.6,ISC +influxdb,0.2.3,MIT +inherits,2.0.3,ISC +ini,1.3.4,ISC +inquirer,0.12.0,MIT +interpret,1.0.1,MIT +invariant,2.2.2,New BSD +invert-kv,1.0.0,MIT +ipaddr.js,1.2.0,MIT +ipaddress,0.8.3,MIT +is-absolute,0.2.6,MIT +is-arrayish,0.2.1,MIT +is-binary-path,1.0.1,MIT +is-buffer,1.1.4,MIT +is-builtin-module,1.0.0,MIT +is-dotfile,1.0.2,MIT +is-equal-shallow,0.1.3,MIT +is-extendable,0.1.1,MIT +is-extglob,1.0.0,MIT +is-finite,1.0.2,MIT +is-fullwidth-code-point,1.0.0,MIT +is-glob,2.0.1,MIT +is-my-json-valid,2.15.0,MIT +is-number,2.1.0,MIT +is-path-cwd,1.0.0,MIT +is-path-in-cwd,1.0.0,MIT +is-path-inside,1.0.0,MIT +is-posix-bracket,0.1.1,MIT +is-primitive,2.0.0,MIT +is-property,1.0.2,MIT +is-relative,0.2.1,MIT +is-resolvable,1.0.0,MIT +is-stream,1.1.0,MIT +is-typedarray,1.0.0,MIT +is-unc-path,0.1.2,MIT +is-utf8,0.2.1,MIT +is-windows,0.2.0,MIT +isarray,1.0.0,MIT +isbinaryfile,3.0.2,MIT +isexe,1.1.2,ISC +isobject,2.1.0,MIT +isstream,0.1.2,MIT +istanbul,0.4.5,New BSD +istanbul-api,1.1.1,New BSD +istanbul-lib-coverage,1.0.1,New BSD +istanbul-lib-hook,1.0.0,New BSD +istanbul-lib-instrument,1.4.2,New BSD +istanbul-lib-report,1.0.0-alpha.3,New BSD +istanbul-lib-source-maps,1.1.0,New BSD +istanbul-reports,1.0.1,New BSD +jasmine-core,2.5.2,MIT +jasmine-jquery,2.1.1,MIT +jira-ruby,1.1.2,MIT +jodid25519,1.0.2,MIT +jquery,2.2.1,MIT +jquery-atwho-rails,1.3.2,MIT +jquery-rails,4.1.1,MIT +jquery-ujs,1.2.1,MIT +js-cookie,2.1.3,MIT +js-tokens,3.0.1,MIT +js-yaml,3.8.1,MIT +jsbn,0.1.0,BSD +jsesc,1.3.0,MIT +json,1.8.6,ruby +json-jwt,1.7.1,MIT +json-loader,0.5.4,MIT +json-schema,0.2.3,"AFLv2.1,BSD" +json-stable-stringify,1.0.1,MIT +json-stringify-safe,5.0.1,ISC +json3,3.3.2,MIT +json5,0.5.1,MIT +jsonfile,2.4.0,MIT +jsonify,0.0.0,Public Domain +jsonpointer,4.0.1,MIT +jsprim,1.3.1,MIT +jwt,1.5.6,MIT +kaminari,0.17.0,MIT +karma,1.4.1,MIT +karma-coverage-istanbul-reporter,0.2.0,MIT +karma-jasmine,1.1.0,MIT +karma-mocha-reporter,2.2.2,MIT +karma-phantomjs-launcher,1.0.2,MIT +karma-sourcemap-loader,0.3.7,MIT +karma-webpack,2.0.2,MIT +kew,0.7.0,Apache 2.0 +kgio,2.10.0,LGPL-2.1+ +kind-of,3.1.0,MIT +klaw,1.3.1,MIT +kubeclient,2.2.0,MIT +launchy,2.4.3,ISC +lazy-cache,1.0.4,MIT +lcid,1.0.0,MIT +levn,0.3.0,MIT +licensee,8.7.0,MIT +little-plugger,1.1.4,MIT +load-json-file,1.1.0,MIT +loader-runner,2.3.0,MIT +loader-utils,0.2.16,MIT +locate-path,2.0.0,MIT +lodash,4.17.4,MIT +lodash._baseget,3.7.2,MIT +lodash._topath,3.8.1,MIT +lodash.camelcase,4.1.1,MIT +lodash.capitalize,4.2.1,MIT +lodash.cond,4.5.2,MIT +lodash.deburr,4.1.0,MIT +lodash.get,3.7.0,MIT +lodash.isarray,3.0.4,MIT +lodash.kebabcase,4.0.1,MIT +lodash.snakecase,4.0.1,MIT +lodash.words,4.2.0,MIT +log4js,0.6.38,Apache 2.0 +logging,2.1.0,MIT +longest,1.0.1,MIT +loofah,2.0.3,MIT +loose-envify,1.3.1,MIT +lru-cache,2.2.4,MIT +mail,2.6.4,MIT +mail_room,0.9.1,MIT +media-typer,0.3.0,MIT +memoist,0.15.0,MIT +memory-fs,0.4.1,MIT +merge-descriptors,1.0.1,MIT +method_source,0.8.2,MIT +methods,1.1.2,MIT +micromatch,2.3.11,MIT +miller-rabin,4.0.0,MIT +mime,1.3.4,MIT +mime-db,1.26.0,MIT +mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0" +mimemagic,0.3.0,MIT +mini_portile2,2.1.0,MIT +minimalistic-assert,1.0.0,ISC +minimatch,3.0.3,ISC +minimist,0.0.8,MIT +mkdirp,0.5.1,MIT +moment,2.17.1,MIT +mousetrap,1.4.6,Apache 2.0 +mousetrap-rails,1.4.6,"MIT,Apache" +ms,0.7.2,MIT +multi_json,1.12.1,MIT +multi_xml,0.6.0,MIT +multipart-post,2.0.0,MIT +mustermann,0.4.0,MIT +mustermann-grape,0.4.0,MIT +mute-stream,0.0.5,ISC +nan,2.5.1,MIT +natural-compare,1.4.0,MIT +negotiator,0.6.1,MIT +net-ldap,0.12.1,MIT +net-ssh,3.0.1,MIT +netrc,0.11.0,MIT +node-libs-browser,2.0.0,MIT +node-pre-gyp,0.6.33,New BSD +node-zopfli,2.0.2,MIT +nokogiri,1.6.8.1,MIT +nopt,3.0.6,ISC +normalize-package-data,2.3.5,Simplified BSD +normalize-path,2.0.1,MIT +npmlog,4.0.2,ISC +number-is-nan,1.0.1,MIT +numerizer,0.1.1,MIT +oauth,0.5.1,MIT +oauth-sign,0.8.2,Apache 2.0 +oauth2,1.2.0,MIT +object-assign,4.1.1,MIT +object-component,0.0.3,unknown +object.omit,2.0.1,MIT +obuf,1.1.1,MIT +octokit,4.6.2,MIT +oj,2.17.4,MIT +omniauth,1.4.2,MIT +omniauth-auth0,1.4.1,MIT +omniauth-authentiq,0.3.0,MIT +omniauth-azure-oauth2,0.0.6,MIT +omniauth-cas3,1.1.3,MIT +omniauth-facebook,4.0.0,MIT +omniauth-github,1.1.2,MIT +omniauth-gitlab,1.0.2,MIT +omniauth-google-oauth2,0.4.1,MIT +omniauth-kerberos,0.3.0,MIT +omniauth-multipassword,0.4.2,MIT +omniauth-oauth,1.1.0,MIT +omniauth-oauth2,1.3.1,MIT +omniauth-oauth2-generic,0.2.2,MIT +omniauth-saml,1.7.0,MIT +omniauth-shibboleth,1.2.1,MIT +omniauth-twitter,1.2.1,MIT +omniauth_crowd,2.2.3,MIT +on-finished,2.3.0,MIT +on-headers,1.0.1,MIT +once,1.3.3,ISC +onetime,1.1.0,MIT +opener,1.4.3,(WTFPL OR MIT) +opn,4.0.2,MIT +optimist,0.6.1,MIT/X11 +optionator,0.8.2,MIT +options,0.0.6,MIT +org-ruby,0.9.12,MIT +original,1.0.0,MIT +orm_adapter,0.5.0,MIT +os,0.9.6,MIT +os-browserify,0.2.1,MIT +os-homedir,1.0.2,MIT +os-locale,1.4.0,MIT +os-tmpdir,1.0.2,MIT +p-limit,1.1.0,MIT +p-locate,2.0.0,MIT +pako,0.2.9,MIT +paranoia,2.2.0,MIT +parse-asn1,5.0.0,ISC +parse-glob,3.0.4,MIT +parse-json,2.2.0,MIT +parsejson,0.0.3,MIT +parseqs,0.0.5,MIT +parseuri,0.0.5,MIT +parseurl,1.3.1,MIT +path-browserify,0.0.0,MIT +path-exists,3.0.0,MIT +path-is-absolute,1.0.1,MIT +path-is-inside,1.0.2,(WTFPL OR MIT) +path-parse,1.0.5,MIT +path-to-regexp,0.1.7,MIT +path-type,1.1.0,MIT +pbkdf2,3.0.9,MIT +pend,1.2.0,MIT +pg,0.18.4,"BSD,ruby,GPL" +phantomjs-prebuilt,2.1.14,Apache 2.0 +pify,2.3.0,MIT +pikaday,1.5.1,"BSD,MIT" +pinkie,2.0.4,MIT +pinkie-promise,2.0.1,MIT +pkg-dir,1.0.0,MIT +pkg-up,1.0.0,MIT +pluralize,1.2.1,MIT +portfinder,1.0.13,MIT +posix-spawn,0.3.11,"MIT,LGPL" +prelude-ls,1.1.2,MIT +premailer,1.8.6,New BSD +premailer-rails,1.9.2,MIT +preserve,0.2.0,MIT +private,0.1.7,MIT +process,0.11.9,MIT +process-nextick-args,1.0.7,MIT +progress,1.1.8,MIT +proxy-addr,1.1.3,MIT +prr,0.0.0,MIT +public-encrypt,4.0.0,MIT +punycode,1.4.1,MIT +pyu-ruby-sasl,0.0.3.3,MIT +qjobs,1.1.5,MIT +qs,6.2.0,New BSD +querystring,0.2.0,MIT +querystring-es3,0.2.1,MIT +querystringify,0.0.4,MIT +rack,1.6.5,MIT +rack-accept,0.4.5,MIT +rack-attack,4.4.1,MIT +rack-cors,0.4.0,MIT +rack-oauth2,1.2.3,MIT +rack-protection,1.5.3,MIT +rack-proxy,0.6.0,MIT +rack-test,0.6.3,MIT +rails,4.2.8,MIT +rails-deprecated_sanitizer,1.0.3,MIT +rails-dom-testing,1.0.8,MIT +rails-html-sanitizer,1.0.3,MIT +railties,4.2.8,MIT +rainbow,2.1.0,MIT +raindrops,0.17.0,LGPL-2.1+ +rake,10.5.0,MIT +randomatic,1.1.6,MIT +randombytes,2.0.3,MIT +range-parser,1.2.0,MIT +raphael,2.2.7,MIT +raw-body,2.2.0,MIT +raw-loader,0.5.1,MIT +rc,1.1.6,(BSD-2-Clause OR MIT OR Apache-2.0) +rdoc,4.2.2,ruby +read-pkg,1.1.0,MIT +read-pkg-up,1.0.1,MIT +readable-stream,2.1.5,MIT +readdirp,2.1.0,MIT +readline2,1.0.1,MIT +recaptcha,3.0.0,MIT +rechoir,0.6.2,MIT +recursive-open-struct,1.0.0,MIT +redcarpet,3.4.0,MIT +redis,3.2.2,MIT +redis-actionpack,5.0.1,MIT +redis-activesupport,5.0.1,MIT +redis-namespace,1.5.2,MIT +redis-rack,1.6.0,MIT +redis-rails,5.0.1,MIT +redis-store,1.2.0,MIT +regenerate,1.3.2,MIT +regenerator-runtime,0.10.1,MIT +regenerator-transform,0.9.8,BSD +regex-cache,0.4.3,MIT +regexpu-core,2.0.0,MIT +regjsgen,0.2.0,MIT +regjsparser,0.1.5,BSD +repeat-element,1.1.2,MIT +repeat-string,1.6.1,MIT +repeating,2.0.1,MIT +request,2.79.0,Apache 2.0 +request-progress,2.0.1,MIT +request_store,1.3.1,MIT +require-directory,2.1.1,MIT +require-main-filename,1.0.1,ISC +require-uncached,1.0.3,MIT +requires-port,1.0.0,MIT +resolve,1.2.0,MIT +resolve-from,1.0.1,MIT +responders,2.3.0,MIT +rest-client,2.0.0,MIT +restore-cursor,1.0.1,MIT +retriable,1.4.1,MIT +right-align,0.1.3,MIT +rimraf,2.5.4,ISC +rinku,2.0.0,ISC +ripemd160,1.0.1,New BSD +rotp,2.1.2,MIT +rouge,2.0.7,MIT +rqrcode,0.7.0,MIT +rqrcode-rails3,0.1.7,MIT +ruby-fogbugz,0.2.1,MIT +ruby-prof,0.16.2,Simplified BSD +ruby-saml,1.4.1,MIT +rubyntlm,0.5.2,MIT +rubypants,0.2.0,BSD +rufus-scheduler,3.1.10,MIT +rugged,0.24.0,MIT +run-async,0.1.0,MIT +rx-lite,3.1.2,Apache 2.0 +safe-buffer,5.0.1,MIT +safe_yaml,1.0.4,MIT +sanitize,2.1.0,MIT +sass,3.4.22,MIT +sass-rails,5.0.6,MIT +sawyer,0.8.1,MIT +securecompare,1.0.0,MIT +seed-fu,2.3.6,MIT +select-hose,2.0.0,MIT +select2,3.5.2-browserify,unknown +select2-rails,3.5.9.3,MIT +semver,5.3.0,ISC +send,0.14.2,MIT +sentry-raven,2.0.2,Apache 2.0 +serve-index,1.8.0,MIT +serve-static,1.11.2,MIT +set-blocking,2.0.0,ISC +set-immediate-shim,1.0.1,MIT +setimmediate,1.0.5,MIT +setprototypeof,1.0.2,ISC +settingslogic,2.0.9,MIT +sha.js,2.4.8,MIT +shelljs,0.7.6,New BSD +sidekiq,4.2.7,LGPL +sidekiq-cron,0.4.4,MIT +sidekiq-limit_fetch,3.4.0,MIT +signal-exit,3.0.2,ISC +signet,0.7.3,Apache 2.0 +slack-notifier,1.5.1,MIT +slash,1.0.0,MIT +slice-ansi,0.0.4,MIT +sntp,1.0.9,BSD +socket.io,1.7.2,MIT +socket.io-adapter,0.5.0,MIT +socket.io-client,1.7.2,MIT +socket.io-parser,2.3.1,MIT +sockjs,0.3.18,MIT +sockjs-client,1.1.1,MIT +source-list-map,0.1.8,MIT +source-map,0.5.6,New BSD +source-map-support,0.4.11,MIT +spdx-correct,1.0.2,Apache 2.0 +spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0) +spdx-license-ids,1.2.2,Unlicense +spdy,3.4.4,MIT +spdy-transport,2.0.18,MIT +sprintf-js,1.0.3,New BSD +sprockets,3.7.1,MIT +sprockets-rails,3.2.0,MIT +sshpk,1.10.2,MIT +state_machines,0.4.0,MIT +state_machines-activemodel,0.4.0,MIT +state_machines-activerecord,0.4.0,MIT +stats-webpack-plugin,0.4.3,MIT +statuses,1.3.1,MIT +stream-browserify,2.0.1,MIT +stream-http,2.6.3,MIT +string-width,1.0.2,MIT +string.fromcodepoint,0.2.1,MIT +string.prototype.codepointat,0.2.0,MIT +string_decoder,0.10.31,MIT +stringex,2.5.2,MIT +stringstream,0.0.5,MIT +strip-ansi,3.0.1,MIT +strip-bom,2.0.0,MIT +strip-json-comments,1.0.4,MIT +supports-color,0.2.0,MIT +sys-filesystem,1.1.6,Artistic 2.0 +table,3.8.3,New BSD +tapable,0.2.6,MIT +tar,2.2.1,ISC +tar-pack,3.3.0,Simplified BSD +temple,0.7.7,MIT +test-exclude,4.0.0,ISC +text-table,0.2.0,MIT +thor,0.19.4,MIT +thread_safe,0.3.6,Apache 2.0 +throttleit,1.0.0,MIT +through,2.3.8,MIT +tilt,2.0.6,MIT +timeago.js,2.0.5,MIT +timers-browserify,2.0.2,MIT +timfel-krb5-auth,0.8.3,LGPL +tmp,0.0.28,MIT +to-array,0.1.4,MIT +to-arraybuffer,1.0.1,MIT +to-fast-properties,1.0.2,MIT +tool,0.2.3,MIT +tough-cookie,2.3.2,New BSD +trim-right,1.0.1,MIT +truncato,0.7.8,MIT +tryit,1.0.3,MIT +tty-browserify,0.0.0,MIT +tunnel-agent,0.4.3,Apache 2.0 +tweetnacl,0.14.5,Unlicense +type-check,0.3.2,MIT +type-is,1.6.14,MIT +typedarray,0.0.6,MIT +tzinfo,1.2.2,MIT +u2f,0.2.1,MIT +uglifier,2.7.2,MIT +uglify-js,2.7.5,Simplified BSD +uglify-to-browserify,1.0.2,MIT +uid-number,0.0.6,ISC +ultron,1.0.2,MIT +unc-path-regex,0.1.2,MIT +underscore,1.8.3,MIT +underscore-rails,1.8.3,MIT +unf,0.1.4,BSD +unf_ext,0.0.7.2,MIT +unicorn,5.1.0,ruby +unicorn-worker-killer,0.4.4,ruby +unpipe,1.0.0,MIT +url,0.11.0,MIT +url-parse,1.0.5,MIT +url_safe_base64,0.2.2,MIT +user-home,2.0.0,MIT +useragent,2.1.12,MIT +util,0.10.3,MIT +util-deprecate,1.0.2,MIT +utils-merge,1.0.0,MIT +uuid,3.0.1,MIT +validate-npm-package-license,3.0.1,Apache 2.0 +validates_hostname,1.0.6,MIT +vary,1.1.0,MIT +verror,1.3.6,MIT +version_sorter,2.1.0,MIT +virtus,1.0.5,MIT +vm-browserify,0.0.4,MIT +vmstat,2.3.0,MIT +void-elements,2.0.1,MIT +vue,2.1.10,MIT +vue-resource,0.9.3,MIT +warden,1.2.6,MIT +watchpack,1.2.1,MIT +wbuf,1.7.2,MIT +webpack,2.2.1,MIT +webpack-bundle-analyzer,2.3.0,MIT +webpack-dev-middleware,1.10.0,MIT +webpack-dev-server,2.3.0,MIT +webpack-rails,0.9.9,MIT +webpack-sources,0.1.4,MIT +websocket-driver,0.6.5,MIT +websocket-extensions,0.1.1,MIT +which,1.2.12,ISC +which-module,1.0.0,ISC +wide-align,1.1.0,ISC +wikicloth,0.8.1,MIT +window-size,0.1.0,MIT +wordwrap,0.0.2,MIT/X11 +wrap-ansi,2.1.0,MIT +wrappy,1.0.2,ISC +write,0.2.1,MIT +ws,1.1.1,MIT +wtf-8,1.0.0,MIT +xmlhttprequest-ssl,1.5.3,MIT +xtend,4.0.1,MIT +y18n,3.2.1,ISC +yargs,3.10.0,MIT +yargs-parser,4.2.1,ISC +yauzl,2.4.1,MIT +yeast,0.1.2,MIT -- cgit v1.2.1 From ac669df526a8cf42cd990b93f5a3f8f87e3eff9d Mon Sep 17 00:00:00 2001 From: mhasbini Date: Thu, 9 Mar 2017 02:14:16 +0200 Subject: link issuable reference to itself in header --- app/helpers/issuables_helper.rb | 19 ++++++++++++++++++- changelogs/unreleased/24137-issuable-permalink.yml | 4 ++++ spec/features/issues/form_spec.rb | 10 ++++++++++ spec/features/merge_requests/form_spec.rb | 11 +++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/24137-issuable-permalink.yml diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index c2b399041c6..aad83731b87 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -1,4 +1,6 @@ module IssuablesHelper + include GitlabRoutingHelper + def sidebar_gutter_toggle_icon sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' }) end @@ -95,8 +97,23 @@ module IssuablesHelper h(milestone_title.presence || default_label) end + def to_url_reference(issuable) + case issuable + when Issue + link_to issuable.to_reference, issue_url(issuable) + when MergeRequest + link_to issuable.to_reference, merge_request_url(issuable) + else + issuable.to_reference + end + end + def issuable_meta(issuable, project, text) - output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier" + output = content_tag(:strong, class: "identifier") do + concat("#{text} ") + concat(to_url_reference(issuable)) + end + output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) diff --git a/changelogs/unreleased/24137-issuable-permalink.yml b/changelogs/unreleased/24137-issuable-permalink.yml new file mode 100644 index 00000000000..bcc6c6957a1 --- /dev/null +++ b/changelogs/unreleased/24137-issuable-permalink.yml @@ -0,0 +1,4 @@ +--- +title: Link issuable reference to itself in meta-header +merge_request: 9641 +author: mhasbini diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index d4e0ef91856..755992069ff 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'New/edit issue', feature: true, js: true do + include GitlabRoutingHelper + let!(:project) { create(:project) } let!(:user) { create(:user)} let!(:user2) { create(:user)} @@ -78,6 +80,14 @@ describe 'New/edit issue', feature: true, js: true do expect(page).to have_content label2.title end end + + page.within '.issuable-meta' do + issue = Issue.find_by(title: 'title') + + expect(page).to have_text("Issue #{issue.to_reference}") + # compare paths because the host differ in test + expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue)) + end end it 'correctly updates the dropdown toggle when removing a label' do diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 1ecdb8b5983..f8518f450dc 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'New/edit merge request', feature: true, js: true do + include GitlabRoutingHelper + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:fork_project) { create(:project, forked_from_project: project) } let!(:user) { create(:user)} @@ -84,6 +86,15 @@ describe 'New/edit merge request', feature: true, js: true do expect(page).to have_content label2.title end end + + page.within '.issuable-meta' do + merge_request = MergeRequest.find_by(source_branch: 'fix') + + expect(page).to have_text("Merge Request #{merge_request.to_reference}") + # compare paths because the host differ in test + expect(find_link(merge_request.to_reference)[:href]) + .to end_with(merge_request_path(merge_request)) + end end end -- cgit v1.2.1 From 5bd2fe9412473abf2d9af3f76bcf6d37489110df Mon Sep 17 00:00:00 2001 From: TM Lee Date: Sun, 5 Mar 2017 14:38:04 +0800 Subject: [#29014] Create issue form buttons are misaligned on mobile - Ensure Save/Create button is aligned with Cancel - Move the cancel button above but float to the right using pull-right - Allows responsive css to flow --- app/views/shared/issuable/_form.html.haml | 27 +++++++++++----------- ...ate-issue-form-buttons-misaligned-on-mobile.yml | 4 ++++ 2 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index cb92b2e97a7..70470c83c51 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -62,24 +62,25 @@ - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{ class: (is_footer ? "footer-block" : "middle-block") } - - if issuable.new_record? - = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - - else - = form.submit 'Save changes', class: 'btn btn-save' + .pull-right + - if issuable.new_record? + = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' + - else + - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) + = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' + = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' + + %span.append-right-10 + - if issuable.new_record? + = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' + - else + = form.submit 'Save changes', class: 'btn btn-save' - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) - .inline.prepend-left-10 + .inline.prepend-top-10 Please review the %strong= link_to('contribution guidelines', guide_url) for this project. - - if issuable.new_record? - = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - - else - .pull-right - - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) - = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, - method: :delete, class: 'btn btn-danger btn-grouped' - = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' = form.hidden_field :lock_version diff --git a/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml new file mode 100644 index 00000000000..f869249c22b --- /dev/null +++ b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml @@ -0,0 +1,4 @@ +--- +title: Fix create issue form buttons are misaligned on mobile +merge_request: 9706 +author: TM Lee -- cgit v1.2.1 From 71eeb8632774dd0f7a38ac867319b8ad9182bbee Mon Sep 17 00:00:00 2001 From: George Andrinopoulos Date: Thu, 9 Mar 2017 01:29:11 +0000 Subject: Fix wrong message on starred projects filtering --- app/views/dashboard/projects/starred.html.haml | 2 +- ...projects-filter-wrong-message-on-no-results.yml | 4 +++ .../dashboard/user_filters_projects_spec.rb | 37 ++++++++++++++++------ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 70705923d42..162ae153b1c 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -6,7 +6,7 @@ - if @last_push = render "events/event_last_push", event: @last_push -- if @projects.any? +- if @projects.any? || params[:filter_projects] = render 'projects' - else %h3 You don't have starred projects yet diff --git a/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml b/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml new file mode 100644 index 00000000000..dd94b3fe663 --- /dev/null +++ b/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml @@ -0,0 +1,4 @@ +--- +title: Fix wrong message on starred projects filtering +merge_request: +author: George Andrinopoulos diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb index c2e0612aef8..34d6257f5fd 100644 --- a/spec/features/dashboard/user_filters_projects_spec.rb +++ b/spec/features/dashboard/user_filters_projects_spec.rb @@ -1,26 +1,45 @@ require 'spec_helper' -describe "Dashboard > User filters projects", feature: true do +describe 'Dashboard > User filters projects', :feature do + let(:user) { create(:user) } + let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace) } + let(:user2) { create(:user) } + let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) } + + before do + project.team << [user, :master] + + login_as(user) + end + describe 'filtering personal projects' do before do - user = create(:user) - project = create(:project, name: "Victorialand", namespace: user.namespace) - project.team << [user, :master] - - user2 = create(:user) - project2 = create(:project, name: "Treasure", namespace: user2.namespace) project2.team << [user, :developer] - login_as(user) visit dashboard_projects_path end it 'filters by projects "Owned by me"' do - click_link "Owned by me" + click_link 'Owned by me' expect(page).to have_css('.is-active', text: 'Owned by me') expect(page).to have_content('Victorialand') expect(page).not_to have_content('Treasure') end end + + describe 'filtering starred projects', :js do + before do + user.toggle_star(project) + + visit dashboard_projects_path + end + + it 'returns message when starred projects fitler returns no results' do + fill_in 'project-filter-form-field', with: 'Beta\n' + + expect(page).to have_content('No projects found') + expect(page).not_to have_content('You don\'t have starred projects yet') + end + end end -- cgit v1.2.1 From 26f28f9654a2f1a49364733e11da2ac9db56645c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 7 Mar 2017 13:58:14 +0200 Subject: Show members of parent groups on project members page Signed-off-by: Dmitriy Zaporozhets --- .../projects/settings/members_controller.rb | 37 +++----------------- app/finders/group_members_finder.rb | 2 +- app/finders/members_finder.rb | 40 +++++++++++++++++----- changelogs/unreleased/dz-nested-groups-members.yml | 4 +++ spec/finders/members_finder_spec.rb | 22 ++++++++++++ 5 files changed, 62 insertions(+), 43 deletions(-) create mode 100644 changelogs/unreleased/dz-nested-groups-members.yml create mode 100644 spec/finders/members_finder_spec.rb diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb index 5735e281f66..cbfa2afa959 100644 --- a/app/controllers/projects/settings/members_controller.rb +++ b/app/controllers/projects/settings/members_controller.rb @@ -7,47 +7,18 @@ module Projects @sort = params[:sort].presence || sort_value_name @group_links = @project.project_group_links - @project_members = @project.project_members - @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) - - group = @project.group - - # group links - @group_links = @project.project_group_links.all - @skip_groups = @group_links.pluck(:group_id) @skip_groups << @project.namespace_id unless @project.personal? - if group - # We need `.where.not(user_id: nil)` here otherwise when a group has an - # invitee, it would make the following query return 0 rows since a NULL - # user_id would be present in the subquery - # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values - group_members = MembersFinder.new(@project_members, group).execute(current_user) - end + @project_members = MembersFinder.new(@project, current_user).execute if params[:search].present? - user_ids = @project.users.search(params[:search]).select(:id) - @project_members = @project_members.where(user_id: user_ids) - - if group_members - user_ids = group.users.search(params[:search]).select(:id) - group_members = group_members.where(user_id: user_ids) - end - - @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) + @project_members = @project_members.joins(:user).merge(User.search(params[:search])) + @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"] - wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members - - @project_members = Member. - where(wheres.join(' OR ')). - sort(@sort). - page(params[:page]) - + @project_members = @project_members.sort(@sort).page(params[:page]) @requesters = AccessRequestsFinder.new(@project).execute(current_user) - @project_member = @project.project_members.new end end diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index 9f2206346ce..fce3775f40e 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -1,4 +1,4 @@ -class GroupMembersFinder < Projects::ApplicationController +class GroupMembersFinder def initialize(group) @group = group end diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb index 702944404f5..af24045886e 100644 --- a/app/finders/members_finder.rb +++ b/app/finders/members_finder.rb @@ -1,13 +1,35 @@ -class MembersFinder < Projects::ApplicationController - def initialize(project_members, project_group) - @project_members = project_members - @project_group = project_group +class MembersFinder + attr_reader :project, :current_user, :group + + def initialize(project, current_user) + @project = project + @current_user = current_user + @group = project.group + end + + def execute + project_members = project.project_members + project_members = project_members.non_invite unless can?(current_user, :admin_project, project) + wheres = ["members.id IN (#{project_members.select(:id).to_sql})"] + + if group + # We need `.where.not(user_id: nil)` here otherwise when a group has an + # invitee, it would make the following query return 0 rows since a NULL + # user_id would be present in the subquery + # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values + non_null_user_ids = project_members.where.not(user_id: nil).select(:user_id) + + group_members = GroupMembersFinder.new(group).execute + group_members = group_members.where.not(user_id: non_null_user_ids) + group_members = group_members.non_invite unless can?(current_user, :admin_group, group) + + wheres << "members.id IN (#{group_members.select(:id).to_sql})" + end + + Member.where(wheres.join(' OR ')) end - def execute(current_user) - non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) - group_members = @project_group.group_members.where.not(user_id: non_null_user_ids) - group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group) - group_members + def can?(*args) + Ability.allowed?(*args) end end diff --git a/changelogs/unreleased/dz-nested-groups-members.yml b/changelogs/unreleased/dz-nested-groups-members.yml new file mode 100644 index 00000000000..bab0c8465c2 --- /dev/null +++ b/changelogs/unreleased/dz-nested-groups-members.yml @@ -0,0 +1,4 @@ +--- +title: Show members of parent groups on project members page +merge_request: +author: diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb new file mode 100644 index 00000000000..cf691cf684b --- /dev/null +++ b/spec/finders/members_finder_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe MembersFinder, '#execute' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, :access_requestable, parent: group) } + let(:project) { create(:project, namespace: nested_group) } + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:user4) { create(:user) } + + it 'returns members for project and parent groups' do + nested_group.request_access(user1) + member1 = group.add_master(user2) + member2 = nested_group.add_master(user3) + member3 = project.add_master(user4) + + result = described_class.new(project, user2).execute + + expect(result.to_a).to eq([member3, member2, member1]) + end +end -- cgit v1.2.1 From dc262839ee582fe6c5f00532c22aff0cfff5abe2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 9 Mar 2017 09:43:16 +0100 Subject: Add zip/unzip as dependencies for Pages source installations [ci skip] --- doc/administration/pages/source.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index 463715e48ca..f6f50e2c571 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -17,14 +17,17 @@ Pages to the latest supported version. ## Prerequisites -Before proceeding with the Pages configuration, you will need to: - -1. Have a separate domain under which the GitLab Pages will be served. In this - document we assume that to be `example.io`. -1. Configure a **wildcard DNS record**. -1. (Optional) Have a **wildcard certificate** for that domain if you decide to - serve Pages under HTTPS. -1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md) +Before proceeding with the Pages configuration, make sure that: + +1. You have a separate domain under which GitLab Pages will be served. In + this document we assume that to be `example.io`. +1. You have configured a **wildcard DNS record** for that domain. +1. You have installed the `zip` and `unzip` packages in the same server that + GitLab is installed since they are needed to compress/uncompress the + Pages artifacts. +1. (Optional) You have a **wildcard certificate** for the Pages domain if you + decide to serve Pages (`*.example.io`) under HTTPS. +1. (Optional but recommended) You have configured and enabled the [Shared Runners][] so that your users don't have to bring their own. ### DNS configuration @@ -390,3 +393,4 @@ than GitLab to prevent XSS attacks. [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../restart_gitlab.md#installations-from-source [gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 +[shared runners]: ../../ci/runners/README.md -- cgit v1.2.1 From 789bae241cab6fdab00fb8ef632e7e1c9dd34025 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Mar 2017 08:53:47 +0000 Subject: Fixed bugs with diff comment avatars The comment button on commit view was way out to the left side because the element that renders the diff avatars was rendering when it shouldnt be When commenting on a discussion on the discussions tab it would try to render the avatar & in some cases work correctly even though it shouldnt be possible for this to happen. Changed the if statement to take this into account Closes #29237, #29238, #29243 --- app/views/projects/diffs/_line.html.haml | 5 +++-- app/views/projects/diffs/_parallel_view.html.haml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index ed279cfe168..b40d9cb928b 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -5,6 +5,7 @@ - line_code = diff_file.line_code(line) - if discussions && !line.meta? - discussion = discussions[line_code] + - show_discussion_avatars = discussion && discussion.resolvable? && !plain %tr.line_holder{ class: type, id: (line_code unless plain) } - case type - when 'match' @@ -14,13 +15,13 @@ %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num.js-avatar-container{ class: type, data: { linenumber: line.old_pos } } + %td.old_line.diff-line-num{ class: [type, ("js-avatar-container" if show_discussion_avatars)], data: { linenumber: line.old_pos } } - link_text = type == "new" ? " " : line.old_pos - if plain = link_text - else %a{ href: "##{line_code}", data: { linenumber: link_text } } - - if discussion && !plain + - if show_discussion_avatars %diff-note-avatars{ "discussion-id" => discussion.id } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " " : line.new_pos diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 6448748113b..e7758c8bdfa 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -20,7 +20,7 @@ - left_position = diff_file.position(left) %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } - - if discussion_left + - if discussion_left && discussion_left.resolvable? %diff-note-avatars{ "discussion-id" => discussion_left.id } %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) - else @@ -39,7 +39,7 @@ - right_position = diff_file.position(right) %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } - - if discussion_right + - if discussion_right && discussion_right.resolvable? %diff-note-avatars{ "discussion-id" => discussion_right.id } %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) - else -- cgit v1.2.1 From 2edd42cafc821422076ec3aeaaaebd8f81b7aa4f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Mar 2017 08:59:29 +0000 Subject: Check was in wrong part, the avatar container class should always be added on lines in the changes tab --- app/views/projects/diffs/_line.html.haml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index b40d9cb928b..62135d3ae32 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -5,7 +5,6 @@ - line_code = diff_file.line_code(line) - if discussions && !line.meta? - discussion = discussions[line_code] - - show_discussion_avatars = discussion && discussion.resolvable? && !plain %tr.line_holder{ class: type, id: (line_code unless plain) } - case type - when 'match' @@ -15,13 +14,13 @@ %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{ class: [type, ("js-avatar-container" if show_discussion_avatars)], data: { linenumber: line.old_pos } } + %td.old_line.diff-line-num{ class: [type, ("js-avatar-container" if !plain)], data: { linenumber: line.old_pos } } - link_text = type == "new" ? " " : line.old_pos - if plain = link_text - else %a{ href: "##{line_code}", data: { linenumber: link_text } } - - if show_discussion_avatars + - if discussion && discussion.resolvable? && !plain %diff-note-avatars{ "discussion-id" => discussion.id } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " " : line.new_pos -- cgit v1.2.1 From 91c2b4ce33ac53a95b6ab62b010eca56f6a43821 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 9 Mar 2017 10:19:37 +0100 Subject: Cleanup CI variables table and add a deprecation note [ci skip] --- doc/ci/README.md | 2 + doc/ci/variables/README.md | 125 ++++++++++++++++++++++----------------------- 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/doc/ci/README.md b/doc/ci/README.md index cbab7c9f18d..d8fba5d7a77 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -27,6 +27,8 @@ ## Breaking changes +- [CI variables renaming](variables/README.md#9-0-renaming) Read about the + deprecated CI variables and what you should use for GitLab 9.0+. - [New CI job permissions model](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your jobs. There's a new way to access your Git submodules and LFS objects in jobs. diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 4c3e7c4e86e..45304d0343f 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -27,58 +27,53 @@ Some of the predefined environment variables are available only if a minimum version of [GitLab Runner][runner] is used. Consult the table below to find the version of Runner required. -| Variable | GitLab | Runner | Description | -|-------------------------|--------|--------|-------------| -| **CI** | all | 0.4 | Mark that job is executed in CI environment | -| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment | -| **CI_SERVER** | all | all | Mark that job is executed in CI environment | -| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs | -| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs | -| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs | -| **CI_BUILD_ID** | all | all | The unique id of the current job that GitLab CI uses internally. Deprecated, use CI_JOB_ID | -| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally | -| **CI_BUILD_REF** | all | all | The commit revision for which project is built. Deprecated, use CI_COMMIT_REF | -| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built | -| **CI_BUILD_TAG** | all | 0.5 | The commit tag name. Present only when building tags. Deprecated, use CI_COMMIT_TAG | -| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. | -| **CI_BUILD_NAME** | all | 0.5 | The name of the job as defined in `.gitlab-ci.yml`. Deprecated, use CI_JOB_NAME | -| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` | -| **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml`. Deprecated, use CI_JOB_STAGE | -| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | -| **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built. Deprecated, use CI_COMMIT_REF_NAME | -| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built | -| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. Deprecated, use CI_COMMIT_REF_SLUG | -| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | -| **CI_BUILD_REPO** | all | all | The URL to clone the Git repository. Deprecated, use CI_REPOSITORY | -| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository | -| **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that job was [triggered]. Deprecated, use CI_PIPELINE_TRIGGERED | -| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] | -| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that job was manually started. Deprecated, use CI_JOB_MANUAL | -| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started | -| **CI_BUILD_TOKEN** | all | 1.2 | Token used for authenticating with the GitLab Container Registry. Deprecated, use CI_JOB_TOKEN | -| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the GitLab Container Registry | -| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally | -| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally | -| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built | -| **CI_PROJECT_NAMESPACE**| 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built | -| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | -| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | -| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run | -| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job | -| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | -| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | -| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project | -| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | -| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab | -| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags | -| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | -| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job | -| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job | -| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job | -| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job | -| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job | -| **CI_REGISTRY_USER** | 9.0 | all | The username to use to push containers to the GitLab Container Registry | -| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry | +>**Note:** +Starting with GitLab 9.0, we have deprecated some variables. Read the +[9.0 Renaming](#9-0-renaming) section to find out their replacements. **You are +strongly advised to use the new variables as we will remove the old ones in +future GitLab releases.** + +| Variable | GitLab | Runner | Description | +|-------------------------------- |--------|--------|-------------| +| **CI** | all | 0.4 | Mark that job is executed in CI environment | +| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built | +| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | +| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built | +| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. | +| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | +| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job | +| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | +| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally | +| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started | +| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` | +| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | +| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the GitLab Container Registry | +| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository | +| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab | +| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | +| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags | +| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally | +| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] | +| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run | +| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally | +| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built | +| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built | +| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | +| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | +| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | +| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project | +| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry | +| **CI_REGISTRY_USER** | 9.0 | all | The username to use to push containers to the GitLab Container Registry | +| **CI_SERVER** | all | all | Mark that job is executed in CI environment | +| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs | +| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs | +| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs | +| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job | +| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job | +| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment | +| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job | +| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job | +| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job | Example values: @@ -121,19 +116,19 @@ To follow conventions of naming across GitLab, and to futher move away from the `build` term and toward `job` CI variables have been renamed for the 9.0 release. -| 8.X name | 9.0 name | -|----------|----------| -| CI_BUILD_ID | CI_JOB_ID | -| CI_BUILD_REF | CI_COMMIT_SHA | -| CI_BUILD_TAG | CI_COMMIT_TAG | -| CI_BUILD_REF_NAME | CI_COMMIT_REF_NAME | -| CI_BUILD_REF_SLUG | CI_COMMIT_REF_SLUG | -| CI_BUILD_NAME | CI_JOB_NAME | -| CI_BUILD_STAGE | CI_JOB_STAGE | -| CI_BUILD_REPO | CI_REPOSITORY | -| CI_BUILD_TRIGGERED | CI_PIPELINE_TRIGGERED | -| CI_BUILD_MANUAL | CI_JOB_MANUAL | -| CI_BUILD_TOKEN | CI_JOB_TOKEN | +| 8.x name | 9.0+ name | +| --------------------- |------------------------ | +| `CI_BUILD_ID` | `CI_JOB_ID` | +| `CI_BUILD_REF` | `CI_COMMIT_SHA` | +| `CI_BUILD_TAG` | `CI_COMMIT_TAG` | +| `CI_BUILD_REF_NAME` | `CI_COMMIT_REF_NAME` | +| `CI_BUILD_REF_SLUG` | `CI_COMMIT_REF_SLUG` | +| `CI_BUILD_NAME` | `CI_JOB_NAME` | +| `CI_BUILD_STAGE` | `CI_JOB_STAGE` | +| `CI_BUILD_REPO` | `CI_REPOSITORY` | +| `CI_BUILD_TRIGGERED` | `CI_PIPELINE_TRIGGERED` | +| `CI_BUILD_MANUAL` | `CI_JOB_MANUAL` | +| `CI_BUILD_TOKEN` | `CI_JOB_TOKEN` | ## `.gitlab-ci.yaml` defined variables -- cgit v1.2.1 From 7d20e47622c9a6e0a780bdbe9b53c8890c00deba Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 12:45:04 +0100 Subject: Add GitLab QA integrations tests to GitLab CE / EE --- qa/.rspec | 3 + qa/Dockerfile | 15 +++++ qa/Gemfile | 8 +++ qa/README.md | 2 + qa/bin/docker | 25 ++++++++ qa/bin/qa | 7 +++ qa/bin/test | 3 + qa/qa.rb | 83 +++++++++++++++++++++++++++ qa/qa/git/repository.rb | 71 +++++++++++++++++++++++ qa/qa/page/admin/license.rb | 20 +++++++ qa/qa/page/admin/menu.rb | 19 ++++++ qa/qa/page/base.rb | 12 ++++ qa/qa/page/main/entry.rb | 26 +++++++++ qa/qa/page/main/groups.rb | 20 +++++++ qa/qa/page/main/menu.rb | 46 +++++++++++++++ qa/qa/page/main/projects.rb | 16 ++++++ qa/qa/page/project/new.rb | 24 ++++++++ qa/qa/page/project/show.rb | 23 ++++++++ qa/qa/runtime/namespace.rb | 15 +++++ qa/qa/runtime/user.rb | 15 +++++ qa/qa/scenario/actable.rb | 23 ++++++++ qa/qa/scenario/gitlab/license/add.rb | 21 +++++++ qa/qa/scenario/gitlab/project/create.rb | 31 ++++++++++ qa/qa/scenario/template.rb | 16 ++++++ qa/qa/scenario/test/instance.rb | 27 +++++++++ qa/qa/specs/config.rb | 78 +++++++++++++++++++++++++ qa/qa/specs/features/login/standard_spec.rb | 14 +++++ qa/qa/specs/features/project/create_spec.rb | 19 ++++++ qa/qa/specs/features/repository/clone_spec.rb | 57 ++++++++++++++++++ qa/qa/specs/features/repository/push_spec.rb | 39 +++++++++++++ qa/qa/specs/runner.rb | 15 +++++ qa/spec/scenario/actable_spec.rb | 47 +++++++++++++++ qa/spec/spec_helper.rb | 19 ++++++ 33 files changed, 859 insertions(+) create mode 100644 qa/.rspec create mode 100644 qa/Dockerfile create mode 100644 qa/Gemfile create mode 100644 qa/README.md create mode 100755 qa/bin/docker create mode 100755 qa/bin/qa create mode 100755 qa/bin/test create mode 100644 qa/qa.rb create mode 100644 qa/qa/git/repository.rb create mode 100644 qa/qa/page/admin/license.rb create mode 100644 qa/qa/page/admin/menu.rb create mode 100644 qa/qa/page/base.rb create mode 100644 qa/qa/page/main/entry.rb create mode 100644 qa/qa/page/main/groups.rb create mode 100644 qa/qa/page/main/menu.rb create mode 100644 qa/qa/page/main/projects.rb create mode 100644 qa/qa/page/project/new.rb create mode 100644 qa/qa/page/project/show.rb create mode 100644 qa/qa/runtime/namespace.rb create mode 100644 qa/qa/runtime/user.rb create mode 100644 qa/qa/scenario/actable.rb create mode 100644 qa/qa/scenario/gitlab/license/add.rb create mode 100644 qa/qa/scenario/gitlab/project/create.rb create mode 100644 qa/qa/scenario/template.rb create mode 100644 qa/qa/scenario/test/instance.rb create mode 100644 qa/qa/specs/config.rb create mode 100644 qa/qa/specs/features/login/standard_spec.rb create mode 100644 qa/qa/specs/features/project/create_spec.rb create mode 100644 qa/qa/specs/features/repository/clone_spec.rb create mode 100644 qa/qa/specs/features/repository/push_spec.rb create mode 100644 qa/qa/specs/runner.rb create mode 100644 qa/spec/scenario/actable_spec.rb create mode 100644 qa/spec/spec_helper.rb diff --git a/qa/.rspec b/qa/.rspec new file mode 100644 index 00000000000..b83d9b7aa65 --- /dev/null +++ b/qa/.rspec @@ -0,0 +1,3 @@ +--color +--format documentation +--require spec_helper diff --git a/qa/Dockerfile b/qa/Dockerfile new file mode 100644 index 00000000000..b4281c02f5a --- /dev/null +++ b/qa/Dockerfile @@ -0,0 +1,15 @@ +FROM ruby:2.3 +LABEL maintainer "Grzegorz Bizon " + +RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \ + apt-get update && apt-get install -y --force-yes \ + libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \ + apt-get clean + + +WORKDIR /home/qa + +COPY ./ ./ +RUN bundle install + +ENTRYPOINT ["bin/test"] diff --git a/qa/Gemfile b/qa/Gemfile new file mode 100644 index 00000000000..baafc976c4b --- /dev/null +++ b/qa/Gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org' + +gem 'capybara', '~> 2.12.1' +gem 'capybara-screenshot', '~> 1.0.14' +gem 'capybara-webkit', '~> 1.12.0' +gem 'rake', '~> 12.0.0' +gem 'rspec', '~> 3.5' +gem 'rubocop', '~> 0.47.1' diff --git a/qa/README.md b/qa/README.md new file mode 100644 index 00000000000..2b4577575c5 --- /dev/null +++ b/qa/README.md @@ -0,0 +1,2 @@ +## Integration tests for GitLab + diff --git a/qa/bin/docker b/qa/bin/docker new file mode 100755 index 00000000000..683e915f698 --- /dev/null +++ b/qa/bin/docker @@ -0,0 +1,25 @@ +#!/bin/sh + +case "$1" in + build) + docker pull $CI_REGISTRY_IMAGE:latest + docker build --cache-from $CI_REGISTRY_IMAGE:latest \ + -t $CI_REGISTRY_IMAGE:ce-latest -t $CI_REGISTRY_IMAGE:ee-latest \ + -t $CI_REGISTRY_IMAGE:ce-nightly -t $CI_REGISTRY_IMAGE:ee-nightly \ + -t $CI_REGISTRY_IMAGE:latest . + ;; + publish) + test -n "$CI_BUILD_TOKEN" || exit 1 + docker login --username gitlab-ci-token --password $CI_BUILD_TOKEN registry.gitlab.com + docker push $CI_REGISTRY_IMAGE:latest + docker push $CI_REGISTRY_IMAGE:ce-latest + docker push $CI_REGISTRY_IMAGE:ee-latest + docker push $CI_REGISTRY_IMAGE:ee-nightly + docker push $CI_REGISTRY_IMAGE:ee-nightly + docker logout registry.gitlab.com + ;; + *) + echo "Usage: $0 [build|publish]" + exit 1 + ;; +esac diff --git a/qa/bin/qa b/qa/bin/qa new file mode 100755 index 00000000000..cecdeac14db --- /dev/null +++ b/qa/bin/qa @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +require_relative '../qa' + +QA::Scenario + .const_get(ARGV.shift) + .perform(*ARGV) diff --git a/qa/bin/test b/qa/bin/test new file mode 100755 index 00000000000..997392ad6e4 --- /dev/null +++ b/qa/bin/test @@ -0,0 +1,3 @@ +#!/bin/bash + +xvfb-run bundle exec bin/qa $@ diff --git a/qa/qa.rb b/qa/qa.rb new file mode 100644 index 00000000000..c47561bfa18 --- /dev/null +++ b/qa/qa.rb @@ -0,0 +1,83 @@ +$LOAD_PATH << File.expand_path(File.dirname(__FILE__)) + +module QA + ## + # GitLab QA runtime classes, mostly singletons. + # + module Runtime + autoload :User, 'qa/runtime/user' + autoload :Namespace, 'qa/runtime/namespace' + end + + ## + # GitLab QA Scenarios + # + module Scenario + ## + # Support files + # + autoload :Actable, 'qa/scenario/actable' + autoload :Template, 'qa/scenario/template' + + ## + # Test scenario entrypoints. + # + module Test + autoload :Instance, 'qa/scenario/test/instance' + end + + ## + # GitLab instance scenarios. + # + module Gitlab + module Project + autoload :Create, 'qa/scenario/gitlab/project/create' + end + + module License + autoload :Add, 'qa/scenario/gitlab/license/add' + end + end + end + + ## + # Classes describing structure of GitLab, pages, menus etc. + # + # Needed to execute click-driven-only black-box tests. + # + module Page + autoload :Base, 'qa/page/base' + + module Main + autoload :Entry, 'qa/page/main/entry' + autoload :Menu, 'qa/page/main/menu' + autoload :Groups, 'qa/page/main/groups' + autoload :Projects, 'qa/page/main/projects' + end + + module Project + autoload :New, 'qa/page/project/new' + autoload :Show, 'qa/page/project/show' + end + + module Admin + autoload :Menu, 'qa/page/admin/menu' + autoload :License, 'qa/page/admin/license' + end + end + + ## + # Classes describing operations on Git repositories. + # + module Git + autoload :Repository, 'qa/git/repository' + end + + ## + # Classes that make it possible to execute features tests. + # + module Specs + autoload :Config, 'qa/specs/config' + autoload :Runner, 'qa/specs/runner' + end +end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb new file mode 100644 index 00000000000..b9e199000d6 --- /dev/null +++ b/qa/qa/git/repository.rb @@ -0,0 +1,71 @@ +require 'uri' + +module QA + module Git + class Repository + include Scenario::Actable + + def self.perform(*args) + Dir.mktmpdir do |dir| + Dir.chdir(dir) { super } + end + end + + def location=(address) + @location = address + @uri = URI(address) + end + + def username=(name) + @username = name + @uri.user = name + end + + def password=(pass) + @password = pass + @uri.password = pass + end + + def use_default_credentials + self.username = Runtime::User.name + self.password = Runtime::User.password + end + + def clone(opts = '') + `git clone #{opts} #{@uri.to_s} ./` + end + + def shallow_clone + clone('--depth 1') + end + + def configure_identity(name, email) + `git config user.name #{name}` + `git config user.email #{email}` + end + + def commit_file(name, contents, message) + add_file(name, contents) + commit(message) + end + + def add_file(name, contents) + File.write(name, contents) + + `git add #{name}` + end + + def commit(message) + `git commit -m "#{message}"` + end + + def push_changes(branch = 'master') + `git push #{@uri.to_s} #{branch}` + end + + def commits + `git log --oneline`.split("\n") + end + end + end +end diff --git a/qa/qa/page/admin/license.rb b/qa/qa/page/admin/license.rb new file mode 100644 index 00000000000..4bdfae30b37 --- /dev/null +++ b/qa/qa/page/admin/license.rb @@ -0,0 +1,20 @@ +module QA + module Page + module Admin + class License < Page::Base + def no_license? + page.has_content?('No GitLab Enterprise Edition ' \ + 'license has been provided yet') + end + + def add_new_license(key) + raise 'License key empty!' if key.to_s.empty? + + choose 'Enter license key' + fill_in 'License key', with: key + click_button 'Upload license' + end + end + end + end +end diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb new file mode 100644 index 00000000000..b01a4e10f93 --- /dev/null +++ b/qa/qa/page/admin/menu.rb @@ -0,0 +1,19 @@ +module QA + module Page + module Admin + class Menu < Page::Base + def go_to_license + within_middle_menu { click_link 'License' } + end + + private + + def within_middle_menu + page.within('.nav-control') do + yield + end + end + end + end + end +end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb new file mode 100644 index 00000000000..d55326c5262 --- /dev/null +++ b/qa/qa/page/base.rb @@ -0,0 +1,12 @@ +module QA + module Page + class Base + include Capybara::DSL + include Scenario::Actable + + def refresh + visit current_path + end + end + end +end diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb new file mode 100644 index 00000000000..fe80deb6429 --- /dev/null +++ b/qa/qa/page/main/entry.rb @@ -0,0 +1,26 @@ +module QA + module Page + module Main + class Entry < Page::Base + def initialize + visit('/') + + # This resolves cold boot problems with login page + find('.application', wait: 120) + end + + def sign_in_using_credentials + if page.has_content?('Change your password') + fill_in :user_password, with: Runtime::User.password + fill_in :user_password_confirmation, with: Runtime::User.password + click_button 'Change your password' + end + + fill_in :user_login, with: Runtime::User.name + fill_in :user_password, with: Runtime::User.password + click_button 'Sign in' + end + end + end + end +end diff --git a/qa/qa/page/main/groups.rb b/qa/qa/page/main/groups.rb new file mode 100644 index 00000000000..84597719a84 --- /dev/null +++ b/qa/qa/page/main/groups.rb @@ -0,0 +1,20 @@ +module QA + module Page + module Main + class Groups < Page::Base + def prepare_test_namespace + return if page.has_content?(Runtime::Namespace.name) + + click_on 'New Group' + + fill_in 'group_path', with: Runtime::Namespace.name + fill_in 'group_description', + with: "QA test run at #{Runtime::Namespace.time}" + choose 'Private' + + click_button 'Create group' + end + end + end + end +end diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb new file mode 100644 index 00000000000..90ff018b9d2 --- /dev/null +++ b/qa/qa/page/main/menu.rb @@ -0,0 +1,46 @@ +module QA + module Page + module Main + class Menu < Page::Base + def go_to_groups + within_global_menu { click_link 'Groups' } + end + + def go_to_projects + within_global_menu { click_link 'Projects' } + end + + def go_to_admin_area + within_user_menu { click_link 'Admin Area' } + end + + def sign_out + within_user_menu do + find('.header-user-dropdown-toggle').click + click_link('Sign out') + end + end + + def has_personal_area? + page.has_selector?('.header-user-dropdown-toggle') + end + + private + + def within_global_menu + find('.global-dropdown-toggle').click + + page.within('.global-dropdown-menu') do + yield + end + end + + def within_user_menu + page.within('.dropdown-menu-nav') do + yield + end + end + end + end + end +end diff --git a/qa/qa/page/main/projects.rb b/qa/qa/page/main/projects.rb new file mode 100644 index 00000000000..28d3a424022 --- /dev/null +++ b/qa/qa/page/main/projects.rb @@ -0,0 +1,16 @@ +module QA + module Page + module Main + class Projects < Page::Base + def go_to_new_project + ## + # There are 'New Project' and 'New project' buttons on the projects + # page, so we can't use `click_on`. + # + button = find('a', text: /^new project$/i) + button.click + end + end + end + end +end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb new file mode 100644 index 00000000000..b31bec27b59 --- /dev/null +++ b/qa/qa/page/project/new.rb @@ -0,0 +1,24 @@ +module QA + module Page + module Project + class New < Page::Base + def choose_test_namespace + find('#s2id_project_namespace_id').click + find('.select2-result-label', text: Runtime::Namespace.name).click + end + + def choose_name(name) + fill_in 'project_path', with: name + end + + def add_description(description) + fill_in 'project_description', with: description + end + + def create_new_project + click_on 'Create project' + end + end + end + end +end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb new file mode 100644 index 00000000000..56a270d8fcc --- /dev/null +++ b/qa/qa/page/project/show.rb @@ -0,0 +1,23 @@ +module QA + module Page + module Project + class Show < Page::Base + def choose_repository_clone_http + find('#clone-dropdown').click + + page.within('#clone-dropdown') do + find('span', text: 'HTTP').click + end + end + + def repository_location + find('#project_clone').value + end + + def wait_for_push + sleep 5 + end + end + end + end +end diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb new file mode 100644 index 00000000000..e4910b63a14 --- /dev/null +++ b/qa/qa/runtime/namespace.rb @@ -0,0 +1,15 @@ +module QA + module Runtime + module Namespace + extend self + + def time + @time ||= Time.now + end + + def name + 'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S') + end + end + end +end diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb new file mode 100644 index 00000000000..12ceda015f0 --- /dev/null +++ b/qa/qa/runtime/user.rb @@ -0,0 +1,15 @@ +module QA + module Runtime + module User + extend self + + def name + ENV['GITLAB_USERNAME'] || 'root' + end + + def password + ENV['GITLAB_PASSWORD'] || 'test1234' + end + end + end +end diff --git a/qa/qa/scenario/actable.rb b/qa/qa/scenario/actable.rb new file mode 100644 index 00000000000..6cdbd24780e --- /dev/null +++ b/qa/qa/scenario/actable.rb @@ -0,0 +1,23 @@ +module QA + module Scenario + module Actable + def act(*args, &block) + instance_exec(*args, &block) + end + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def perform + yield new if block_given? + end + + def act(*args, &block) + new.act(*args, &block) + end + end + end + end +end diff --git a/qa/qa/scenario/gitlab/license/add.rb b/qa/qa/scenario/gitlab/license/add.rb new file mode 100644 index 00000000000..ca5e1176959 --- /dev/null +++ b/qa/qa/scenario/gitlab/license/add.rb @@ -0,0 +1,21 @@ +module QA + module Scenario + module Gitlab + module License + class Add < Scenario::Template + def perform + Page::Main::Entry.act { sign_in_using_credentials } + Page::Main::Menu.act { go_to_admin_area } + Page::Admin::Menu.act { go_to_license } + + Page::Admin::License.act do + add_new_license(ENV['EE_LICENSE']) if no_license? + end + + Page::Main::Menu.act { sign_out } + end + end + end + end + end +end diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb new file mode 100644 index 00000000000..38522714e64 --- /dev/null +++ b/qa/qa/scenario/gitlab/project/create.rb @@ -0,0 +1,31 @@ +require 'securerandom' + +module QA + module Scenario + module Gitlab + module Project + class Create < Scenario::Template + attr_writer :description + + def name=(name) + @name = "#{name}-#{SecureRandom.hex(8)}" + end + + def perform + Page::Main::Menu.act { go_to_groups } + Page::Main::Groups.act { prepare_test_namespace } + Page::Main::Menu.act { go_to_projects } + Page::Main::Projects.act { go_to_new_project } + + Page::Project::New.perform do |page| + page.choose_test_namespace + page.choose_name(@name) + page.add_description(@description) + page.create_new_project + end + end + end + end + end + end +end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb new file mode 100644 index 00000000000..341998af160 --- /dev/null +++ b/qa/qa/scenario/template.rb @@ -0,0 +1,16 @@ +module QA + module Scenario + class Template + def self.perform(*args) + new.tap do |scenario| + yield scenario if block_given? + return scenario.perform(*args) + end + end + + def perform(*_args) + raise NotImplementedError + end + end + end +end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb new file mode 100644 index 00000000000..dcd0a32d79d --- /dev/null +++ b/qa/qa/scenario/test/instance.rb @@ -0,0 +1,27 @@ +module QA + module Scenario + module Test + ## + # Run test suite against any GitLab instance, + # including staging and on-premises installation. + # + class Instance < Scenario::Template + def perform(address, tag, *files) + Specs::Config.perform do |specs| + specs.address = address + end + + ## + # Temporary CE + EE support + Scenario::Gitlab::License::Add.perform if tag.to_s == 'ee' + + Specs::Runner.perform do |specs| + files = files.any? ? files : 'qa/specs/features' + + specs.rspec('--tty', '--tag', tag.to_s, files) + end + end + end + end + end +end diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb new file mode 100644 index 00000000000..d72187fcd34 --- /dev/null +++ b/qa/qa/specs/config.rb @@ -0,0 +1,78 @@ +require 'rspec/core' +require 'capybara/rspec' +require 'capybara-webkit' +require 'capybara-screenshot/rspec' + +# rubocop:disable Metrics/MethodLength +# rubocop:disable Metrics/LineLength + +module QA + module Specs + class Config < Scenario::Template + attr_writer :address + + def initialize + @address = ENV['GITLAB_URL'] + end + + def perform + raise 'Please configure GitLab address!' unless @address + + configure_rspec! + configure_capybara! + configure_webkit! + end + + def configure_rspec! + RSpec.configure do |config| + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`. + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # Run specs in random order to surface order dependencies. + config.order = :random + Kernel.srand config.seed + + config.before(:all) do + page.current_window.resize_to(1200, 1800) + end + + config.formatter = :documentation + config.color = true + end + end + + def configure_capybara! + Capybara.configure do |config| + config.app_host = @address + config.default_driver = :webkit + config.javascript_driver = :webkit + config.default_max_wait_time = 4 + + # https://github.com/mattheworiordan/capybara-screenshot/issues/164 + config.save_path = 'tmp' + end + end + + def configure_webkit! + Capybara::Webkit.configure do |config| + config.allow_url(@address) + config.block_unknown_urls + end + rescue RuntimeError # rubocop:disable Lint/HandleExceptions + # TODO, Webkit is already configured, this make this + # configuration step idempotent, should be improved. + end + end + end +end diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb new file mode 100644 index 00000000000..ecb3f0cb68c --- /dev/null +++ b/qa/qa/specs/features/login/standard_spec.rb @@ -0,0 +1,14 @@ +module QA + feature 'standard root login', :ce, :ee do + scenario 'user logs in using credentials' do + Page::Main::Entry.act { sign_in_using_credentials } + + # TODO, since `Signed in successfully` message was removed + # this is the only way to tell if user is signed in correctly. + # + Page::Main::Menu.perform do |menu| + expect(menu).to have_personal_area + end + end + end +end diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb new file mode 100644 index 00000000000..cf4226252a6 --- /dev/null +++ b/qa/qa/specs/features/project/create_spec.rb @@ -0,0 +1,19 @@ +module QA + feature 'create a new project', :ce, :ee, :staging do + scenario 'user creates a new project' do + Page::Main::Entry.act { sign_in_using_credentials } + + Scenario::Gitlab::Project::Create.perform do |project| + project.name = 'awesome-project' + project.description = 'create awesome project test' + end + + expect(page).to have_content( + /Project \S?awesome-project\S+ was successfully created/ + ) + + expect(page).to have_content('create awesome project test') + expect(page).to have_content('The repository for this project is empty') + end + end +end diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb new file mode 100644 index 00000000000..a772dc227e3 --- /dev/null +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -0,0 +1,57 @@ +module QA + feature 'clone code from the repository', :ce, :ee, :staging do + context 'with regular account over http' do + given(:location) do + Page::Project::Show.act do + choose_repository_clone_http + repository_location + end + end + + before do + Page::Main::Entry.act { sign_in_using_credentials } + + Scenario::Gitlab::Project::Create.perform do |scenario| + scenario.name = 'project-with-code' + scenario.description = 'project for git clone tests' + end + + Git::Repository.perform do |repository| + repository.location = location + repository.use_default_credentials + + repository.act do + clone + configure_identity('GitLab QA', 'root@gitlab.com') + commit_file('test.rb', 'class Test; end', 'Add Test class') + commit_file('README.md', '# Test', 'Add Readme') + push_changes + end + end + end + + scenario 'user performs a deep clone' do + Git::Repository.perform do |repository| + repository.location = location + repository.use_default_credentials + + repository.act { clone } + + expect(repository.commits.size).to eq 2 + end + end + + scenario 'user performs a shallow clone' do + Git::Repository.perform do |repository| + repository.location = location + repository.use_default_credentials + + repository.act { shallow_clone } + + expect(repository.commits.size).to eq 1 + expect(repository.commits.first).to include 'Add Readme' + end + end + end + end +end diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb new file mode 100644 index 00000000000..4b6cb7908bb --- /dev/null +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -0,0 +1,39 @@ +module QA + feature 'push code to repository', :ce, :ee, :staging do + context 'with regular account over http' do + scenario 'user pushes code to the repository' do + Page::Main::Entry.act { sign_in_using_credentials } + + Scenario::Gitlab::Project::Create.perform do |scenario| + scenario.name = 'project_with_code' + scenario.description = 'project with repository' + end + + Git::Repository.perform do |repository| + repository.location = Page::Project::Show.act do + choose_repository_clone_http + repository_location + end + + repository.use_default_credentials + + repository.act do + clone + configure_identity('GitLab QA', 'root@gitlab.com') + add_file('README.md', '# This is test project') + commit('Add README.md') + push_changes + end + end + + Page::Project::Show.act do + wait_for_push + refresh + end + + expect(page).to have_content('README.md') + expect(page).to have_content('This is test project') + end + end + end +end diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb new file mode 100644 index 00000000000..83ae15d0995 --- /dev/null +++ b/qa/qa/specs/runner.rb @@ -0,0 +1,15 @@ +require 'rspec/core' + +module QA + module Specs + class Runner + include Scenario::Actable + + def rspec(*args) + RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| + abort if status.nonzero? + end + end + end + end +end diff --git a/qa/spec/scenario/actable_spec.rb b/qa/spec/scenario/actable_spec.rb new file mode 100644 index 00000000000..422763910e4 --- /dev/null +++ b/qa/spec/scenario/actable_spec.rb @@ -0,0 +1,47 @@ +describe QA::Scenario::Actable do + subject do + Class.new do + include QA::Scenario::Actable + + attr_accessor :something + + def do_something(arg = nil) + "some#{arg}" + end + end + end + + describe '.act' do + it 'provides means to run steps' do + result = subject.act { do_something } + + expect(result).to eq 'some' + end + + it 'supports passing variables' do + result = subject.act('thing') do |variable| + do_something(variable) + end + + expect(result).to eq 'something' + end + + it 'returns value from the last method' do + result = subject.act { 'test' } + + expect(result).to eq 'test' + end + end + + describe '.perform' do + it 'makes it possible to pass binding' do + variable = 'something' + + result = subject.perform do |object| + object.something = variable + end + + expect(result).to eq 'something' + end + end +end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb new file mode 100644 index 00000000000..c07a3234673 --- /dev/null +++ b/qa/spec/spec_helper.rb @@ -0,0 +1,19 @@ +require_relative '../qa' + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + config.disable_monkey_patching! + config.expose_dsl_globally = true + config.warnings = true + config.profile_examples = 10 + config.order = :random + Kernel.srand config.seed +end -- cgit v1.2.1 From 50cd3990c320464a8d72d522599774111f59913f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 13:05:27 +0100 Subject: Remove legacy scripts for building docker images [ci skip] --- qa/bin/docker | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100755 qa/bin/docker diff --git a/qa/bin/docker b/qa/bin/docker deleted file mode 100755 index 683e915f698..00000000000 --- a/qa/bin/docker +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh - -case "$1" in - build) - docker pull $CI_REGISTRY_IMAGE:latest - docker build --cache-from $CI_REGISTRY_IMAGE:latest \ - -t $CI_REGISTRY_IMAGE:ce-latest -t $CI_REGISTRY_IMAGE:ee-latest \ - -t $CI_REGISTRY_IMAGE:ce-nightly -t $CI_REGISTRY_IMAGE:ee-nightly \ - -t $CI_REGISTRY_IMAGE:latest . - ;; - publish) - test -n "$CI_BUILD_TOKEN" || exit 1 - docker login --username gitlab-ci-token --password $CI_BUILD_TOKEN registry.gitlab.com - docker push $CI_REGISTRY_IMAGE:latest - docker push $CI_REGISTRY_IMAGE:ce-latest - docker push $CI_REGISTRY_IMAGE:ee-latest - docker push $CI_REGISTRY_IMAGE:ee-nightly - docker push $CI_REGISTRY_IMAGE:ee-nightly - docker logout registry.gitlab.com - ;; - *) - echo "Usage: $0 [build|publish]" - exit 1 - ;; -esac -- cgit v1.2.1 From e63f3e849fd7ce14b6714b0021b2a17b3179d916 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 13:18:41 +0100 Subject: Extend README for GitLab QA files in `qa/` [ci skip] --- qa/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qa/README.md b/qa/README.md index 2b4577575c5..841e787488c 100644 --- a/qa/README.md +++ b/qa/README.md @@ -1,2 +1,5 @@ ## Integration tests for GitLab +This directory contains integration tests for GitLab. + +It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa). -- cgit v1.2.1 From adbbcc1cf43cdbea51db23338b14670da444b2b8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 13:19:47 +0100 Subject: Remove blank line from GitLab QA Dockerfile --- qa/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/qa/Dockerfile b/qa/Dockerfile index b4281c02f5a..2814a7bdef0 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -6,7 +6,6 @@ RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \ apt-get clean - WORKDIR /home/qa COPY ./ ./ -- cgit v1.2.1 From 161d0aa43dc767485c6f8a2300b6f4014c29ad7b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 13:59:11 +0100 Subject: Fix Rubocop offense and remove QA Rubocop from deps --- qa/Gemfile | 1 - qa/qa.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/qa/Gemfile b/qa/Gemfile index baafc976c4b..6bfe25ba437 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -5,4 +5,3 @@ gem 'capybara-screenshot', '~> 1.0.14' gem 'capybara-webkit', '~> 1.12.0' gem 'rake', '~> 12.0.0' gem 'rspec', '~> 3.5' -gem 'rubocop', '~> 0.47.1' diff --git a/qa/qa.rb b/qa/qa.rb index c47561bfa18..106761fd215 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -1,4 +1,4 @@ -$LOAD_PATH << File.expand_path(File.dirname(__FILE__)) +$: << File.expand_path(File.dirname(__FILE__)) module QA ## -- cgit v1.2.1 From 37d68f217b6565e2e3991c14cc46c60de8a71632 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 10:41:10 +0100 Subject: Extend README.md for GitLab QA in `qa/` directory [ci skip] --- qa/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/qa/README.md b/qa/README.md index 841e787488c..b6b5a76f1d3 100644 --- a/qa/README.md +++ b/qa/README.md @@ -3,3 +3,16 @@ This directory contains integration tests for GitLab. It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa). + +## What GitLab QA is? + +GitLab QA is an integration tests suite for GitLab. + +These are black-box and entirely click-driven integration tests you can run +against any existing instance. + +## How does it work? + +1. When we release a new version of GitLab, we build a Docker images for it. +1. Along with GitLab Docker Images we also build and publish GitLab QA images. +1. GitLab QA project uses these images to execute integration tests. -- cgit v1.2.1 From aa9aa11de549fc2486b4d37ebfad1b3bc63b07d1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Mar 2017 09:47:01 +0000 Subject: Added regression tests --- .../merge_requests/diff_notes_avatars_spec.rb | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index 7df102067d6..a6c72b0b3ac 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -23,6 +23,56 @@ feature 'Diff note avatars', feature: true, js: true do login_as user end + context 'discussion tab' do + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not show avatars on discussion tab' do + expect(page).not_to have_selector('.js-avatar-container') + expect(page).not_to have_selector('.diff-comment-avatar-holders') + end + + it 'does not render avatars after commening on discussion tab' do + click_button 'Reply...' + + page.within('.js-discussion-note-form') do + find('.note-textarea').native.send_keys('Test comment') + + click_button 'Comment' + end + + expect(page).to have_content('Test comment') + expect(page).not_to have_selector('.js-avatar-container') + expect(page).not_to have_selector('.diff-comment-avatar-holders') + end + end + + context 'commit view' do + before do + visit namespace_project_commit_path(project.namespace, project, merge_request.commits.first.id) + end + + it 'does not render avatar after commenting' do + first('.diff-line-num').trigger('mouseover') + find('.js-add-diff-note-button').click + + page.within('.js-discussion-note-form') do + find('.note-textarea').native.send_keys('test comment') + + click_button 'Comment' + + wait_for_ajax + end + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(page).to have_content('test comment') + expect(page).not_to have_selector('.js-avatar-container') + expect(page).not_to have_selector('.diff-comment-avatar-holders') + end + end + %w(inline parallel).each do |view| context "#{view} view" do before do -- cgit v1.2.1 From 2ae578c6b11bcd06b0c6dc6827e01de09f2747cc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 10:48:18 +0100 Subject: Add env var that describes QA release to Dockerfile --- qa/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qa/Dockerfile b/qa/Dockerfile index 2814a7bdef0..2a1390193e7 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,6 +1,8 @@ FROM ruby:2.3 LABEL maintainer "Grzegorz Bizon " +ENV GITLAB_RELEASE CE + RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \ apt-get update && apt-get install -y --force-yes \ libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \ -- cgit v1.2.1 From 5becdf01941e3a471def26dd82282784c58b5590 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 12:18:55 +0100 Subject: Implement GitLab QA release inflection strategy --- qa/qa.rb | 3 ++ qa/qa/runtime/release.rb | 45 ++++++++++++++++++++++++++++++ qa/spec/runtime/release_spec.rb | 62 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 qa/qa/runtime/release.rb create mode 100644 qa/spec/runtime/release_spec.rb diff --git a/qa/qa.rb b/qa/qa.rb index 106761fd215..3cc542b5c16 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -5,6 +5,7 @@ module QA # GitLab QA runtime classes, mostly singletons. # module Runtime + autoload :Release, 'qa/runtime/release' autoload :User, 'qa/runtime/user' autoload :Namespace, 'qa/runtime/namespace' end @@ -81,3 +82,5 @@ module QA autoload :Runner, 'qa/specs/runner' end end + +QA::Runtime::Release.autoloads diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb new file mode 100644 index 00000000000..d64b478a41a --- /dev/null +++ b/qa/qa/runtime/release.rb @@ -0,0 +1,45 @@ +module QA + module Runtime + ## + # Class that is responsible for plugging CE/EE extensions in, depending on + # environment variable GITLAB_RELEASE that should be present in the runtime + # environment. + # + # We need that to reduce the probability of conflicts when merging + # CE to EE. + # + class Release + UnspecifiedReleaseError = Class.new(StandardError) + + def initialize(version = ENV['GITLAB_RELEASE']) + @version = version.to_s.upcase + + unless %w[CE EE].include?(@version) + raise UnspecifiedReleaseError, 'GITLAB_RELEASE env not defined!' + end + + begin + require "#{version.downcase}/strategy" + rescue LoadError + # noop + end + end + + def has_strategy? + QA.const_defined?("#{@version}::Strategy") + end + + def strategy + QA.const_get("#{@version}::Strategy") + end + + def self.method_missing(name, *args) + @release ||= self.new + + if @release.has_strategy? + @release.strategy.public_send(name, *args) + end + end + end + end +end diff --git a/qa/spec/runtime/release_spec.rb b/qa/spec/runtime/release_spec.rb new file mode 100644 index 00000000000..4995ad48ee6 --- /dev/null +++ b/qa/spec/runtime/release_spec.rb @@ -0,0 +1,62 @@ +describe QA::Runtime::Release do + context 'when release version has extension strategy' do + subject { described_class.new('CE') } + let(:strategy) { spy('CE::Strategy') } + + before do + stub_const('QA::CE::Strategy', strategy) + end + + describe '#has_strategy?' do + it 'return true' do + expect(subject.has_strategy?).to be true + end + end + + describe '#strategy' do + it 'return the strategy constant' do + expect(subject.strategy).to eq QA::CE::Strategy + end + end + + describe 'delegated class methods' do + it 'delegates all calls to strategy class' do + described_class.some_method(1, 2) + + expect(strategy).to have_received(:some_method) + .with(1, 2) + end + end + end + + context 'when release version does not have extension strategy' do + subject { described_class.new('CE') } + + describe '#has_strategy?' do + it 'returns false' do + expect(subject.has_strategy?).to be false + end + end + + describe '#strategy' do + it 'raises error' do + expect { subject.strategy }.to raise_error(NameError) + end + end + + describe 'delegated class methods' do + it 'behaves like a null object and does nothing' do + expect { described_class.some_method(2, 3) }.not_to raise_error + end + end + end + + context 'when release version is invalid or unspecified' do + describe '#new' do + it 'raises an exception' do + expect { described_class.new(nil) } + .to raise_error(described_class::UnspecifiedReleaseError) + end + end + end +end -- cgit v1.2.1 From 92c3a9941cb519ed7ef18d09338bf4b855e3b911 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 12:22:47 +0100 Subject: Fix using release inflector to define autoloads --- qa/qa.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qa/qa.rb b/qa/qa.rb index 3cc542b5c16..bc54f20e17b 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -83,4 +83,6 @@ module QA end end -QA::Runtime::Release.autoloads +if QA::Runtime::Release.has_autoloads? + require QA::Runtime::Release.autoloads_file +end -- cgit v1.2.1 From 6373ef07c74b91b489d58cd9e20f7e5ea4c47664 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 12:29:55 +0100 Subject: Remove EE classes from GitLab QA merged into CE --- qa/qa.rb | 5 ----- qa/qa/page/admin/license.rb | 20 -------------------- qa/qa/scenario/gitlab/license/add.rb | 21 --------------------- qa/qa/scenario/test/instance.rb | 5 +++-- 4 files changed, 3 insertions(+), 48 deletions(-) delete mode 100644 qa/qa/page/admin/license.rb delete mode 100644 qa/qa/scenario/gitlab/license/add.rb diff --git a/qa/qa.rb b/qa/qa.rb index bc54f20e17b..7fe18676634 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -34,10 +34,6 @@ module QA module Project autoload :Create, 'qa/scenario/gitlab/project/create' end - - module License - autoload :Add, 'qa/scenario/gitlab/license/add' - end end end @@ -63,7 +59,6 @@ module QA module Admin autoload :Menu, 'qa/page/admin/menu' - autoload :License, 'qa/page/admin/license' end end diff --git a/qa/qa/page/admin/license.rb b/qa/qa/page/admin/license.rb deleted file mode 100644 index 4bdfae30b37..00000000000 --- a/qa/qa/page/admin/license.rb +++ /dev/null @@ -1,20 +0,0 @@ -module QA - module Page - module Admin - class License < Page::Base - def no_license? - page.has_content?('No GitLab Enterprise Edition ' \ - 'license has been provided yet') - end - - def add_new_license(key) - raise 'License key empty!' if key.to_s.empty? - - choose 'Enter license key' - fill_in 'License key', with: key - click_button 'Upload license' - end - end - end - end -end diff --git a/qa/qa/scenario/gitlab/license/add.rb b/qa/qa/scenario/gitlab/license/add.rb deleted file mode 100644 index ca5e1176959..00000000000 --- a/qa/qa/scenario/gitlab/license/add.rb +++ /dev/null @@ -1,21 +0,0 @@ -module QA - module Scenario - module Gitlab - module License - class Add < Scenario::Template - def perform - Page::Main::Entry.act { sign_in_using_credentials } - Page::Main::Menu.act { go_to_admin_area } - Page::Admin::Menu.act { go_to_license } - - Page::Admin::License.act do - add_new_license(ENV['EE_LICENSE']) if no_license? - end - - Page::Main::Menu.act { sign_out } - end - end - end - end - end -end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index dcd0a32d79d..1557fdeff34 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -12,8 +12,9 @@ module QA end ## - # Temporary CE + EE support - Scenario::Gitlab::License::Add.perform if tag.to_s == 'ee' + # Perform before hooks, which are different for CE and EE + # + Runtime::Release.perform_before_hooks Specs::Runner.perform do |specs| files = files.any? ? files : 'qa/specs/features' -- cgit v1.2.1 From 8a418f3e487cf7d89369ae4ef355c334c89ac6da Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 12:34:23 +0100 Subject: Remove unused tags in GitLab QA feature specs [ci skip] --- qa/qa/scenario/test/instance.rb | 6 ++---- qa/qa/specs/features/login/standard_spec.rb | 2 +- qa/qa/specs/features/project/create_spec.rb | 2 +- qa/qa/specs/features/repository/clone_spec.rb | 2 +- qa/qa/specs/features/repository/push_spec.rb | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index 1557fdeff34..689292bc60b 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -6,7 +6,7 @@ module QA # including staging and on-premises installation. # class Instance < Scenario::Template - def perform(address, tag, *files) + def perform(address, *files) Specs::Config.perform do |specs| specs.address = address end @@ -17,9 +17,7 @@ module QA Runtime::Release.perform_before_hooks Specs::Runner.perform do |specs| - files = files.any? ? files : 'qa/specs/features' - - specs.rspec('--tty', '--tag', tag.to_s, files) + specs.rspec('--tty', files.any? ? files : 'qa/specs/features') end end end diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb index ecb3f0cb68c..8e1ae6efa47 100644 --- a/qa/qa/specs/features/login/standard_spec.rb +++ b/qa/qa/specs/features/login/standard_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'standard root login', :ce, :ee do + feature 'standard root login' do scenario 'user logs in using credentials' do Page::Main::Entry.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index cf4226252a6..610492b9717 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'create a new project', :ce, :ee, :staging do + feature 'create a new project' do scenario 'user creates a new project' do Page::Main::Entry.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb index a772dc227e3..521bd955857 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'clone code from the repository', :ce, :ee, :staging do + feature 'clone code from the repository' do context 'with regular account over http' do given(:location) do Page::Project::Show.act do diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 4b6cb7908bb..5fe45d63d37 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'push code to repository', :ce, :ee, :staging do + feature 'push code to repository' do context 'with regular account over http' do scenario 'user pushes code to the repository' do Page::Main::Entry.act { sign_in_using_credentials } -- cgit v1.2.1 From 534f02b9340d30d9bb751b429ffdfc6351abc1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Sad=C5=82ocha?= Date: Thu, 9 Mar 2017 11:40:08 +0000 Subject: Fix miswording --- doc/ci/ssh_keys/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 49e7ac38b26..d00faaadc8b 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -30,8 +30,8 @@ This is the universal solution which works with any type of executor ## SSH keys when using the Docker executor You will first need to create an SSH key pair. For more information, follow the -instructions to [generate an SSH key](../../ssh/README.md). Do not add a comment -to the SSH key, or the `before_script` will prompt for a passphrase. +instructions to [generate an SSH key](../../ssh/README.md). Do not add a +passphrase to the SSH key, or the `before_script` will prompt for it. Then, create a new **Secret Variable** in your project settings on GitLab following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` -- cgit v1.2.1 From 5b52adcedb78287c4599ead8ffc1d00f84dc4e55 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 9 Mar 2017 13:40:57 +0200 Subject: Fix group members method for project import/export Signed-off-by: Dmitriy Zaporozhets --- lib/gitlab/import_export/project_tree_saver.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index b79be62245b..3473b466936 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -47,7 +47,13 @@ module Gitlab def group_members return [] unless @current_user.can?(:admin_group, @project.group) - MembersFinder.new(@project.project_members, @project.group).execute(@current_user) + # We need `.where.not(user_id: nil)` here otherwise when a group has an + # invitee, it would make the following query return 0 rows since a NULL + # user_id would be present in the subquery + # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values + non_null_user_ids = @project.project_members.where.not(user_id: nil).select(:user_id) + + GroupMembersFinder.new(@project.group).execute.where.not(user_id: non_null_user_ids) end end end -- cgit v1.2.1 From 90d7b95ae10472aa418b91a03eaa2f1cf29b5a30 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 9 Mar 2017 13:06:11 +0100 Subject: Move example variables output to bottom [ci skip] --- doc/ci/variables/README.md | 72 +++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 45304d0343f..03e6b5303c5 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -75,41 +75,6 @@ future GitLab releases.** | **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job | | **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job | -Example values: - -```bash -export CI_JOB_ID="50" -export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a" -export CI_COMMIT_REF_NAME="master" -export CI_REPOSITORY="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git" -export CI_COMMIT_TAG="1.0.0" -export CI_JOB_NAME="spec:other" -export CI_JOB_STAGE="test" -export CI_JOB_MANUAL="true" -export CI_JOB_TRIGGERED="true" -export CI_JOB_TOKEN="abcde-1234ABCD5678ef" -export CI_PIPELINE_ID="1000" -export CI_PROJECT_ID="34" -export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" -export CI_PROJECT_NAME="gitlab-ce" -export CI_PROJECT_NAMESPACE="gitlab-org" -export CI_PROJECT_PATH="gitlab-org/gitlab-ce" -export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce" -export CI_REGISTRY="registry.example.com" -export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce" -export CI_RUNNER_ID="10" -export CI_RUNNER_DESCRIPTION="my runner" -export CI_RUNNER_TAGS="docker, linux" -export CI_SERVER="yes" -export CI_SERVER_NAME="GitLab" -export CI_SERVER_REVISION="70606bf" -export CI_SERVER_VERSION="8.9.0" -export GITLAB_USER_ID="42" -export GITLAB_USER_EMAIL="user@example.com" -export CI_REGISTRY_USER="gitlab-ci-token" -export CI_REGISTRY_PASSWORD="longalfanumstring" -``` - ## 9.0 Renaming To follow conventions of naming across GitLab, and to futher move away from the @@ -125,7 +90,7 @@ release. | `CI_BUILD_REF_SLUG` | `CI_COMMIT_REF_SLUG` | | `CI_BUILD_NAME` | `CI_JOB_NAME` | | `CI_BUILD_STAGE` | `CI_JOB_STAGE` | -| `CI_BUILD_REPO` | `CI_REPOSITORY` | +| `CI_BUILD_REPO` | `CI_REPOSITORY_URL` | | `CI_BUILD_TRIGGERED` | `CI_PIPELINE_TRIGGERED` | | `CI_BUILD_MANUAL` | `CI_JOB_MANUAL` | | `CI_BUILD_TOKEN` | `CI_JOB_TOKEN` | @@ -381,6 +346,41 @@ job_name: - export ``` +Example values: + +```bash +export CI_JOB_ID="50" +export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a" +export CI_COMMIT_REF_NAME="master" +export CI_REPOSITORY="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git" +export CI_COMMIT_TAG="1.0.0" +export CI_JOB_NAME="spec:other" +export CI_JOB_STAGE="test" +export CI_JOB_MANUAL="true" +export CI_JOB_TRIGGERED="true" +export CI_JOB_TOKEN="abcde-1234ABCD5678ef" +export CI_PIPELINE_ID="1000" +export CI_PROJECT_ID="34" +export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" +export CI_PROJECT_NAME="gitlab-ce" +export CI_PROJECT_NAMESPACE="gitlab-org" +export CI_PROJECT_PATH="gitlab-org/gitlab-ce" +export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce" +export CI_REGISTRY="registry.example.com" +export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce" +export CI_RUNNER_ID="10" +export CI_RUNNER_DESCRIPTION="my runner" +export CI_RUNNER_TAGS="docker, linux" +export CI_SERVER="yes" +export CI_SERVER_NAME="GitLab" +export CI_SERVER_REVISION="70606bf" +export CI_SERVER_VERSION="8.9.0" +export GITLAB_USER_ID="42" +export GITLAB_USER_EMAIL="user@example.com" +export CI_REGISTRY_USER="gitlab-ci-token" +export CI_REGISTRY_PASSWORD="longalfanumstring" +``` + [ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 [runner]: https://docs.gitlab.com/runner/ [triggered]: ../triggers/README.md -- cgit v1.2.1 From 7eabb7a9641481d89ccb52b421dcbd8cd63c3bb6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Mar 2017 12:32:43 +0000 Subject: Use reduce instead of a forEach Changed an isArray check to use -1 Added comment to boards search manager to explain behaviour --- app/assets/javascripts/boards/boards_bundle.js | 15 +++++++++++++-- app/assets/javascripts/boards/components/board_card.js | 3 ++- .../javascripts/boards/components/issue_card_inner.js | 10 +++++++++- app/assets/javascripts/boards/filtered_search_boards.js | 3 +++ app/assets/javascripts/boards/models/list.js | 14 +++++++------- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 2fd1f43f02c..4d60fedaeb8 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -62,7 +62,13 @@ $(() => { created () { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); - gl.boardsFilterManager = new FilteredSearchBoards(Store.filter, true); + this.filterManager = new FilteredSearchBoards(Store.filter, true); + + // Listen for updateTokens event + this.$on('updateTokens', this.updateTokens); + }, + beforeDestroy() { + this.$off('updateTokens', this.updateTokens); }, mounted () { Store.disabled = this.disabled; @@ -81,7 +87,12 @@ $(() => { Store.addBlankState(); this.loading = false; }); - } + }, + methods: { + updateTokens() { + this.filterManager.updateTokens(); + } + }, }); gl.IssueBoardsSearch = new Vue({ diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js index 795b3cf2ec0..4b72090df31 100644 --- a/app/assets/javascripts/boards/components/board_card.js +++ b/app/assets/javascripts/boards/components/board_card.js @@ -17,7 +17,8 @@ export default { :list="list" :issue="issue" :issue-link-base="issueLinkBase" - :root-path="rootPath" /> + :root-path="rootPath" + :update-filters="true" /> `, components: { diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index dce573ed6ca..3d57ec429c6 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -23,6 +23,11 @@ type: String, required: true, }, + updateFilters: { + type: Boolean, + required: false, + default: false, + }, }, methods: { showLabel(label) { @@ -31,6 +36,8 @@ return !this.list.label || label.id !== this.list.label.id; }, filterByLabel(label, e) { + if (!this.updateFilters) return; + const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); const labelTitle = encodeURIComponent(label.title); const param = `label_name[]=${labelTitle}`; @@ -46,7 +53,8 @@ gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); Store.updateFiltersUrl(); - gl.boardsFilterManager.updateTokens(); + + gl.IssueBoardsApp.$emit('updateTokens'); }, labelStyle(label) { return { diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 3014557c440..47448b02bdd 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -4,6 +4,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { this.store = store; this.updateUrl = updateUrl; + + // Issue boards is slightly different, we handle all the requests async + // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index ad968d2120f..3251ca76b26 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -64,16 +64,14 @@ class List { } getIssues (emptyIssues = true) { - const data = { page: this.page }; - gl.issueBoards.BoardsStore.filter.path.split('&').forEach((filterParam) => { - if (filterParam === '') return; + const data = gl.issueBoards.BoardsStore.filter.path.split('&').reduce((data, filterParam) => { + if (filterParam === '') return data; const paramSplit = filterParam.split('='); const paramKeyNormalized = paramSplit[0].replace('[]', ''); const isArray = paramSplit[0].indexOf('[]'); - let value = decodeURIComponent(paramSplit[1]); - value = value.replace(/\+/g, ' '); + const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' '); - if (isArray >= 0) { + if (isArray !== -1) { if (!data[paramKeyNormalized]) { data[paramKeyNormalized] = []; } @@ -82,7 +80,9 @@ class List { } else { data[paramKeyNormalized] = value; } - }); + + return data; + }, { page: this.page }); if (this.label && data.label_name) { data.label_name = data.label_name.filter(label => label !== this.label.title); -- cgit v1.2.1 From 235e225ca7d5884e507fe8eabc7b48a33675ed91 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 9 Mar 2017 13:56:34 +0100 Subject: Add confirmation dialogs guidelines in UX copy [ci skip] --- doc/development/ux_guide/copy.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index ead79ba6a10..794c8eb6bfe 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -167,6 +167,15 @@ A **comment** is a written piece of text that users of GitLab can create. Commen #### Discussion A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved. +## Confirmation dialogs + +- Destruction buttons should be clear and always say what they are destroying. + E.g., `Delete page` instead of just `Delete`. +- If the copy describes another action the user can take instead of the + destructive one, provide a way for them to do that as a secondary button. +- Avoid the word `cancel` or `canceled` in the descriptive copy. It can be + confusing when you then see the `Cancel` button. + --- Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons]. -- cgit v1.2.1 From 53220a28b80f0ac5259b062855d01078bdc4d6c8 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 9 Mar 2017 12:58:46 +0000 Subject: Update ACE to 1.2.6 Fixes input problems with compose key in Chrome 53+ - has been reported for Chinese and Hangul at least. --- Gemfile.lock | 2 +- changelogs/unreleased/update-ace.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/update-ace.yml diff --git a/Gemfile.lock b/Gemfile.lock index 62388628eaa..c60c045a4c2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ GEM remote: https://rubygems.org/ specs: RedCloth (4.3.2) - ace-rails-ap (4.1.0) + ace-rails-ap (4.1.2) actionmailer (4.2.8) actionpack (= 4.2.8) actionview (= 4.2.8) diff --git a/changelogs/unreleased/update-ace.yml b/changelogs/unreleased/update-ace.yml new file mode 100644 index 00000000000..dbe476e3ae0 --- /dev/null +++ b/changelogs/unreleased/update-ace.yml @@ -0,0 +1,4 @@ +--- +title: Update code editor (ACE) to 1.2.6, to fix input problems with compose key +merge_request: +author: -- cgit v1.2.1 From 429eb466ea910102cb402792ee12661dd9977215 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 9 Mar 2017 13:03:08 +0000 Subject: Prevent dropdown from closing when user clicks in a build. --- app/assets/javascripts/mini_pipeline_graph_dropdown.js | 16 ++++++++++++++++ spec/javascripts/mini_pipeline_graph_dropdown_spec.js | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 2145e531331..483c201305a 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -21,6 +21,8 @@ this.container = opts.container || ''; this.dropdownListSelector = '.js-builds-dropdown-container'; this.getBuildsList = this.getBuildsList.bind(this); + + this.stopDropdownClickPropagation(); } /** @@ -31,6 +33,20 @@ $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); } + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(document).on('click', `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`, (e) => { + e.stopPropagation(); + }); + } + /** * For the clicked stage, renders the given data in the dropdown list. * diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index 7cdade01e00..f6b3dc87cd8 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -46,6 +46,21 @@ require('~/mini_pipeline_graph_dropdown'); document.querySelector('.js-builds-dropdown-button').click(); expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); }); + + it('should not close when user uses cmd/ctrl + click', () => { + spyOn($, 'ajax').and.callFake(function (params) { + params.success({ + html: '\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" href="#"\u003e\u003cspan class="ci-status-icon ci-status-icon-failed"\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ebuild\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" href="#"\u003e\u003c/a\u003e\n\u003c/li\u003e\n', + }); + }); + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + + document.querySelector('.js-builds-dropdown-button').click(); + + document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); + + expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); + }); }); }); })(); -- cgit v1.2.1 From c2c17811b2b6c10a6079a7cc76f872e773e4b0b5 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 2 Mar 2017 14:48:20 +0100 Subject: Instructions for installing google-protobuf from source --- doc/install/google-protobuf.md | 26 ++++++++++++++++++++++++++ doc/install/installation.md | 6 ++++++ 2 files changed, 32 insertions(+) create mode 100644 doc/install/google-protobuf.md diff --git a/doc/install/google-protobuf.md b/doc/install/google-protobuf.md new file mode 100644 index 00000000000..a531b4519b3 --- /dev/null +++ b/doc/install/google-protobuf.md @@ -0,0 +1,26 @@ +# Installing a locally compiled google-protobuf gem + +First we must find the exact version of google-protobuf that your +GitLab installation requires. + + cd /home/git/gitlab + + # Only one of the following two commands will print something. It + # will look like: * google-protobuf (3.2.0) + bundle list | grep google-protobuf + bundle check | grep google-protobuf + +Below we use `3.2.0` as an example. Replace it with the version number +you found above. + + cd /home/git/gitlab + sudo -u git -H gem install google-protobuf --version 3.2.0 --platform ruby + +Finally, you can test whether google-protobuf loads correctly. The +following should print 'OK'. + + sudo -u git -H bundle exec ruby -rgoogle/protobuf -e 'puts :OK' + +If the `gem install` command fails you may need to install developer +tools. On Debian: `apt-get install build-essential libgmp-dev`, on +Centos/RedHat `yum groupinstall 'Development Tools'`. diff --git a/doc/install/installation.md b/doc/install/installation.md index bb4141c6cd3..8e74970b8e9 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -658,6 +658,12 @@ misconfigured gitlab-workhorse instance. Double-check that you've [installed Go](#3-go), [installed gitlab-workhorse](#install-gitlab-workhorse), and correctly [configured Nginx](#site-configuration). +### google-protobuf "LoadError: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.14' not found" + +This can happen on some platforms for some versions of the +google-protobuf gem. The workaround is to [install a source-only +version of this gem](google-protobuf.md). + [RVM]: https://rvm.io/ "RVM Homepage" [rbenv]: https://github.com/sstephenson/rbenv "rbenv on GitHub" [chruby]: https://github.com/postmodern/chruby "chruby on GitHub" -- cgit v1.2.1 From 175a3dfda00fb5a2bf1703803277ee4abb721baf Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 15:04:05 +0100 Subject: Fix GitLab QA release inflector strategy --- qa/qa/runtime/release.rb | 2 +- qa/spec/runtime/release_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb index d64b478a41a..e3da00a1881 100644 --- a/qa/qa/runtime/release.rb +++ b/qa/qa/runtime/release.rb @@ -19,7 +19,7 @@ module QA end begin - require "#{version.downcase}/strategy" + require "qa/#{version.downcase}/strategy" rescue LoadError # noop end diff --git a/qa/spec/runtime/release_spec.rb b/qa/spec/runtime/release_spec.rb index 4995ad48ee6..97f0b7e3c89 100644 --- a/qa/spec/runtime/release_spec.rb +++ b/qa/spec/runtime/release_spec.rb @@ -5,6 +5,7 @@ describe QA::Runtime::Release do before do stub_const('QA::CE::Strategy', strategy) + stub_const('QA::EE::Strategy', strategy) end describe '#has_strategy?' do @@ -32,6 +33,11 @@ describe QA::Runtime::Release do context 'when release version does not have extension strategy' do subject { described_class.new('CE') } + before do + hide_const('QA::CE::Strategy') + hide_const('QA::EE::Strategy') + end + describe '#has_strategy?' do it 'returns false' do expect(subject.has_strategy?).to be false -- cgit v1.2.1 From 88750f83ba11a5fd3eb26f17d02941ff37fa2d85 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 9 Mar 2017 09:37:46 -0600 Subject: Remove inline-block styling from dropdown avatars --- app/views/shared/issuable/_search_bar.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 32128f3b3dc..afd37e76c79 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -39,7 +39,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } .dropdown-user-details %span {{name}} @@ -54,7 +54,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } .dropdown-user-details %span {{name}} -- cgit v1.2.1 From c444f2588ec350f4a9966af61d05718db4fdca01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 9 Mar 2017 16:04:21 +0100 Subject: Document "How to get your MR reviewed, approved, and merged" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable --- CONTRIBUTING.md | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3cbc826e6db..57d94dad672 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -299,10 +299,13 @@ request is as follows: 1. [Generate a changelog entry with `bin/changelog`][changelog] 1. If you are writing documentation, make sure to follow the [documentation styleguide][doc-styleguide] -1. If you have multiple commits please combine them into one commit by - [squashing them][git-squash] +1. If you have multiple commits please combine them into a few logically + organized commits by [squashing them][git-squash] 1. Push the commit(s) to your fork 1. Submit a merge request (MR) to the `master` branch +1. Leave the approvals settings as they are: + 1. Your merge request needs at least 1 approval + 1. You don't have to select any approvers 1. The MR title should describe the change you want to make 1. The MR description should give a motive for your change and the method you used to achieve it. @@ -345,13 +348,31 @@ The ['How to get faster PR reviews' document of Kubernetes](https://github.com/k For examples of feedback on merge requests please look at already [closed merge requests][closed-merge-requests]. If you would like quick feedback -on your merge request feel free to mention one of the Merge Marshalls in the -[core team] or one of the [Merge request coaches](https://about.gitlab.com/team/). +on your merge request feel free to mention someone from the [core team] or one +of the [Merge request coaches][team]. Please ensure that your merge request meets the contribution acceptance criteria. When having your code reviewed and when reviewing merge requests please take the [code review guidelines](doc/development/code_review.md) into account. +### Getting your merge request reviewed, approved, and merged + +There are a few rules to get your merge request accepted: + +1. Your merge request should only be **merged by a [maintainer][team]**. + 1. If your merge request includes only backend changes [^1], it must be + **approved by a [backend maintainer][team]**. + 1. If your merge request includes only frontend changes [^1], it must be + **approved by a [frontend maintainer][team]**. + 1. If your merge request includes frontend and backend changes [^1], it must + be approved by a frontend **and** a backend maintainer. +1. To lower the amount of merge requests maintainers need to review, you can + ask or assign any [reviewers][team] for a first review. + 1. If you need some guidance (e.g. it's your first merge request), feel free + to ask one of the [Merge request coaches][team]. + 1. The reviewer will assign the merge request to a maintainer once the + reviewer is satisfied with the state of the merge request. + ### Contribution acceptance criteria 1. The change is as small as possible @@ -489,6 +510,7 @@ This Code of Conduct is adapted from the [Contributor Covenant][contributor-cove available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/). [core team]: https://about.gitlab.com/core-team/ +[team]: https://about.gitlab.com/team/ [getting-help]: https://about.gitlab.com/getting-help/ [codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq [accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?assignee_id=0&label_name[]=Accepting%20Merge%20Requests&sort=weight_asc @@ -513,3 +535,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" [UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/ [license-finder-doc]: doc/development/licensing.md + +[^1]: Specs other than JavaScript specs are considered backend code. Haml + changes are considered backend code if they include Ruby code other than just + pure HTML. -- cgit v1.2.1 From 5a3b2f83b09a58d487a85676563b38008bd2ff86 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 9 Mar 2017 18:26:15 +0000 Subject: Uses vanilla JS to listen to click event Removes `:visible` from selector Adds CHANGELOG entry Removes iife --- app/assets/javascripts/dispatcher.js | 3 +- app/assets/javascripts/merge_request_widget.js | 5 +- .../javascripts/mini_pipeline_graph_dropdown.js | 165 ++++++++++----------- .../unreleased/24166-close-builds-dropdown.yml | 4 + .../mini_pipeline_graph_dropdown_spec.js | 16 +- 5 files changed, 97 insertions(+), 96 deletions(-) create mode 100644 changelogs/unreleased/24166-close-builds-dropdown.yml diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 017980271b1..7b9b9123c31 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -39,6 +39,7 @@ import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import GroupsList from './groups_list'; import ProjectsList from './projects_list'; +import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; const ShortcutsBlob = require('./shortcuts_blob'); const UserCallout = require('./user_callout'); @@ -181,7 +182,7 @@ const UserCallout = require('./user_callout'); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:commit:pipelines': - new gl.MiniPipelineGraph({ + new MiniPipelineGraph({ container: '.js-pipeline-table', }).bindEvents(); break; diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index 5f1bd474a0c..a80ae128fa4 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -4,6 +4,7 @@ /* global merge_request_widget */ require('./smart_interval'); +const MiniPipelineGraph = require('./mini_pipeline_graph_dropdown').default; ((global) => { var 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; }; @@ -285,8 +286,8 @@ require('./smart_interval'); }; MergeRequestWidget.prototype.initMiniPipelineGraph = function() { - new gl.MiniPipelineGraph({ - container: '.js-pipeline-inline-mr-widget-graph:visible', + new MiniPipelineGraph({ + container: '.js-pipeline-inline-mr-widget-graph', }).bindEvents(); }; diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 483c201305a..3029ec17a37 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -15,97 +15,92 @@ * * */ -(() => { - class MiniPipelineGraph { - constructor(opts = {}) { - this.container = opts.container || ''; - this.dropdownListSelector = '.js-builds-dropdown-container'; - this.getBuildsList = this.getBuildsList.bind(this); - this.stopDropdownClickPropagation(); - } - - /** - * Adds the event listener when the dropdown is opened. - * All dropdown events are fired at the .dropdown-menu's parent element. - */ - bindEvents() { - $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); - } +export default class MiniPipelineGraph { + constructor(opts = {}) { + this.container = opts.container || ''; + this.dropdownListSelector = '.js-builds-dropdown-container'; + this.getBuildsList = this.getBuildsList.bind(this); + } - /** - * When the user right clicks or cmd/ctrl + click in the job name - * the dropdown should not be closed and the link should open in another tab, - * so we stop propagation of the click event inside the dropdown. - * - * Since this component is rendered multiple times per page we need to guarantee we only - * target the click event of this component. - */ - stopDropdownClickPropagation() { - $(document).on('click', `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`, (e) => { - e.stopPropagation(); - }); - } + /** + * Adds the event listener when the dropdown is opened. + * All dropdown events are fired at the .dropdown-menu's parent element. + */ + bindEvents() { + $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); + } - /** - * For the clicked stage, renders the given data in the dropdown list. - * - * @param {HTMLElement} stageContainer - * @param {Object} data - */ - renderBuildsList(stageContainer, data) { - const dropdownContainer = stageContainer.parentElement.querySelector( - `${this.dropdownListSelector} .js-builds-dropdown-list`, - ); + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + document.querySelector(`${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`).addEventListener('click', (e) => { + e.stopPropagation(); + }); + } - dropdownContainer.innerHTML = data; - } + /** + * For the clicked stage, renders the given data in the dropdown list. + * + * @param {HTMLElement} stageContainer + * @param {Object} data + */ + renderBuildsList(stageContainer, data) { + const dropdownContainer = stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-list`, + ); - /** - * For the clicked stage, gets the list of builds. - * - * All dropdown events have a relatedTarget property, - * whose value is the toggling anchor element. - * - * @param {Object} e bootstrap dropdown event - * @return {Promise} - */ - getBuildsList(e) { - const button = e.relatedTarget; - const endpoint = button.dataset.stageEndpoint; + dropdownContainer.innerHTML = data; + } - return $.ajax({ - dataType: 'json', - type: 'GET', - url: endpoint, - beforeSend: () => { - this.renderBuildsList(button, ''); - this.toggleLoading(button); - }, - success: (data) => { - this.toggleLoading(button); - this.renderBuildsList(button, data.html); - }, - error: () => { - this.toggleLoading(button); - new Flash('An error occurred while fetching the builds.', 'alert'); - }, - }); - } + /** + * For the clicked stage, gets the list of builds. + * + * All dropdown events have a relatedTarget property, + * whose value is the toggling anchor element. + * + * @param {Object} e bootstrap dropdown event + * @return {Promise} + */ + getBuildsList(e) { + const button = e.relatedTarget; + const endpoint = button.dataset.stageEndpoint; - /** - * Toggles the visibility of the loading icon. - * - * @param {HTMLElement} stageContainer - * @return {type} - */ - toggleLoading(stageContainer) { - stageContainer.parentElement.querySelector( - `${this.dropdownListSelector} .js-builds-dropdown-loading`, - ).classList.toggle('hidden'); - } + return $.ajax({ + dataType: 'json', + type: 'GET', + url: endpoint, + beforeSend: () => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + }, + success: (data) => { + this.toggleLoading(button); + this.renderBuildsList(button, data.html); + this.stopDropdownClickPropagation(); + }, + error: () => { + this.toggleLoading(button); + new Flash('An error occurred while fetching the builds.', 'alert'); + }, + }); } - window.gl = window.gl || {}; - window.gl.MiniPipelineGraph = MiniPipelineGraph; -})(); + /** + * Toggles the visibility of the loading icon. + * + * @param {HTMLElement} stageContainer + * @return {type} + */ + toggleLoading(stageContainer) { + stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-loading`, + ).classList.toggle('hidden'); + } +} diff --git a/changelogs/unreleased/24166-close-builds-dropdown.yml b/changelogs/unreleased/24166-close-builds-dropdown.yml new file mode 100644 index 00000000000..c57ffed6b45 --- /dev/null +++ b/changelogs/unreleased/24166-close-builds-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Prevent builds dropdown to close when the user clicks in a build +merge_request: +author: diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index f6b3dc87cd8..4e8637054ba 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -1,7 +1,7 @@ /* eslint-disable no-new */ -require('~/flash'); -require('~/mini_pipeline_graph_dropdown'); +import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; +import '~/flash'; (() => { describe('Mini Pipeline Graph Dropdown', () => { @@ -13,7 +13,7 @@ require('~/mini_pipeline_graph_dropdown'); describe('When is initialized', () => { it('should initialize without errors when no options are given', () => { - const miniPipelineGraph = new window.gl.MiniPipelineGraph(); + const miniPipelineGraph = new MiniPipelineGraph(); expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container'); }); @@ -21,7 +21,7 @@ require('~/mini_pipeline_graph_dropdown'); it('should set the container as the given prop', () => { const container = '.foo'; - const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container }); + const miniPipelineGraph = new MiniPipelineGraph({ container }); expect(miniPipelineGraph.container).toEqual(container); }); @@ -29,9 +29,9 @@ require('~/mini_pipeline_graph_dropdown'); describe('When dropdown is clicked', () => { it('should call getBuildsList', () => { - const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); + const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); @@ -41,7 +41,7 @@ require('~/mini_pipeline_graph_dropdown'); it('should make a request to the endpoint provided in the html', () => { const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); @@ -53,7 +53,7 @@ require('~/mini_pipeline_graph_dropdown'); html: '\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" href="#"\u003e\u003cspan class="ci-status-icon ci-status-icon-failed"\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ebuild\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" href="#"\u003e\u003c/a\u003e\n\u003c/li\u003e\n', }); }); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); -- cgit v1.2.1 From fb25327e8085c872402d7703f3aea4c42048a0ed Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 8 Mar 2017 12:43:17 -0600 Subject: Convert data attributes in search_bar.html.haml --- app/views/shared/issuable/_search_bar.html.haml | 34 ++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index afd37e76c79..f8123846596 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -14,18 +14,18 @@ .scroll-container %ul.tokens-container.list-unstyled %li.input-token - %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) } + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: 'filtered-search', 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-action' => 'submit' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { action: 'submit' } } %button.btn.btn-link = icon('search') %span Keep typing and press Enter - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link -# Encapsulate static class name `{{icon}}` inside #{} to bypass @@ -36,50 +36,50 @@ %span.js-filter-tag.dropdown-light-content {{tag}} #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } } - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } .dropdown-user-details %span {{name}} %span.dropdown-light-content @{{username}} #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Assignee %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } .dropdown-user-details %span {{name}} %span.dropdown-light-content @{{username}} #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Milestone - %li.filter-dropdown-item{ 'data-value' => 'upcoming' } + %li.filter-dropdown-item{ data: { value: 'upcoming' } } %button.btn.btn-link Upcoming %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.js-data-value {{title}} #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Label %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ style: 'background: {{color}}' } -- cgit v1.2.1 From 12a0d5a2bca66dc332f160f09a993fef42d356d5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 9 Mar 2017 18:39:53 +0000 Subject: Use harmony syntax instead of cjs --- app/assets/javascripts/merge_request_widget.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index a80ae128fa4..27d78ec776c 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -3,8 +3,8 @@ /* global notifyPermissions */ /* global merge_request_widget */ -require('./smart_interval'); -const MiniPipelineGraph = require('./mini_pipeline_graph_dropdown').default; +import './smart_interval'; +import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ((global) => { var 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; }; -- cgit v1.2.1 From dd309469997558541debd36aa7a2ea7e8cbece5d Mon Sep 17 00:00:00 2001 From: George Andrinopoulos Date: Thu, 9 Mar 2017 18:43:07 +0000 Subject: Order milestone issues by position ascending in api --- app/models/concerns/issuable.rb | 2 ++ app/views/shared/milestones/_issuables.html.haml | 2 +- ...-fix-milestone-issues-position-order-in-api.yml | 4 +++ lib/api/milestones.rb | 6 ++-- spec/requests/api/milestones_spec.rb | 32 ++++++++++++++++++++-- 5 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 37c727b5d9f..3cf4c67d7e7 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -63,6 +63,7 @@ module Issuable scope :authored, ->(user) { where(author_id: user) } scope :assigned_to, ->(u) { where(assignee_id: u.id)} scope :recent, -> { reorder(id: :desc) } + scope :order_position_asc, -> { reorder(position: :asc) } scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } @@ -144,6 +145,7 @@ module Issuable when 'downvotes_desc' then order_downvotes_desc when 'upvotes_desc' then order_upvotes_desc when 'priority' then order_labels_priority(excluded_labels: excluded_labels) + when 'position_asc' then order_position_asc else order_by(method) end diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index a93cbd1041f..8af3bd597c5 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -13,6 +13,6 @@ - class_prefix = dom_class(issuables).pluralize %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } = render partial: 'shared/milestones/issuable', - collection: issuables.sort_by(&:position), + collection: issuables.order_position_asc, as: :issuable, locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name } diff --git a/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml new file mode 100644 index 00000000000..0177394aa0f --- /dev/null +++ b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml @@ -0,0 +1,4 @@ +--- +title: Order milestone issues by position ascending in api +merge_request: 9635 +author: George Andrinopoulos diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index e7f7edd95c7..abd263c1dfc 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -116,7 +116,8 @@ module API finder_params = { project_id: user_project.id, - milestone_title: milestone.title + milestone_title: milestone.title, + sort: 'position_asc' } issues = IssuesFinder.new(current_user, finder_params).execute @@ -138,7 +139,8 @@ module API finder_params = { project_id: user_project.id, - milestone_id: milestone.id + milestone_id: milestone.id, + sort: 'position_asc' } merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 3bb8b6fdbeb..7fb728fed6f 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -243,8 +243,8 @@ describe API::Milestones, api: true do describe 'confidential issues' do let(:public_project) { create(:empty_project, :public) } let(:milestone) { create(:milestone, project: public_project) } - let(:issue) { create(:issue, project: public_project) } - let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } + let(:issue) { create(:issue, project: public_project, position: 2) } + let(:confidential_issue) { create(:issue, confidential: true, project: public_project, position: 1) } before do public_project.team << [user, :developer] @@ -283,11 +283,24 @@ describe API::Milestones, api: true do expect(json_response.size).to eq(1) expect(json_response.map { |issue| issue['id'] }).to include(issue.id) end + + it 'returns issues ordered by position asc' do + get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.first['id']).to eq(confidential_issue.id) + expect(json_response.second['id']).to eq(issue.id) + end end end describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do - let(:merge_request) { create(:merge_request, source_project: project) } + let(:merge_request) { create(:merge_request, source_project: project, position: 2) } + let(:another_merge_request) { create(:merge_request, :simple, source_project: project, position: 1) } + before do milestone.merge_requests << merge_request end @@ -320,5 +333,18 @@ describe API::Milestones, api: true do expect(response).to have_http_status(401) end + + it 'returns merge_requests ordered by position asc' do + milestone.merge_requests << another_merge_request + + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.first['id']).to eq(another_merge_request.id) + expect(json_response.second['id']).to eq(merge_request.id) + end end end -- cgit v1.2.1 From a7a1a60421f49c1459d46a4092008af12127994c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 9 Mar 2017 18:51:52 +0000 Subject: Target all build links inside a dropdown. Use jQuery to handle event delegation. --- app/assets/javascripts/merge_request_widget.js | 2 +- app/assets/javascripts/mini_pipeline_graph_dropdown.js | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index 27d78ec776c..66cc270ab4d 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -287,7 +287,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; MergeRequestWidget.prototype.initMiniPipelineGraph = function() { new MiniPipelineGraph({ - container: '.js-pipeline-inline-mr-widget-graph', + container: '.js-pipeline-inline-mr-widget-graph:visible', }).bindEvents(); }; diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 3029ec17a37..9c58c465001 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -40,9 +40,13 @@ export default class MiniPipelineGraph { * target the click event of this component. */ stopDropdownClickPropagation() { - document.querySelector(`${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`).addEventListener('click', (e) => { - e.stopPropagation(); - }); + $(document).on( + 'click', + `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`, + (e) => { + e.stopPropagation(); + }, + ); } /** -- cgit v1.2.1 From 3f5919e2c44ac7b18f06647342476ad5c3d757ba Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 9 Mar 2017 11:55:18 -0600 Subject: Add frequently used emojis back to awards menu Thanks @filipa for the shout` --- app/assets/javascripts/awards_handler.js | 30 ++++++------- .../add-frequently-used-emojis-back-to-menu.yml | 4 ++ spec/javascripts/awards_handler_spec.js | 50 +++++++++++++++++++--- 3 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 54836efdf29..8a077f0081a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -45,12 +45,12 @@ function buildCategoryMap() { }); } -function renderCategory(name, emojiList) { +function renderCategory(name, emojiList, opts = {}) { return `
${name}
-
    +
      ${emojiList.map(emojiName => `
    • "; - project_uploads_path = window.project_uploads_path || null; - max_file_size = gon.max_file_size || 10; - form_textarea = $(form).find(".js-gfm-input"); - form_textarea.wrap("
      "); - form_textarea.on('paste', (function(_this) { - return function(event) { - return handlePaste(event); - }; - })(this)); - $mdArea = $(form_textarea).closest('.md-area'); - $(form).setupMarkdownPreview(); - form_dropzone = $(form).find('.div-dropzone'); - form_dropzone.parent().addClass("div-dropzone-wrapper"); - form_dropzone.append(divHover); - form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); - form_dropzone.append(divSpinner); - form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); - form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); - dropzone = form_dropzone.dropzone({ - url: project_uploads_path, - dictDefaultMessage: "", - clickable: true, - paramName: "file", - maxFilesize: max_file_size, - uploadMultiple: false, - headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - }, - previewContainer: false, - processing: function() { - return $(".div-dropzone-alert").alert("close"); - }, - dragover: function() { - $mdArea.addClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0.7); - }, - dragleave: function() { - $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); - }, - drop: function() { - $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); - form_textarea.focus(); - }, - success: function(header, response) { - pasteText(response.link.markdown); - }, - error: function(temp) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); - } - }, - totaluploadprogress: function(totalUploadProgress) { - uploadProgress.text(Math.round(totalUploadProgress) + "%"); - }, - sending: function() { - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0.7, - "display": "inherit" - }); - }, - queuecomplete: function() { - uploadProgress.text(""); - $(".dz-preview").remove(); - $(".markdown-area").trigger("input"); - $(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); - } - }); - child = $(dropzone[0]).children("textarea"); - handlePaste = function(event) { - var filename, image, pasteEvent, text; - pasteEvent = event.originalEvent; - if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { - image = isImage(pasteEvent); - if (image) { - event.preventDefault(); - filename = getFilename(pasteEvent) || "image.png"; - text = "{{" + filename + "}}"; - pasteText(text); - return uploadFile(image.getAsFile(), filename); - } - } +window.DropzoneInput = (function() { + function DropzoneInput(form) { + var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress; + Dropzone.autoDiscover = false; + alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; + alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; + divHover = "
      "; + divSpinner = "
      "; + divAlert = "
      "; + iconPaperclip = ""; + iconSpinner = ""; + uploadProgress = $("
      "); + btnAlert = ""; + project_uploads_path = window.project_uploads_path || null; + max_file_size = gon.max_file_size || 10; + form_textarea = $(form).find(".js-gfm-input"); + form_textarea.wrap("
      "); + form_textarea.on('paste', (function(_this) { + return function(event) { + return handlePaste(event); }; - isImage = function(data) { - var i, item; - i = 0; - while (i < data.clipboardData.items.length) { - item = data.clipboardData.items[i]; - if (item.type.indexOf("image") !== -1) { - return item; - } - i += 1; - } - return false; - }; - pasteText = function(text) { - var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; - var formattedText = text + "\n\n"; - caretStart = $(child)[0].selectionStart; - caretEnd = $(child)[0].selectionEnd; - textEnd = $(child).val().length; - beforeSelection = $(child).val().substring(0, caretStart); - afterSelection = $(child).val().substring(caretEnd, textEnd); - $(child).val(beforeSelection + formattedText + afterSelection); - child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); - return form_textarea.trigger("input"); - }; - getFilename = function(e) { - var value; - if (window.clipboardData && window.clipboardData.getData) { - value = window.clipboardData.getData("Text"); - } else if (e.clipboardData && e.clipboardData.getData) { - value = e.clipboardData.getData("text/plain"); + })(this)); + $mdArea = $(form_textarea).closest('.md-area'); + $(form).setupMarkdownPreview(); + form_dropzone = $(form).find('.div-dropzone'); + form_dropzone.parent().addClass("div-dropzone-wrapper"); + form_dropzone.append(divHover); + form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); + form_dropzone.append(divSpinner); + form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); + form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); + form_dropzone.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" + }); + dropzone = form_dropzone.dropzone({ + url: project_uploads_path, + dictDefaultMessage: "", + clickable: true, + paramName: "file", + maxFilesize: max_file_size, + uploadMultiple: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + previewContainer: false, + processing: function() { + return $(".div-dropzone-alert").alert("close"); + }, + dragover: function() { + $mdArea.addClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0.7); + }, + dragleave: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + }, + drop: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + form_textarea.focus(); + }, + success: function(header, response) { + pasteText(response.link.markdown); + }, + error: function(temp) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); } - value = value.split("\r"); - return value.first(); - }; - uploadFile = function(item, filename) { - var formData; - formData = new FormData(); - formData.append("file", item, filename); - return $.ajax({ - url: project_uploads_path, - type: "POST", - data: formData, - dataType: "json", - processData: false, - contentType: false, - headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - }, - beforeSend: function() { - showSpinner(); - return closeAlertMessage(); - }, - success: function(e, textStatus, response) { - return insertToTextArea(filename, response.responseJSON.link.markdown); - }, - error: function(response) { - return showError(response.responseJSON.message); - }, - complete: function() { - return closeSpinner(); - } - }); - }; - insertToTextArea = function(filename, url) { - return $(child).val(function(index, val) { - return val.replace("{{" + filename + "}}", url + "\n"); - }); - }; - appendToTextArea = function(url) { - return $(child).val(function(index, val) { - return val + url + "\n"; - }); - }; - showSpinner = function(e) { - return form.find(".div-dropzone-spinner").css({ + }, + totaluploadprogress: function(totalUploadProgress) { + uploadProgress.text(Math.round(totalUploadProgress) + "%"); + }, + sending: function() { + form_dropzone.find(".div-dropzone-spinner").css({ "opacity": 0.7, "display": "inherit" }); - }; - closeSpinner = function() { - return form.find(".div-dropzone-spinner").css({ + }, + queuecomplete: function() { + uploadProgress.text(""); + $(".dz-preview").remove(); + $(".markdown-area").trigger("input"); + $(".div-dropzone-spinner").css({ "opacity": 0, "display": "none" }); - }; - showError = function(message) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - return $(".div-dropzone-alert").append(btnAlert + message); + } + }); + child = $(dropzone[0]).children("textarea"); + handlePaste = function(event) { + var filename, image, pasteEvent, text; + pasteEvent = event.originalEvent; + if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { + image = isImage(pasteEvent); + if (image) { + event.preventDefault(); + filename = getFilename(pasteEvent) || "image.png"; + text = "{{" + filename + "}}"; + pasteText(text); + return uploadFile(image.getAsFile(), filename); } - }; - closeAlertMessage = function() { - return form.find(".div-dropzone-alert").alert("close"); - }; - form.find(".markdown-selector").click(function(e) { - e.preventDefault(); - $(this).closest('.gfm-form').find('.div-dropzone').click(); + } + }; + isImage = function(data) { + var i, item; + i = 0; + while (i < data.clipboardData.items.length) { + item = data.clipboardData.items[i]; + if (item.type.indexOf("image") !== -1) { + return item; + } + i += 1; + } + return false; + }; + pasteText = function(text) { + var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; + var formattedText = text + "\n\n"; + caretStart = $(child)[0].selectionStart; + caretEnd = $(child)[0].selectionEnd; + textEnd = $(child).val().length; + beforeSelection = $(child).val().substring(0, caretStart); + afterSelection = $(child).val().substring(caretEnd, textEnd); + $(child).val(beforeSelection + formattedText + afterSelection); + child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); + return form_textarea.trigger("input"); + }; + getFilename = function(e) { + var value; + if (window.clipboardData && window.clipboardData.getData) { + value = window.clipboardData.getData("Text"); + } else if (e.clipboardData && e.clipboardData.getData) { + value = e.clipboardData.getData("text/plain"); + } + value = value.split("\r"); + return value.first(); + }; + uploadFile = function(item, filename) { + var formData; + formData = new FormData(); + formData.append("file", item, filename); + return $.ajax({ + url: project_uploads_path, + type: "POST", + data: formData, + dataType: "json", + processData: false, + contentType: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + beforeSend: function() { + showSpinner(); + return closeAlertMessage(); + }, + success: function(e, textStatus, response) { + return insertToTextArea(filename, response.responseJSON.link.markdown); + }, + error: function(response) { + return showError(response.responseJSON.message); + }, + complete: function() { + return closeSpinner(); + } + }); + }; + insertToTextArea = function(filename, url) { + return $(child).val(function(index, val) { + return val.replace("{{" + filename + "}}", url + "\n"); + }); + }; + appendToTextArea = function(url) { + return $(child).val(function(index, val) { + return val + url + "\n"; + }); + }; + showSpinner = function(e) { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0.7, + "display": "inherit" + }); + }; + closeSpinner = function() { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" }); - } + }; + showError = function(message) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + return $(".div-dropzone-alert").append(btnAlert + message); + } + }; + closeAlertMessage = function() { + return form.find(".div-dropzone-alert").alert("close"); + }; + form.find(".markdown-selector").click(function(e) { + e.preventDefault(); + $(this).closest('.gfm-form').find('.div-dropzone').click(); + }); + } - return DropzoneInput; - })(); -}).call(window); + return DropzoneInput; +})(); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 9169fcd7328..fdbb4644971 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -2,203 +2,202 @@ /* global dateFormat */ /* global Pikaday */ -(function(global) { - class DueDateSelect { - constructor({ $dropdown, $loading } = {}) { - const $dropdownParent = $dropdown.closest('.dropdown'); - const $block = $dropdown.closest('.block'); - this.$loading = $loading; - this.$dropdown = $dropdown; - this.$dropdownParent = $dropdownParent; - this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); - this.$block = $block; - this.$selectbox = $dropdown.closest('.selectbox'); - this.$value = $block.find('.value'); - this.$valueContent = $block.find('.value-content'); - this.$sidebarValue = $('.js-due-date-sidebar-value', $block); - this.fieldName = $dropdown.data('field-name'), - this.abilityName = $dropdown.data('ability-name'), - this.issueUpdateURL = $dropdown.data('issue-update'); - - this.rawSelectedDate = null; - this.displayedDate = null; - this.datePayload = null; - - this.initGlDropdown(); - this.initRemoveDueDate(); - this.initDatePicker(); - } - - initGlDropdown() { - this.$dropdown.glDropdown({ - opened: () => { - const calendar = this.$datePicker.data('pikaday'); - calendar.show(); - }, - hidden: () => { - this.$selectbox.hide(); - this.$value.css('display', ''); - } - }); - } - - initDatePicker() { - const $dueDateInput = $(`input[name='${this.fieldName}']`); - - const calendar = new Pikaday({ - field: $dueDateInput.get(0), - theme: 'gitlab-theme', - format: 'yyyy-mm-dd', - onSelect: (dateText) => { - const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); - - $dueDateInput.val(formattedDate); +class DueDateSelect { + constructor({ $dropdown, $loading } = {}) { + const $dropdownParent = $dropdown.closest('.dropdown'); + const $block = $dropdown.closest('.block'); + this.$loading = $loading; + this.$dropdown = $dropdown; + this.$dropdownParent = $dropdownParent; + this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); + this.$block = $block; + this.$selectbox = $dropdown.closest('.selectbox'); + this.$value = $block.find('.value'); + this.$valueContent = $block.find('.value-content'); + this.$sidebarValue = $('.js-due-date-sidebar-value', $block); + this.fieldName = $dropdown.data('field-name'), + this.abilityName = $dropdown.data('ability-name'), + this.issueUpdateURL = $dropdown.data('issue-update'); + + this.rawSelectedDate = null; + this.displayedDate = null; + this.datePayload = null; + + this.initGlDropdown(); + this.initRemoveDueDate(); + this.initDatePicker(); + } - if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); - this.updateIssueBoardIssue(); - } else { - this.saveDueDate(true); - } - } - }); + initGlDropdown() { + this.$dropdown.glDropdown({ + opened: () => { + const calendar = this.$datePicker.data('pikaday'); + calendar.show(); + }, + hidden: () => { + this.$selectbox.hide(); + this.$value.css('display', ''); + } + }); + } - calendar.setDate(new Date($dueDateInput.val())); - this.$datePicker.append(calendar.el); - this.$datePicker.data('pikaday', calendar); - } + initDatePicker() { + const $dueDateInput = $(`input[name='${this.fieldName}']`); - initRemoveDueDate() { - this.$block.on('click', '.js-remove-due-date', (e) => { - const calendar = this.$datePicker.data('pikaday'); - e.preventDefault(); + const calendar = new Pikaday({ + field: $dueDateInput.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + onSelect: (dateText) => { + const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); - calendar.setDate(null); + $dueDateInput.val(formattedDate); if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; + gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); this.updateIssueBoardIssue(); } else { - $("input[name='" + this.fieldName + "']").val(''); - return this.saveDueDate(false); + this.saveDueDate(true); } - }); - } + } + }); - saveDueDate(isDropdown) { - this.parseSelectedDate(); - this.prepSelectedDate(); - this.submitSelectedDate(isDropdown); - } + calendar.setDate(new Date($dueDateInput.val())); + this.$datePicker.append(calendar.el); + this.$datePicker.data('pikaday', calendar); + } + + initRemoveDueDate() { + this.$block.on('click', '.js-remove-due-date', (e) => { + const calendar = this.$datePicker.data('pikaday'); + e.preventDefault(); - parseSelectedDate() { - this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); + calendar.setDate(null); - if (this.rawSelectedDate.length) { - // Construct Date object manually to avoid buggy dateString support within Date constructor - const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); - const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); - this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy'); + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { + gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; + this.updateIssueBoardIssue(); } else { - this.displayedDate = 'No due date'; + $("input[name='" + this.fieldName + "']").val(''); + return this.saveDueDate(false); } - } + }); + } - prepSelectedDate() { - const datePayload = {}; - datePayload[this.abilityName] = {}; - datePayload[this.abilityName].due_date = this.rawSelectedDate; - this.datePayload = datePayload; - } + saveDueDate(isDropdown) { + this.parseSelectedDate(); + this.prepSelectedDate(); + this.submitSelectedDate(isDropdown); + } - updateIssueBoardIssue () { - this.$loading.fadeIn(); - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - this.$value.css('display', ''); + parseSelectedDate() { + this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); - gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) - .then(() => { - this.$loading.fadeOut(); - }); + if (this.rawSelectedDate.length) { + // Construct Date object manually to avoid buggy dateString support within Date constructor + const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); + const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); + this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy'); + } else { + this.displayedDate = 'No due date'; } + } + + prepSelectedDate() { + const datePayload = {}; + datePayload[this.abilityName] = {}; + datePayload[this.abilityName].due_date = this.rawSelectedDate; + this.datePayload = datePayload; + } + + updateIssueBoardIssue () { + this.$loading.fadeIn(); + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + this.$value.css('display', ''); + + gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) + .then(() => { + this.$loading.fadeOut(); + }); + } + + submitSelectedDate(isDropdown) { + return $.ajax({ + type: 'PUT', + url: this.issueUpdateURL, + data: this.datePayload, + dataType: 'json', + beforeSend: () => { + const selectedDateValue = this.datePayload[this.abilityName].due_date; + const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + + this.$loading.fadeIn(); - submitSelectedDate(isDropdown) { - return $.ajax({ - type: 'PUT', - url: this.issueUpdateURL, - data: this.datePayload, - dataType: 'json', - beforeSend: () => { - const selectedDateValue = this.datePayload[this.abilityName].due_date; - const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; - - this.$loading.fadeIn(); - - if (isDropdown) { - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - } - - this.$value.css('display', ''); - this.$valueContent.html(`${this.displayedDate}`); - this.$sidebarValue.html(this.displayedDate); - - return selectedDateValue.length ? - $('.js-remove-due-date-holder').removeClass('hidden') : - $('.js-remove-due-date-holder').addClass('hidden'); - } - }).done((data) => { if (isDropdown) { - this.$dropdown.trigger('loaded.gl.dropdown'); - this.$dropdown.dropdown('toggle'); + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); } - return this.$loading.fadeOut(); - }); - } + + this.$value.css('display', ''); + this.$valueContent.html(`${this.displayedDate}`); + this.$sidebarValue.html(this.displayedDate); + + return selectedDateValue.length ? + $('.js-remove-due-date-holder').removeClass('hidden') : + $('.js-remove-due-date-holder').addClass('hidden'); + } + }).done((data) => { + if (isDropdown) { + this.$dropdown.trigger('loaded.gl.dropdown'); + this.$dropdown.dropdown('toggle'); + } + return this.$loading.fadeOut(); + }); } +} - class DueDateSelectors { - constructor() { - this.initMilestoneDatePicker(); - this.initIssuableSelect(); - } +class DueDateSelectors { + constructor() { + this.initMilestoneDatePicker(); + this.initIssuableSelect(); + } - initMilestoneDatePicker() { - $('.datepicker').each(function() { - const $datePicker = $(this); - const calendar = new Pikaday({ - field: $datePicker.get(0), - theme: 'gitlab-theme', - format: 'yyyy-mm-dd', - onSelect(dateText) { - $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); - } - }); - calendar.setDate(new Date($datePicker.val())); - - $datePicker.data('pikaday', calendar); + initMilestoneDatePicker() { + $('.datepicker').each(function() { + const $datePicker = $(this); + const calendar = new Pikaday({ + field: $datePicker.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + onSelect(dateText) { + $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); + } }); + calendar.setDate(new Date($datePicker.val())); - $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { - e.preventDefault(); - const calendar = $(e.target).siblings('.datepicker').data('pikaday'); - calendar.setDate(null); - }); - } + $datePicker.data('pikaday', calendar); + }); - initIssuableSelect() { - const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); + $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { + e.preventDefault(); + const calendar = $(e.target).siblings('.datepicker').data('pikaday'); + calendar.setDate(null); + }); + } + + initIssuableSelect() { + const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); - $('.js-due-date-select').each((i, dropdown) => { - const $dropdown = $(dropdown); - new DueDateSelect({ - $dropdown, - $loading - }); + $('.js-due-date-select').each((i, dropdown) => { + const $dropdown = $(dropdown); + new DueDateSelect({ + $dropdown, + $loading }); - } + }); } +} - global.DueDateSelectors = DueDateSelectors; -})(window.gl || (window.gl = {})); +window.gl = window.gl || {}; +window.gl.DueDateSelectors = DueDateSelectors; diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index bf84f2a0a8f..3f041172ff3 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -2,142 +2,140 @@ /* global FilesCommentButton */ /* global notes */ -(function() { - let $commentButtonTemplate; - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +let $commentButtonTemplate; +var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - this.FilesCommentButton = (function() { - var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; +window.FilesCommentButton = (function() { + var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; - COMMENT_BUTTON_CLASS = '.add-diff-note'; + COMMENT_BUTTON_CLASS = '.add-diff-note'; - LINE_HOLDER_CLASS = '.line_holder'; + LINE_HOLDER_CLASS = '.line_holder'; - LINE_NUMBER_CLASS = 'diff-line-num'; + LINE_NUMBER_CLASS = 'diff-line-num'; - LINE_CONTENT_CLASS = 'line_content'; + LINE_CONTENT_CLASS = 'line_content'; - UNFOLDABLE_LINE_CLASS = 'js-unfold'; + UNFOLDABLE_LINE_CLASS = 'js-unfold'; - EMPTY_CELL_CLASS = 'empty-cell'; + EMPTY_CELL_CLASS = 'empty-cell'; - OLD_LINE_CLASS = 'old_line'; + OLD_LINE_CLASS = 'old_line'; - LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; + LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; - TEXT_FILE_SELECTOR = '.text-file'; + TEXT_FILE_SELECTOR = '.text-file'; - function FilesCommentButton(filesContainerElement) { - this.render = bind(this.render, this); - this.hideButton = bind(this.hideButton, this); - this.isParallelView = notes.isParallelView(); - filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render) - .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton); + function FilesCommentButton(filesContainerElement) { + this.render = bind(this.render, this); + this.hideButton = bind(this.hideButton, this); + this.isParallelView = notes.isParallelView(); + filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render) + .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton); + } + + FilesCommentButton.prototype.render = function(e) { + var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; + $currentTarget = $(e.currentTarget); + + if ($currentTarget.hasClass('js-no-comment-btn')) return; + + lineContentElement = this.getLineContent($currentTarget); + buttonParentElement = this.getButtonParent($currentTarget); + + if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return; + + $button = $(COMMENT_BUTTON_CLASS, buttonParentElement); + buttonParentElement.addClass('is-over') + .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over'); + + if ($button.length) { + return; } - FilesCommentButton.prototype.render = function(e) { - var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; - $currentTarget = $(e.currentTarget); + textFileElement = this.getTextFileElement($currentTarget); + buttonParentElement.append(this.buildButton({ + noteableType: textFileElement.attr('data-noteable-type'), + noteableID: textFileElement.attr('data-noteable-id'), + commitID: textFileElement.attr('data-commit-id'), + noteType: lineContentElement.attr('data-note-type'), + position: lineContentElement.attr('data-position'), + lineType: lineContentElement.attr('data-line-type'), + discussionID: lineContentElement.attr('data-discussion-id'), + lineCode: lineContentElement.attr('data-line-code') + })); + }; - if ($currentTarget.hasClass('js-no-comment-btn')) return; + FilesCommentButton.prototype.hideButton = function(e) { + var $currentTarget = $(e.currentTarget); + var buttonParentElement = this.getButtonParent($currentTarget); - lineContentElement = this.getLineContent($currentTarget); - buttonParentElement = this.getButtonParent($currentTarget); + buttonParentElement.removeClass('is-over') + .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over'); + }; - if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return; + FilesCommentButton.prototype.buildButton = function(buttonAttributes) { + return $commentButtonTemplate.clone().attr({ + 'data-noteable-type': buttonAttributes.noteableType, + 'data-noteable-id': buttonAttributes.noteableID, + 'data-commit-id': buttonAttributes.commitID, + 'data-note-type': buttonAttributes.noteType, + 'data-line-code': buttonAttributes.lineCode, + 'data-position': buttonAttributes.position, + 'data-discussion-id': buttonAttributes.discussionID, + 'data-line-type': buttonAttributes.lineType + }); + }; - $button = $(COMMENT_BUTTON_CLASS, buttonParentElement); - buttonParentElement.addClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over'); + FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { + return hoveredElement.closest(TEXT_FILE_SELECTOR); + }; - if ($button.length) { - return; - } + FilesCommentButton.prototype.getLineContent = function(hoveredElement) { + if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { + return hoveredElement; + } + if (!this.isParallelView) { + return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); + } else { + return $(hoveredElement).next("." + LINE_CONTENT_CLASS); + } + }; - textFileElement = this.getTextFileElement($currentTarget); - buttonParentElement.append(this.buildButton({ - noteableType: textFileElement.attr('data-noteable-type'), - noteableID: textFileElement.attr('data-noteable-id'), - commitID: textFileElement.attr('data-commit-id'), - noteType: lineContentElement.attr('data-note-type'), - position: lineContentElement.attr('data-position'), - lineType: lineContentElement.attr('data-line-type'), - discussionID: lineContentElement.attr('data-discussion-id'), - lineCode: lineContentElement.attr('data-line-code') - })); - }; - - FilesCommentButton.prototype.hideButton = function(e) { - var $currentTarget = $(e.currentTarget); - var buttonParentElement = this.getButtonParent($currentTarget); - - buttonParentElement.removeClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over'); - }; - - FilesCommentButton.prototype.buildButton = function(buttonAttributes) { - return $commentButtonTemplate.clone().attr({ - 'data-noteable-type': buttonAttributes.noteableType, - 'data-noteable-id': buttonAttributes.noteableID, - 'data-commit-id': buttonAttributes.commitID, - 'data-note-type': buttonAttributes.noteType, - 'data-line-code': buttonAttributes.lineCode, - 'data-position': buttonAttributes.position, - 'data-discussion-id': buttonAttributes.discussionID, - 'data-line-type': buttonAttributes.lineType - }); - }; - - FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { - return hoveredElement.closest(TEXT_FILE_SELECTOR); - }; - - FilesCommentButton.prototype.getLineContent = function(hoveredElement) { - if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { + FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { + if (!this.isParallelView) { + if (hoveredElement.hasClass(OLD_LINE_CLASS)) { return hoveredElement; } - if (!this.isParallelView) { - return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); - } else { - return $(hoveredElement).next("." + LINE_CONTENT_CLASS); - } - }; - - FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { - if (!this.isParallelView) { - if (hoveredElement.hasClass(OLD_LINE_CLASS)) { - return hoveredElement; - } - return hoveredElement.parent().find("." + OLD_LINE_CLASS); - } else { - if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { - return hoveredElement; - } - return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + return hoveredElement.parent().find("." + OLD_LINE_CLASS); + } else { + if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { + return hoveredElement; } - }; + return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + } + }; - FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { - return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); - }; + FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { + return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); + }; - FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { - return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== ''; - }; + FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { + return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== ''; + }; - return FilesCommentButton; - })(); + return FilesCommentButton; +})(); - $.fn.filesCommentButton = function() { - $commentButtonTemplate = $(''); +$.fn.filesCommentButton = function() { + $commentButtonTemplate = $(''); - if (!(this && (this.parent().data('can-create-note') != null))) { - return; + if (!(this && (this.parent().data('can-create-note') != null))) { + return; + } + return this.each(function() { + if (!$.data(this, 'filesCommentButton')) { + return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); } - return this.each(function() { - if (!$.data(this, 'filesCommentButton')) { - return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); - } - }); - }; -}).call(window); + }); +}; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 47a40e28461..aaaeb9bddb1 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -2,6 +2,7 @@ * Makes search request for content when user types a value in the search input. * Updates the html content of the page with the received one. */ + export default class FilterableList { constructor(form, filter, holder) { this.filterForm = form; diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 730104b89f9..eec30624ff2 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,42 +1,41 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */ -(function() { - this.Flash = (function() { - var hideFlash; - hideFlash = function() { - return $(this).fadeOut(); - }; +window.Flash = (function() { + var hideFlash; - function Flash(message, type, parent) { - var flash, textDiv; - if (type == null) { - type = 'alert'; - } - if (parent == null) { - parent = null; - } - if (parent) { - this.flashContainer = parent.find('.flash-container'); - } else { - this.flashContainer = $('.flash-container-page'); - } - this.flashContainer.html(''); - flash = $('
      ', { - "class": "flash-" + type - }); - flash.on('click', hideFlash); - textDiv = $('
      ', { - "class": 'flash-text', - text: message - }); - textDiv.appendTo(flash); - if (this.flashContainer.parent().hasClass('content-wrapper')) { - textDiv.addClass('container-fluid container-limited'); - } - flash.appendTo(this.flashContainer); - this.flashContainer.show(); + hideFlash = function() { + return $(this).fadeOut(); + }; + + function Flash(message, type, parent) { + var flash, textDiv; + if (type == null) { + type = 'alert'; + } + if (parent == null) { + parent = null; + } + if (parent) { + this.flashContainer = parent.find('.flash-container'); + } else { + this.flashContainer = $('.flash-container-page'); + } + this.flashContainer.html(''); + flash = $('
      ', { + "class": "flash-" + type + }); + flash.on('click', hideFlash); + textDiv = $('
      ', { + "class": 'flash-text', + text: message + }); + textDiv.appendTo(flash); + if (this.flashContainer.parent().hasClass('content-wrapper')) { + textDiv.addClass('container-fluid container-limited'); } + flash.appendTo(this.flashContainer); + this.flashContainer.show(); + } - return Flash; - })(); -}).call(window); + return Flash; +})(); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 4f7ce1fa197..9ac4c49d697 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -5,390 +5,386 @@ import emojiAliases from 'emojis/aliases.json'; import { glEmojiTag } from '~/behaviors/gl_emoji'; // Creates the variables for setting up GFM auto-completion -(function() { - if (window.gl == null) { - window.gl = {}; - } +window.gl = window.gl || {}; - function sanitize(str) { - return str.replace(/<(?:.|\n)*?>/gm, ''); - } +function sanitize(str) { + return str.replace(/<(?:.|\n)*?>/gm, ''); +} - window.gl.GfmAutoComplete = { - dataSources: {}, - defaultLoadingData: ['loading'], - cachedData: {}, - isLoadingData: {}, - atTypeMap: { - ':': 'emojis', - '@': 'members', - '#': 'issues', - '!': 'mergeRequests', - '~': 'labels', - '%': 'milestones', - '/': 'commands' - }, - // Emoji - Emoji: { - templateFunction: function(name) { - return `
    • - ${name} ${glEmojiTag(name)} -
    • - `; +window.gl.GfmAutoComplete = { + dataSources: {}, + defaultLoadingData: ['loading'], + cachedData: {}, + isLoadingData: {}, + atTypeMap: { + ':': 'emojis', + '@': 'members', + '#': 'issues', + '!': 'mergeRequests', + '~': 'labels', + '%': 'milestones', + '/': 'commands' + }, + // Emoji + Emoji: { + templateFunction: function(name) { + return `
    • + ${name} ${glEmojiTag(name)} +
    • + `; + } + }, + // Team Members + Members: { + template: '
    • ${avatarTag} ${username} ${title}
    • ' + }, + Labels: { + template: '
    • ${title}
    • ' + }, + // Issues and MergeRequests + Issues: { + template: '
    • ${id} ${title}
    • ' + }, + // Milestones + Milestones: { + template: '
    • ${title}
    • ' + }, + Loading: { + template: '
    • Loading...
    • ' + }, + DefaultOptions: { + sorter: function(query, items, searchKey) { + this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; + if (gl.GfmAutoComplete.isLoading(items)) { + this.setting.highlightFirst = false; + return items; } + return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); }, - // Team Members - Members: { - template: '
    • ${avatarTag} ${username} ${title}
    • ' - }, - Labels: { - template: '
    • ${title}
    • ' - }, - // Issues and MergeRequests - Issues: { - template: '
    • ${id} ${title}
    • ' - }, - // Milestones - Milestones: { - template: '
    • ${title}
    • ' + filter: function(query, data, searchKey) { + if (gl.GfmAutoComplete.isLoading(data)) { + gl.GfmAutoComplete.fetchData(this.$inputor, this.at); + return data; + } else { + return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); + } }, - Loading: { - template: '
    • Loading...
    • ' + beforeInsert: function(value) { + if (value && !this.setting.skipSpecialCharacterTest) { + var withoutAt = value.substring(1); + if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; + } + return value; }, - DefaultOptions: { - sorter: function(query, items, searchKey) { - this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; - if (gl.GfmAutoComplete.isLoading(items)) { - this.setting.highlightFirst = false; - return items; - } - return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); - }, - filter: function(query, data, searchKey) { - if (gl.GfmAutoComplete.isLoading(data)) { - gl.GfmAutoComplete.fetchData(this.$inputor, this.at); - return data; - } else { - return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); - } - }, - beforeInsert: function(value) { - if (value && !this.setting.skipSpecialCharacterTest) { - var withoutAt = value.substring(1); - if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; - } - return value; - }, - matcher: function (flag, subtext) { - // The below is taken from At.js source - // Tweaked to commands to start without a space only if char before is a non-word character - // https://github.com/ichord/At.js - var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; - atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); - atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); - subtext = subtext.split(/\s+/g).pop(); - flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + matcher: function (flag, subtext) { + // The below is taken from At.js source + // Tweaked to commands to start without a space only if char before is a non-word character + // https://github.com/ichord/At.js + var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; + atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); + subtext = subtext.split(/\s+/g).pop(); + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - _a = decodeURI("%C3%80"); - _y = decodeURI("%C3%BF"); + _a = decodeURI("%C3%80"); + _y = decodeURI("%C3%BF"); - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); - match = regexp.exec(subtext); + match = regexp.exec(subtext); - if (match) { - return match[1]; - } else { - return null; - } + if (match) { + return match[1]; + } else { + return null; } - }, - setup: function(input) { - // Add GFM auto-completion to all input fields, that accept GFM input. - this.input = input || $('.js-gfm-input'); - this.setupLifecycle(); - }, - setupLifecycle() { - this.input.each((i, input) => { - const $input = $(input); - $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); - // This triggers at.js again - // Needed for slash commands with suffixes (ex: /label ~) - $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); - }); - }, - setupAtWho: function($input) { - // Emoji - $input.atwho({ - at: ':', - displayTpl: function(value) { - return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template; - }.bind(this), - insertTpl: ':${name}:', - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - callbacks: { - sorter: this.DefaultOptions.sorter, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter - } - }); - // Team Members - $input.atwho({ - at: '@', - displayTpl: function(value) { - return value.username != null ? this.Members.template : this.Loading.template; - }.bind(this), - insertTpl: '${atwho-at}${username}', - searchKey: 'search', - alwaysHighlightFirst: true, - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(members) { - return $.map(members, function(m) { - let title = ''; - if (m.username == null) { - return m; - } - title = m.name; - if (m.count) { - title += " (" + m.count + ")"; - } + } + }, + setup: function(input) { + // Add GFM auto-completion to all input fields, that accept GFM input. + this.input = input || $('.js-gfm-input'); + this.setupLifecycle(); + }, + setupLifecycle() { + this.input.each((i, input) => { + const $input = $(input); + $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + // This triggers at.js again + // Needed for slash commands with suffixes (ex: /label ~) + $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); + }); + }, + setupAtWho: function($input) { + // Emoji + $input.atwho({ + at: ':', + displayTpl: function(value) { + return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template; + }.bind(this), + insertTpl: ':${name}:', + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + callbacks: { + sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter + } + }); + // Team Members + $input.atwho({ + at: '@', + displayTpl: function(value) { + return value.username != null ? this.Members.template : this.Loading.template; + }.bind(this), + insertTpl: '${atwho-at}${username}', + searchKey: 'search', + alwaysHighlightFirst: true, + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(members) { + return $.map(members, function(m) { + let title = ''; + if (m.username == null) { + return m; + } + title = m.name; + if (m.count) { + title += " (" + m.count + ")"; + } - const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); - const imgAvatar = `${m.username}`; - const txtAvatar = `
      ${autoCompleteAvatar}
      `; + const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); + const imgAvatar = `${m.username}`; + const txtAvatar = `
      ${autoCompleteAvatar}
      `; - return { - username: m.username, - avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, - title: sanitize(title), - search: sanitize(m.username + " " + m.name) - }; - }); - } + return { + username: m.username, + avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, + title: sanitize(title), + search: sanitize(m.username + " " + m.name) + }; + }); } - }); - $input.atwho({ - at: '#', - alias: 'issues', - searchKey: 'search', - displayTpl: function(value) { - return value.title != null ? this.Issues.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, - insertTpl: '${atwho-at}${id}', - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(issues) { - return $.map(issues, function(i) { - if (i.title == null) { - return i; - } - return { - id: i.iid, - title: sanitize(i.title), - search: i.iid + " " + i.title - }; - }); - } + } + }); + $input.atwho({ + at: '#', + alias: 'issues', + searchKey: 'search', + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(issues) { + return $.map(issues, function(i) { + if (i.title == null) { + return i; + } + return { + id: i.iid, + title: sanitize(i.title), + search: i.iid + " " + i.title + }; + }); } - }); - $input.atwho({ - at: '%', - alias: 'milestones', - searchKey: 'search', - insertTpl: '${atwho-at}${title}', - displayTpl: function(value) { - return value.title != null ? this.Milestones.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, - callbacks: { - matcher: this.DefaultOptions.matcher, - sorter: this.DefaultOptions.sorter, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - beforeSave: function(milestones) { - return $.map(milestones, function(m) { - if (m.title == null) { - return m; - } - return { - id: m.iid, - title: sanitize(m.title), - search: "" + m.title - }; - }); - } + } + }); + $input.atwho({ + at: '%', + alias: 'milestones', + searchKey: 'search', + insertTpl: '${atwho-at}${title}', + displayTpl: function(value) { + return value.title != null ? this.Milestones.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, + callbacks: { + matcher: this.DefaultOptions.matcher, + sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, + beforeSave: function(milestones) { + return $.map(milestones, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: "" + m.title + }; + }); } - }); - $input.atwho({ - at: '!', - alias: 'mergerequests', - searchKey: 'search', - displayTpl: function(value) { - return value.title != null ? this.Issues.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, - insertTpl: '${atwho-at}${id}', - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(merges) { - return $.map(merges, function(m) { - if (m.title == null) { - return m; - } - return { - id: m.iid, - title: sanitize(m.title), - search: m.iid + " " + m.title - }; - }); - } + } + }); + $input.atwho({ + at: '!', + alias: 'mergerequests', + searchKey: 'search', + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(merges) { + return $.map(merges, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: m.iid + " " + m.title + }; + }); } - }); - $input.atwho({ - at: '~', - alias: 'labels', - searchKey: 'search', - data: this.defaultLoadingData, - displayTpl: function(value) { - return this.isLoading(value) ? this.Loading.template : this.Labels.template; - }.bind(this), - insertTpl: '${atwho-at}${title}', - callbacks: { - matcher: this.DefaultOptions.matcher, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - sorter: this.DefaultOptions.sorter, - beforeSave: function(merges) { - if (gl.GfmAutoComplete.isLoading(merges)) return merges; - var sanitizeLabelTitle; - sanitizeLabelTitle = function(title) { - if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { - return "\"" + (sanitize(title)) + "\""; - } else { - return sanitize(title); - } + } + }); + $input.atwho({ + at: '~', + alias: 'labels', + searchKey: 'search', + data: this.defaultLoadingData, + displayTpl: function(value) { + return this.isLoading(value) ? this.Loading.template : this.Labels.template; + }.bind(this), + insertTpl: '${atwho-at}${title}', + callbacks: { + matcher: this.DefaultOptions.matcher, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, + sorter: this.DefaultOptions.sorter, + beforeSave: function(merges) { + if (gl.GfmAutoComplete.isLoading(merges)) return merges; + var sanitizeLabelTitle; + sanitizeLabelTitle = function(title) { + if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { + return "\"" + (sanitize(title)) + "\""; + } else { + return sanitize(title); + } + }; + return $.map(merges, function(m) { + return { + title: sanitize(m.title), + color: m.color, + search: "" + m.title }; - return $.map(merges, function(m) { - return { - title: sanitize(m.title), - color: m.color, - search: "" + m.title - }; - }); - } + }); } - }); - // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - $input.filter('[data-supports-slash-commands="true"]').atwho({ - at: '/', - alias: 'commands', - searchKey: 'search', - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - displayTpl: function(value) { - if (this.isLoading(value)) return this.Loading.template; - var tpl = '
    • /${name}'; - if (value.aliases.length > 0) { - tpl += ' (or /<%- aliases.join(", /") %>)'; - } - if (value.params.length > 0) { - tpl += ' <%- params.join(" ") %>'; - } - if (value.description !== '') { - tpl += '<%- description %>'; + } + }); + // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms + $input.filter('[data-supports-slash-commands="true"]').atwho({ + at: '/', + alias: 'commands', + searchKey: 'search', + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + displayTpl: function(value) { + if (this.isLoading(value)) return this.Loading.template; + var tpl = '
    • /${name}'; + if (value.aliases.length > 0) { + tpl += ' (or /<%- aliases.join(", /") %>)'; + } + if (value.params.length > 0) { + tpl += ' <%- params.join(" ") %>'; + } + if (value.description !== '') { + tpl += '<%- description %>'; + } + tpl += '
    • '; + return _.template(tpl)(value); + }.bind(this), + insertTpl: function(value) { + var tpl = "/${name} "; + var reference_prefix = null; + if (value.params.length > 0) { + reference_prefix = value.params[0][0]; + if (/^[@%~]/.test(reference_prefix)) { + tpl += '<%- reference_prefix %>'; } - tpl += ''; - return _.template(tpl)(value); - }.bind(this), - insertTpl: function(value) { - var tpl = "/${name} "; - var reference_prefix = null; - if (value.params.length > 0) { - reference_prefix = value.params[0][0]; - if (/^[@%~]/.test(reference_prefix)) { - tpl += '<%- reference_prefix %>'; + } + return _.template(tpl)({ reference_prefix: reference_prefix }); + }, + suffix: '', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + beforeSave: function(commands) { + if (gl.GfmAutoComplete.isLoading(commands)) return commands; + return $.map(commands, function(c) { + var search = c.name; + if (c.aliases.length > 0) { + search = search + " " + c.aliases.join(" "); } - } - return _.template(tpl)({ reference_prefix: reference_prefix }); + return { + name: c.name, + aliases: c.aliases, + params: c.params, + description: c.description, + search: search + }; + }); }, - suffix: '', - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - beforeSave: function(commands) { - if (gl.GfmAutoComplete.isLoading(commands)) return commands; - return $.map(commands, function(c) { - var search = c.name; - if (c.aliases.length > 0) { - search = search + " " + c.aliases.join(" "); - } - return { - name: c.name, - aliases: c.aliases, - params: c.params, - description: c.description, - search: search - }; - }); - }, - matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { - var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; - var match = regexp.exec(subtext); - if (match) { - return match[1]; - } else { - return null; - } + matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { + var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; + var match = regexp.exec(subtext); + if (match) { + return match[1]; + } else { + return null; } } - }); - return; - }, - fetchData: function($input, at) { - if (this.isLoadingData[at]) return; - this.isLoadingData[at] = true; - if (this.cachedData[at]) { - this.loadData($input, at, this.cachedData[at]); - } else if (this.atTypeMap[at] === 'emojis') { - this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); - } else { - $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { - this.loadData($input, at, data); - }).fail(() => { this.isLoadingData[at] = false; }); - } - }, - loadData: function($input, at, data) { - this.isLoadingData[at] = false; - this.cachedData[at] = data; - $input.atwho('load', at, data); - // This trigger at.js again - // otherwise we would be stuck with loading until the user types - return $input.trigger('keyup'); - }, - isLoading(data) { - var dataToInspect = data; - if (data && data.length > 0) { - dataToInspect = data[0]; } - - var loadingState = this.defaultLoadingData[0]; - return dataToInspect && - (dataToInspect === loadingState || dataToInspect.name === loadingState); + }); + return; + }, + fetchData: function($input, at) { + if (this.isLoadingData[at]) return; + this.isLoadingData[at] = true; + if (this.cachedData[at]) { + this.loadData($input, at, this.cachedData[at]); + } else if (this.atTypeMap[at] === 'emojis') { + this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); + } else { + $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { + this.loadData($input, at, data); + }).fail(() => { this.isLoadingData[at] = false; }); + } + }, + loadData: function($input, at, data) { + this.isLoadingData[at] = false; + this.cachedData[at] = data; + $input.atwho('load', at, data); + // This trigger at.js again + // otherwise we would be stuck with loading until the user types + return $input.trigger('keyup'); + }, + isLoading(data) { + var dataToInspect = data; + if (data && data.length > 0) { + dataToInspect = data[0]; } - }; -}).call(window); + + var loadingState = this.defaultLoadingData[0]; + return dataToInspect && + (dataToInspect === loadingState || dataToInspect.name === loadingState); + } +}; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 9e6ed06054b..a03f1202a6d 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,850 +1,848 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* global fuzzaldrinPlus */ -(function() { - var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, - 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; }; - - GitLabDropdownFilter = (function() { - var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; - - BLUR_KEYCODES = [27, 40]; - - ARROW_KEY_CODES = [38, 40]; - - HAS_VALUE_CLASS = "has-value"; - - function GitLabDropdownFilter(input, options) { - var $clearButton, $inputContainer, ref, timeout; - this.input = input; - this.options = options; - this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; - $inputContainer = this.input.parent(); - $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - $clearButton.on('click', (function(_this) { - // Clear click - return function(e) { +var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, + 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; }; + +GitLabDropdownFilter = (function() { + var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; + + BLUR_KEYCODES = [27, 40]; + + ARROW_KEY_CODES = [38, 40]; + + HAS_VALUE_CLASS = "has-value"; + + function GitLabDropdownFilter(input, options) { + var $clearButton, $inputContainer, ref, timeout; + this.input = input; + this.options = options; + this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; + $inputContainer = this.input.parent(); + $clearButton = $inputContainer.find('.js-dropdown-input-clear'); + $clearButton.on('click', (function(_this) { + // Clear click + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.input.val('').trigger('input').focus(); + }; + })(this)); + // Key events + timeout = ""; + this.input + .on('keydown', function (e) { + var keyCode = e.which; + if (keyCode === 13 && !options.elIsInput) { e.preventDefault(); - e.stopPropagation(); - return _this.input.val('').trigger('input').focus(); - }; - })(this)); - // Key events - timeout = ""; - this.input - .on('keydown', function (e) { - var keyCode = e.which; - if (keyCode === 13 && !options.elIsInput) { - e.preventDefault(); - } - }) - .on('input', function() { - if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { - $inputContainer.addClass(HAS_VALUE_CLASS); - } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { - $inputContainer.removeClass(HAS_VALUE_CLASS); - } - // Only filter asynchronously only if option remote is set - if (this.options.remote) { - clearTimeout(timeout); - return timeout = setTimeout(function() { - $inputContainer.parent().addClass('is-loading'); - - return this.options.query(this.input.val(), function(data) { - $inputContainer.parent().removeClass('is-loading'); - return this.options.callback(data); - }.bind(this)); - }.bind(this), 250); - } else { - return this.filter(this.input.val()); - } - }.bind(this)); - } + } + }) + .on('input', function() { + if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.addClass(HAS_VALUE_CLASS); + } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.removeClass(HAS_VALUE_CLASS); + } + // Only filter asynchronously only if option remote is set + if (this.options.remote) { + clearTimeout(timeout); + return timeout = setTimeout(function() { + $inputContainer.parent().addClass('is-loading'); + + return this.options.query(this.input.val(), function(data) { + $inputContainer.parent().removeClass('is-loading'); + return this.options.callback(data); + }.bind(this)); + }.bind(this), 250); + } else { + return this.filter(this.input.val()); + } + }.bind(this)); + } - GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { - return BLUR_KEYCODES.indexOf(keyCode) !== -1; - }; + GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { + return BLUR_KEYCODES.indexOf(keyCode) !== -1; + }; - GitLabDropdownFilter.prototype.filter = function(search_text) { - var data, elements, group, key, results, tmp; - if (this.options.onFilter) { - this.options.onFilter(search_text); - } - data = this.options.data(); - if ((data != null) && !this.options.filterByText) { - results = data; - if (search_text !== '') { - // When data is an array of objects therefore [object Array] e.g. - // [ - // { prop: 'foo' }, - // { prop: 'baz' } - // ] - if (_.isArray(data)) { - results = fuzzaldrinPlus.filter(data, search_text, { - key: this.options.keys - }); - } else { - // If data is grouped therefore an [object Object]. e.g. - // { - // groupName1: [ - // { prop: 'foo' }, - // { prop: 'baz' } - // ], - // groupName2: [ - // { prop: 'abc' }, - // { prop: 'def' } - // ] - // } - if (gl.utils.isObject(data)) { - results = {}; - for (key in data) { - group = data[key]; - tmp = fuzzaldrinPlus.filter(group, search_text, { - key: this.options.keys + GitLabDropdownFilter.prototype.filter = function(search_text) { + var data, elements, group, key, results, tmp; + if (this.options.onFilter) { + this.options.onFilter(search_text); + } + data = this.options.data(); + if ((data != null) && !this.options.filterByText) { + results = data; + if (search_text !== '') { + // When data is an array of objects therefore [object Array] e.g. + // [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ] + if (_.isArray(data)) { + results = fuzzaldrinPlus.filter(data, search_text, { + key: this.options.keys + }); + } else { + // If data is grouped therefore an [object Object]. e.g. + // { + // groupName1: [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ], + // groupName2: [ + // { prop: 'abc' }, + // { prop: 'def' } + // ] + // } + if (gl.utils.isObject(data)) { + results = {}; + for (key in data) { + group = data[key]; + tmp = fuzzaldrinPlus.filter(group, search_text, { + key: this.options.keys + }); + if (tmp.length) { + results[key] = tmp.map(function(item) { + return item; }); - if (tmp.length) { - results[key] = tmp.map(function(item) { - return item; - }); - } } } } } - return this.options.callback(results); - } else { - elements = this.options.elements(); - if (search_text) { - return elements.each(function() { - var $el, matches; - $el = $(this); - matches = fuzzaldrinPlus.match($el.text().trim(), search_text); - if (!$el.is('.dropdown-header')) { - if (matches.length) { - return $el.show().removeClass('option-hidden'); - } else { - return $el.hide().addClass('option-hidden'); - } + } + return this.options.callback(results); + } else { + elements = this.options.elements(); + if (search_text) { + return elements.each(function() { + var $el, matches; + $el = $(this); + matches = fuzzaldrinPlus.match($el.text().trim(), search_text); + if (!$el.is('.dropdown-header')) { + if (matches.length) { + return $el.show().removeClass('option-hidden'); + } else { + return $el.hide().addClass('option-hidden'); } - }); - } else { - return elements.show().removeClass('option-hidden'); - } + } + }); + } else { + return elements.show().removeClass('option-hidden'); } - }; - - return GitLabDropdownFilter; - })(); + } + }; - GitLabDropdownRemote = (function() { - function GitLabDropdownRemote(dataEndpoint, options) { - this.dataEndpoint = dataEndpoint; - this.options = options; + return GitLabDropdownFilter; +})(); + +GitLabDropdownRemote = (function() { + function GitLabDropdownRemote(dataEndpoint, options) { + this.dataEndpoint = dataEndpoint; + this.options = options; + } + + GitLabDropdownRemote.prototype.execute = function() { + if (typeof this.dataEndpoint === "string") { + return this.fetchData(); + } else if (typeof this.dataEndpoint === "function") { + if (this.options.beforeSend) { + this.options.beforeSend(); + } + return this.dataEndpoint("", (function(_this) { + // Fetch the data by calling the data funcfion + return function(data) { + if (_this.options.success) { + _this.options.success(data); + } + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this)); } + }; - GitLabDropdownRemote.prototype.execute = function() { - if (typeof this.dataEndpoint === "string") { - return this.fetchData(); - } else if (typeof this.dataEndpoint === "function") { - if (this.options.beforeSend) { - this.options.beforeSend(); - } - return this.dataEndpoint("", (function(_this) { - // Fetch the data by calling the data funcfion - return function(data) { - if (_this.options.success) { - _this.options.success(data); - } - if (_this.options.beforeSend) { - return _this.options.beforeSend(); - } - }; - })(this)); - } - }; + GitLabDropdownRemote.prototype.fetchData = function() { + return $.ajax({ + url: this.dataEndpoint, + dataType: this.options.dataType, + beforeSend: (function(_this) { + return function() { + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this), + success: (function(_this) { + return function(data) { + if (_this.options.success) { + return _this.options.success(data); + } + }; + })(this) + }); + // Fetch the data through ajax if the data is a string + }; - GitLabDropdownRemote.prototype.fetchData = function() { - return $.ajax({ - url: this.dataEndpoint, - dataType: this.options.dataType, - beforeSend: (function(_this) { - return function() { - if (_this.options.beforeSend) { - return _this.options.beforeSend(); - } - }; - })(this), - success: (function(_this) { - return function(data) { - if (_this.options.success) { - return _this.options.success(data); - } - }; - })(this) - }); - // Fetch the data through ajax if the data is a string - }; + return GitLabDropdownRemote; +})(); - return GitLabDropdownRemote; - })(); +GitLabDropdown = (function() { + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; - GitLabDropdown = (function() { - var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; + LOADING_CLASS = "is-loading"; - LOADING_CLASS = "is-loading"; + PAGE_TWO_CLASS = "is-page-two"; - PAGE_TWO_CLASS = "is-page-two"; + ACTIVE_CLASS = "is-active"; - ACTIVE_CLASS = "is-active"; + INDETERMINATE_CLASS = "is-indeterminate"; - INDETERMINATE_CLASS = "is-indeterminate"; + currentIndex = -1; - currentIndex = -1; + NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; - NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; - - SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; - - CURSOR_SELECT_SCROLL_PADDING = 5; - - FILTER_INPUT = '.dropdown-input .dropdown-input-field'; - - function GitLabDropdown(el1, options) { - var searchFields, selector, self; - this.el = el1; - this.options = options; - this.updateLabel = bind(this.updateLabel, this); - this.hidden = bind(this.hidden, this); - this.opened = bind(this.opened, this); - this.shouldPropagate = bind(this.shouldPropagate, this); - self = this; - selector = $(this.el).data("target"); - this.dropdown = selector != null ? $(selector) : $(this.el).parent(); - // Set Defaults - this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); - this.highlight = !!this.options.highlight; - this.filterInputBlur = this.options.filterInputBlur != null - ? this.options.filterInputBlur - : true; - // If no input is passed create a default one - self = this; - // If selector was passed - if (_.isString(this.filterInput)) { - this.filterInput = this.getElement(this.filterInput); - } - searchFields = this.options.search ? this.options.search.fields : []; - if (this.options.data) { - // If we provided data - // data could be an array of objects or a group of arrays - if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { - this.fullData = this.options.data; - currentIndex = -1; - this.parseData(this.options.data); - this.focusTextInput(); - } else { - this.remote = new GitLabDropdownRemote(this.options.data, { - dataType: this.options.dataType, - beforeSend: this.toggleLoading.bind(this), - success: (function(_this) { - return function(data) { - _this.fullData = data; - _this.parseData(_this.fullData); - _this.focusTextInput(); - if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { - return _this.filter.input.trigger('input'); - } - }; - // Remote data - })(this) - }); - } - } - // Init filterable - if (this.options.filterable) { - this.filter = new GitLabDropdownFilter(this.filterInput, { - elIsInput: $(this.el).is('input'), - filterInputBlur: this.filterInputBlur, - filterByText: this.options.filterByText, - onFilter: this.options.onFilter, - remote: this.options.filterRemote, - query: this.options.data, - keys: searchFields, - elements: (function(_this) { - return function() { - selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; - if (_this.dropdown.find('.dropdown-toggle-page').length) { - selector = ".dropdown-page-one " + selector; - } - return $(selector); - }; - })(this), - data: (function(_this) { - return function() { - return _this.fullData; - }; - })(this), - callback: (function(_this) { + SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; + + CURSOR_SELECT_SCROLL_PADDING = 5; + + FILTER_INPUT = '.dropdown-input .dropdown-input-field'; + + function GitLabDropdown(el1, options) { + var searchFields, selector, self; + this.el = el1; + this.options = options; + this.updateLabel = bind(this.updateLabel, this); + this.hidden = bind(this.hidden, this); + this.opened = bind(this.opened, this); + this.shouldPropagate = bind(this.shouldPropagate, this); + self = this; + selector = $(this.el).data("target"); + this.dropdown = selector != null ? $(selector) : $(this.el).parent(); + // Set Defaults + this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); + this.highlight = !!this.options.highlight; + this.filterInputBlur = this.options.filterInputBlur != null + ? this.options.filterInputBlur + : true; + // If no input is passed create a default one + self = this; + // If selector was passed + if (_.isString(this.filterInput)) { + this.filterInput = this.getElement(this.filterInput); + } + searchFields = this.options.search ? this.options.search.fields : []; + if (this.options.data) { + // If we provided data + // data could be an array of objects or a group of arrays + if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { + this.fullData = this.options.data; + currentIndex = -1; + this.parseData(this.options.data); + this.focusTextInput(); + } else { + this.remote = new GitLabDropdownRemote(this.options.data, { + dataType: this.options.dataType, + beforeSend: this.toggleLoading.bind(this), + success: (function(_this) { return function(data) { - _this.parseData(data); - if (_this.filterInput.val() !== '') { - selector = SELECTABLE_CLASSES; - if (_this.dropdown.find('.dropdown-toggle-page').length) { - selector = ".dropdown-page-one " + selector; - } - if ($(_this.el).is('input')) { - currentIndex = -1; - } else { - $(selector, _this.dropdown).first().find('a').addClass('is-focused'); - currentIndex = 0; - } + _this.fullData = data; + _this.parseData(_this.fullData); + _this.focusTextInput(); + if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { + return _this.filter.input.trigger('input'); } }; + // Remote data })(this) }); } - // Event listeners - this.dropdown.on("shown.bs.dropdown", this.opened); - this.dropdown.on("hidden.bs.dropdown", this.hidden); - $(this.el).on("update.label", this.updateLabel); - this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); - this.dropdown.on('keyup', (function(_this) { - return function(e) { - // Escape key - if (e.which === 27) { - return $('.dropdown-menu-close', _this.dropdown).trigger('click'); - } - }; - })(this)); - this.dropdown.on('blur', 'a', (function(_this) { - return function(e) { - var $dropdownMenu, $relatedTarget; - if (e.relatedTarget != null) { - $relatedTarget = $(e.relatedTarget); - $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); - if ($dropdownMenu.length === 0) { - return _this.dropdown.removeClass('open'); + } + // Init filterable + if (this.options.filterable) { + this.filter = new GitLabDropdownFilter(this.filterInput, { + elIsInput: $(this.el).is('input'), + filterInputBlur: this.filterInputBlur, + filterByText: this.options.filterByText, + onFilter: this.options.onFilter, + remote: this.options.filterRemote, + query: this.options.data, + keys: searchFields, + elements: (function(_this) { + return function() { + selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + return $(selector); + }; + })(this), + data: (function(_this) { + return function() { + return _this.fullData; + }; + })(this), + callback: (function(_this) { + return function(data) { + _this.parseData(data); + if (_this.filterInput.val() !== '') { + selector = SELECTABLE_CLASSES; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + if ($(_this.el).is('input')) { + currentIndex = -1; + } else { + $(selector, _this.dropdown).first().find('a').addClass('is-focused'); + currentIndex = 0; + } } + }; + })(this) + }); + } + // Event listeners + this.dropdown.on("shown.bs.dropdown", this.opened); + this.dropdown.on("hidden.bs.dropdown", this.hidden); + $(this.el).on("update.label", this.updateLabel); + this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); + this.dropdown.on('keyup', (function(_this) { + return function(e) { + // Escape key + if (e.which === 27) { + return $('.dropdown-menu-close', _this.dropdown).trigger('click'); + } + }; + })(this)); + this.dropdown.on('blur', 'a', (function(_this) { + return function(e) { + var $dropdownMenu, $relatedTarget; + if (e.relatedTarget != null) { + $relatedTarget = $(e.relatedTarget); + $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); + if ($dropdownMenu.length === 0) { + return _this.dropdown.removeClass('open'); } + } + }; + })(this)); + if (this.dropdown.find(".dropdown-toggle-page").length) { + this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.togglePage(); }; })(this)); + } + if (this.options.selectable) { + selector = ".dropdown-content a"; if (this.dropdown.find(".dropdown-toggle-page").length) { - this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) { - return function(e) { - e.preventDefault(); - e.stopPropagation(); - return _this.togglePage(); - }; - })(this)); - } - if (this.options.selectable) { - selector = ".dropdown-content a"; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content a"; + selector = ".dropdown-page-one .dropdown-content a"; + } + this.dropdown.on("click", selector, function(e) { + var $el, selected, selectedObj, isMarking; + $el = $(this); + selected = self.rowClicked($el); + selectedObj = selected ? selected[0] : null; + isMarking = selected ? selected[1] : null; + if (self.options.clicked) { + self.options.clicked(selectedObj, $el, e, isMarking); } - this.dropdown.on("click", selector, function(e) { - var $el, selected, selectedObj, isMarking; - $el = $(this); - selected = self.rowClicked($el); - selectedObj = selected ? selected[0] : null; - isMarking = selected ? selected[1] : null; - if (self.options.clicked) { - self.options.clicked(selectedObj, $el, e, isMarking); - } - // Update label right after all modifications in dropdown has been done - if (self.options.toggleLabel) { - self.updateLabel(selectedObj, $el, self); - } + // Update label right after all modifications in dropdown has been done + if (self.options.toggleLabel) { + self.updateLabel(selectedObj, $el, self); + } - $el.trigger('blur'); - }); - } + $el.trigger('blur'); + }); } + } - // Finds an element inside wrapper element - GitLabDropdown.prototype.getElement = function(selector) { - return this.dropdown.find(selector); - }; + // Finds an element inside wrapper element + GitLabDropdown.prototype.getElement = function(selector) { + return this.dropdown.find(selector); + }; - GitLabDropdown.prototype.toggleLoading = function() { - return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); - }; + GitLabDropdown.prototype.toggleLoading = function() { + return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); + }; - GitLabDropdown.prototype.togglePage = function() { - var menu; - menu = $('.dropdown-menu', this.dropdown); - if (menu.hasClass(PAGE_TWO_CLASS)) { - if (this.remote) { - this.remote.execute(); - } - } - menu.toggleClass(PAGE_TWO_CLASS); - // Focus first visible input on active page - return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); - }; - - GitLabDropdown.prototype.parseData = function(data) { - var full_html, groupData, html, name; - this.renderedData = data; - if (this.options.filterable && data.length === 0) { - // render no matching results - html = [this.noResults()]; - } else { - // Handle array groups - if (gl.utils.isObject(data)) { - html = []; - for (name in data) { - groupData = data[name]; - html.push(this.renderItem({ - header: name - // Add header for each group - }, name)); - this.renderData(groupData, name).map(function(item) { - return html.push(item); - }); - } - } else { - // Render each row - html = this.renderData(data); - } - } - // Render the full menu - full_html = this.renderMenu(html); - return this.appendMenu(full_html); - }; - - GitLabDropdown.prototype.renderData = function(data, group) { - if (group == null) { - group = false; + GitLabDropdown.prototype.togglePage = function() { + var menu; + menu = $('.dropdown-menu', this.dropdown); + if (menu.hasClass(PAGE_TWO_CLASS)) { + if (this.remote) { + this.remote.execute(); } - return data.map((function(_this) { - return function(obj, index) { - return _this.renderItem(obj, group, index); - }; - })(this)); - }; - - GitLabDropdown.prototype.shouldPropagate = function(e) { - var $target; - if (this.options.multiSelect) { - $target = $(e.target); - if ($target && !$target.hasClass('dropdown-menu-close') && - !$target.hasClass('dropdown-menu-close-icon') && - !$target.data('is-link')) { - e.stopPropagation(); - return false; - } else { - return true; + } + menu.toggleClass(PAGE_TWO_CLASS); + // Focus first visible input on active page + return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); + }; + + GitLabDropdown.prototype.parseData = function(data) { + var full_html, groupData, html, name; + this.renderedData = data; + if (this.options.filterable && data.length === 0) { + // render no matching results + html = [this.noResults()]; + } else { + // Handle array groups + if (gl.utils.isObject(data)) { + html = []; + for (name in data) { + groupData = data[name]; + html.push(this.renderItem({ + header: name + // Add header for each group + }, name)); + this.renderData(groupData, name).map(function(item) { + return html.push(item); + }); } + } else { + // Render each row + html = this.renderData(data); } - }; + } + // Render the full menu + full_html = this.renderMenu(html); + return this.appendMenu(full_html); + }; - GitLabDropdown.prototype.opened = function(e) { - var contentHtml; - this.resetRows(); - this.addArrowKeyEvent(); + GitLabDropdown.prototype.renderData = function(data, group) { + if (group == null) { + group = false; + } + return data.map((function(_this) { + return function(obj, index) { + return _this.renderItem(obj, group, index); + }; + })(this)); + }; - // Makes indeterminate items effective - if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - this.parseData(this.fullData); - } - contentHtml = $('.dropdown-content', this.dropdown).html(); - if (this.remote && contentHtml === "") { - this.remote.execute(); + GitLabDropdown.prototype.shouldPropagate = function(e) { + var $target; + if (this.options.multiSelect) { + $target = $(e.target); + if ($target && !$target.hasClass('dropdown-menu-close') && + !$target.hasClass('dropdown-menu-close-icon') && + !$target.data('is-link')) { + e.stopPropagation(); + return false; } else { - this.focusTextInput(); + return true; } + } + }; - if (this.options.showMenuAbove) { - this.positionMenuAbove(); - } + GitLabDropdown.prototype.opened = function(e) { + var contentHtml; + this.resetRows(); + this.addArrowKeyEvent(); - if (this.options.opened) { - this.options.opened.call(this, e); - } + // Makes indeterminate items effective + if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { + this.parseData(this.fullData); + } + contentHtml = $('.dropdown-content', this.dropdown).html(); + if (this.remote && contentHtml === "") { + this.remote.execute(); + } else { + this.focusTextInput(); + } + + if (this.options.showMenuAbove) { + this.positionMenuAbove(); + } - return this.dropdown.trigger('shown.gl.dropdown'); - }; + if (this.options.opened) { + this.options.opened.call(this, e); + } - GitLabDropdown.prototype.positionMenuAbove = function() { - var $button = $(this.el); - var $menu = this.dropdown.find('.dropdown-menu'); + return this.dropdown.trigger('shown.gl.dropdown'); + }; - $menu.css('top', ($button.height() + $menu.height()) * -1); - }; + GitLabDropdown.prototype.positionMenuAbove = function() { + var $button = $(this.el); + var $menu = this.dropdown.find('.dropdown-menu'); - GitLabDropdown.prototype.hidden = function(e) { - var $input; - this.resetRows(); - this.removeArrayKeyEvent(); - $input = this.dropdown.find(".dropdown-input-field"); - if (this.options.filterable) { - $input.blur(); - } - if (this.dropdown.find(".dropdown-toggle-page").length) { - $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); - } - if (this.options.hidden) { - this.options.hidden.call(this, e); - } - return this.dropdown.trigger('hidden.gl.dropdown'); - }; + $menu.css('top', ($button.height() + $menu.height()) * -1); + }; - // Render the full menu - GitLabDropdown.prototype.renderMenu = function(html) { - if (this.options.renderMenu) { - return this.options.renderMenu(html); - } else { - var ul = document.createElement('ul'); + GitLabDropdown.prototype.hidden = function(e) { + var $input; + this.resetRows(); + this.removeArrayKeyEvent(); + $input = this.dropdown.find(".dropdown-input-field"); + if (this.options.filterable) { + $input.blur(); + } + if (this.dropdown.find(".dropdown-toggle-page").length) { + $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); + } + if (this.options.hidden) { + this.options.hidden.call(this, e); + } + return this.dropdown.trigger('hidden.gl.dropdown'); + }; - for (var i = 0; i < html.length; i += 1) { - var el = html[i]; + // Render the full menu + GitLabDropdown.prototype.renderMenu = function(html) { + if (this.options.renderMenu) { + return this.options.renderMenu(html); + } else { + var ul = document.createElement('ul'); - if (el instanceof jQuery) { - el = el.get(0); - } + for (var i = 0; i < html.length; i += 1) { + var el = html[i]; - if (typeof el === 'string') { - ul.innerHTML += el; - } else { - ul.appendChild(el); - } + if (el instanceof jQuery) { + el = el.get(0); } - return ul; + if (typeof el === 'string') { + ul.innerHTML += el; + } else { + ul.appendChild(el); + } } - }; - // Append the menu into the dropdown - GitLabDropdown.prototype.appendMenu = function(html) { - return this.clearMenu().append(html); - }; + return ul; + } + }; + + // Append the menu into the dropdown + GitLabDropdown.prototype.appendMenu = function(html) { + return this.clearMenu().append(html); + }; - GitLabDropdown.prototype.clearMenu = function() { - var selector; - selector = '.dropdown-content'; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content"; - } + GitLabDropdown.prototype.clearMenu = function() { + var selector; + selector = '.dropdown-content'; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one .dropdown-content"; + } - return $(selector, this.dropdown).empty(); - }; + return $(selector, this.dropdown).empty(); + }; - GitLabDropdown.prototype.renderItem = function(data, group, index) { - var field, fieldName, html, selected, text, url, value; - if (group == null) { - group = false; + GitLabDropdown.prototype.renderItem = function(data, group, index) { + var field, fieldName, html, selected, text, url, value; + if (group == null) { + group = false; + } + if (index == null) { + // Render the row + index = false; + } + html = document.createElement('li'); + if (data === 'divider' || data === 'separator') { + html.className = data; + return html; + } + // Header + if (data.header != null) { + html.className = 'dropdown-header'; + html.innerHTML = data.header; + return html; + } + if (this.options.renderRow) { + // Call the render function + html = this.options.renderRow.call(this.options, data, this); + } else { + if (!selected) { + value = this.options.id ? this.options.id(data) : data.id; + fieldName = this.options.fieldName; + + if (value) { value = value.toString().replace(/'/g, '\\\''); } + + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + if (field.length) { + selected = true; + } } - if (index == null) { - // Render the row - index = false; + // Set URL + if (this.options.url != null) { + url = this.options.url(data); + } else { + url = data.url != null ? data.url : '#'; } - html = document.createElement('li'); - if (data === 'divider' || data === 'separator') { - html.className = data; - return html; + // Set Text + if (this.options.text != null) { + text = this.options.text(data); + } else { + text = data.text != null ? data.text : ''; } - // Header - if (data.header != null) { - html.className = 'dropdown-header'; - html.innerHTML = data.header; - return html; + if (this.highlight) { + text = this.highlightTextMatches(text, this.filterInput.val()); } - if (this.options.renderRow) { - // Call the render function - html = this.options.renderRow.call(this.options, data, this); - } else { - if (!selected) { - value = this.options.id ? this.options.id(data) : data.id; - fieldName = this.options.fieldName; - - if (value) { value = value.toString().replace(/'/g, '\\\''); } - - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); - if (field.length) { - selected = true; - } - } - // Set URL - if (this.options.url != null) { - url = this.options.url(data); - } else { - url = data.url != null ? data.url : '#'; - } - // Set Text - if (this.options.text != null) { - text = this.options.text(data); - } else { - text = data.text != null ? data.text : ''; - } - if (this.highlight) { - text = this.highlightTextMatches(text, this.filterInput.val()); - } - // Create the list item & the link - var link = document.createElement('a'); - - link.href = url; - link.innerHTML = text; + // Create the list item & the link + var link = document.createElement('a'); - if (selected) { - link.className = 'is-active'; - } - - if (group) { - link.dataset.group = group; - link.dataset.index = index; - } + link.href = url; + link.innerHTML = text; - html.appendChild(link); + if (selected) { + link.className = 'is-active'; } - return html; - }; - - GitLabDropdown.prototype.highlightTextMatches = function(text, term) { - var occurrences; - occurrences = fuzzaldrinPlus.match(text, term); - return text.split('').map(function(character, i) { - if (indexOf.call(occurrences, i) !== -1) { - return "" + character + ""; - } else { - return character; - } - }).join(''); - }; - - GitLabDropdown.prototype.noResults = function() { - var html; - return html = "
      "; - }; - - GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; - - fieldName = this.options.fieldName; - isInput = $(this.el).is('input'); - if (this.renderedData) { - groupName = el.data('group'); - if (groupName) { - selectedIndex = el.data('index'); - selectedObject = this.renderedData[groupName][selectedIndex]; - } else { - selectedIndex = el.closest('li').index(); - selectedObject = this.renderedData[selectedIndex]; - } + + if (group) { + link.dataset.group = group; + link.dataset.index = index; } - if (this.options.vue) { - if (el.hasClass(ACTIVE_CLASS)) { - el.removeClass(ACTIVE_CLASS); - } else { - el.addClass(ACTIVE_CLASS); - } + html.appendChild(link); + } + return html; + }; - return [selectedObject]; + GitLabDropdown.prototype.highlightTextMatches = function(text, term) { + var occurrences; + occurrences = fuzzaldrinPlus.match(text, term); + return text.split('').map(function(character, i) { + if (indexOf.call(occurrences, i) !== -1) { + return "" + character + ""; + } else { + return character; } + }).join(''); + }; - field = []; - value = this.options.id - ? this.options.id(selectedObject, el) - : selectedObject.id; - if (isInput) { - field = $(this.el); - } else if (value) { - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); - } + GitLabDropdown.prototype.noResults = function() { + var html; + return html = ""; + }; + + GitLabDropdown.prototype.rowClicked = function(el) { + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; - if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { - return; + fieldName = this.options.fieldName; + isInput = $(this.el).is('input'); + if (this.renderedData) { + groupName = el.data('group'); + if (groupName) { + selectedIndex = el.data('index'); + selectedObject = this.renderedData[groupName][selectedIndex]; + } else { + selectedIndex = el.closest('li').index(); + selectedObject = this.renderedData[selectedIndex]; } + } + if (this.options.vue) { if (el.hasClass(ACTIVE_CLASS)) { - isMarking = false; el.removeClass(ACTIVE_CLASS); - if (field && field.length) { - this.clearField(field, isInput); - } - } else if (el.hasClass(INDETERMINATE_CLASS)) { - isMarking = true; - el.addClass(ACTIVE_CLASS); - el.removeClass(INDETERMINATE_CLASS); - if (field && field.length && value == null) { - this.clearField(field, isInput); - } - if ((!field || !field.length) && fieldName) { - this.addInput(fieldName, value, selectedObject); - } } else { - isMarking = true; - if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { - this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); - if (!isInput) { - this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); - } - } - if (field && field.length && value == null) { - this.clearField(field, isInput); - } - // Toggle active class for the tick mark el.addClass(ACTIVE_CLASS); - if (value != null) { - if ((!field || !field.length) && fieldName) { - this.addInput(fieldName, value, selectedObject); - } else if (field && field.length) { - field.val(value).trigger('change'); - } - } } - return [selectedObject, isMarking]; - }; + return [selectedObject]; + } - GitLabDropdown.prototype.focusTextInput = function() { - if (this.options.filterable) { this.filterInput.focus(); } - }; + field = []; + value = this.options.id + ? this.options.id(selectedObject, el) + : selectedObject.id; + if (isInput) { + field = $(this.el); + } else if (value) { + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); + } - GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { - var $input; - // Create hidden input for form - $input = $('').attr('type', 'hidden').attr('name', fieldName).val(value); - if (this.options.inputId != null) { - $input.attr('id', this.options.inputId); - } - return this.dropdown.before($input); - }; - - GitLabDropdown.prototype.selectRowAtIndex = function(index) { - var $el, selector; - // If we pass an option index - if (typeof index !== "undefined") { - selector = SELECTABLE_CLASSES + ":eq(" + index + ") a"; - } else { - selector = ".dropdown-content .is-focused"; + if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { + return; + } + + if (el.hasClass(ACTIVE_CLASS)) { + isMarking = false; + el.removeClass(ACTIVE_CLASS); + if (field && field.length) { + this.clearField(field, isInput); + } + } else if (el.hasClass(INDETERMINATE_CLASS)) { + isMarking = true; + el.addClass(ACTIVE_CLASS); + el.removeClass(INDETERMINATE_CLASS); + if (field && field.length && value == null) { + this.clearField(field, isInput); + } + if ((!field || !field.length) && fieldName) { + this.addInput(fieldName, value, selectedObject); + } + } else { + isMarking = true; + if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { + this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); + if (!isInput) { + this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); + } } - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one " + selector; + if (field && field.length && value == null) { + this.clearField(field, isInput); } - // simulate a click on the first link - $el = $(selector, this.dropdown); - if ($el.length) { - var href = $el.attr('href'); - if (href && href !== '#') { - gl.utils.visitUrl(href); - } else { - $el.first().trigger('click'); + // Toggle active class for the tick mark + el.addClass(ACTIVE_CLASS); + if (value != null) { + if ((!field || !field.length) && fieldName) { + this.addInput(fieldName, value, selectedObject); + } else if (field && field.length) { + field.val(value).trigger('change'); } } - }; + } - GitLabDropdown.prototype.addArrowKeyEvent = function() { - var $input, ARROW_KEY_CODES, selector; - ARROW_KEY_CODES = [38, 40]; - $input = this.dropdown.find(".dropdown-input-field"); - selector = SELECTABLE_CLASSES; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one " + selector; + return [selectedObject, isMarking]; + }; + + GitLabDropdown.prototype.focusTextInput = function() { + if (this.options.filterable) { this.filterInput.focus(); } + }; + + GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { + var $input; + // Create hidden input for form + $input = $('').attr('type', 'hidden').attr('name', fieldName).val(value); + if (this.options.inputId != null) { + $input.attr('id', this.options.inputId); + } + return this.dropdown.before($input); + }; + + GitLabDropdown.prototype.selectRowAtIndex = function(index) { + var $el, selector; + // If we pass an option index + if (typeof index !== "undefined") { + selector = SELECTABLE_CLASSES + ":eq(" + index + ") a"; + } else { + selector = ".dropdown-content .is-focused"; + } + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + // simulate a click on the first link + $el = $(selector, this.dropdown); + if ($el.length) { + var href = $el.attr('href'); + if (href && href !== '#') { + gl.utils.visitUrl(href); + } else { + $el.first().trigger('click'); } - return $('body').on('keydown', (function(_this) { - return function(e) { - var $listItems, PREV_INDEX, currentKeyCode; - currentKeyCode = e.which; - if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { - e.preventDefault(); - e.stopImmediatePropagation(); - PREV_INDEX = currentIndex; - $listItems = $(selector, _this.dropdown); - // if @options.filterable - // $input.blur() - if (currentKeyCode === 40) { - // Move down - if (currentIndex < ($listItems.length - 1)) { - currentIndex += 1; - } - } else if (currentKeyCode === 38) { - // Move up - if (currentIndex > 0) { - currentIndex -= 1; - } + } + }; + + GitLabDropdown.prototype.addArrowKeyEvent = function() { + var $input, ARROW_KEY_CODES, selector; + ARROW_KEY_CODES = [38, 40]; + $input = this.dropdown.find(".dropdown-input-field"); + selector = SELECTABLE_CLASSES; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + return $('body').on('keydown', (function(_this) { + return function(e) { + var $listItems, PREV_INDEX, currentKeyCode; + currentKeyCode = e.which; + if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { + e.preventDefault(); + e.stopImmediatePropagation(); + PREV_INDEX = currentIndex; + $listItems = $(selector, _this.dropdown); + // if @options.filterable + // $input.blur() + if (currentKeyCode === 40) { + // Move down + if (currentIndex < ($listItems.length - 1)) { + currentIndex += 1; } - if (currentIndex !== PREV_INDEX) { - _this.highlightRowAtIndex($listItems, currentIndex); + } else if (currentKeyCode === 38) { + // Move up + if (currentIndex > 0) { + currentIndex -= 1; } - return false; } - if (currentKeyCode === 13 && currentIndex !== -1) { - e.preventDefault(); - _this.selectRowAtIndex(); + if (currentIndex !== PREV_INDEX) { + _this.highlightRowAtIndex($listItems, currentIndex); } - }; - })(this)); - }; - - GitLabDropdown.prototype.removeArrayKeyEvent = function() { - return $('body').off('keydown'); - }; - - GitLabDropdown.prototype.resetRows = function resetRows() { - currentIndex = -1; - $('.is-focused', this.dropdown).removeClass('is-focused'); - }; - - GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { - var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; - // Remove the class for the previously focused row - $('.is-focused', this.dropdown).removeClass('is-focused'); - // Update the class for the row at the specific index - $listItem = $listItems.eq(index); - $listItem.find('a:first-child').addClass("is-focused"); - // Dropdown content scroll area - $dropdownContent = $listItem.closest('.dropdown-content'); - dropdownScrollTop = $dropdownContent.scrollTop(); - dropdownContentHeight = $dropdownContent.outerHeight(); - dropdownContentTop = $dropdownContent.prop('offsetTop'); - dropdownContentBottom = dropdownContentTop + dropdownContentHeight; - // Get the offset bottom of the list item - listItemHeight = $listItem.outerHeight(); - listItemTop = $listItem.prop('offsetTop'); - listItemBottom = listItemTop + listItemHeight; - if (!index) { - // Scroll the dropdown content to the top - $dropdownContent.scrollTop(0); - } else if (index === ($listItems.length - 1)) { - // Scroll the dropdown content to the bottom - $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); - } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { - // Scroll the dropdown content down - $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); - } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { - // Scroll the dropdown content up - return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); - } - }; + return false; + } + if (currentKeyCode === 13 && currentIndex !== -1) { + e.preventDefault(); + _this.selectRowAtIndex(); + } + }; + })(this)); + }; - GitLabDropdown.prototype.updateLabel = function(selected, el, instance) { - if (selected == null) { - selected = null; - } - if (el == null) { - el = null; - } - if (instance == null) { - instance = null; - } - return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); - }; + GitLabDropdown.prototype.removeArrayKeyEvent = function() { + return $('body').off('keydown'); + }; - GitLabDropdown.prototype.clearField = function(field, isInput) { - return isInput ? field.val('') : field.remove(); - }; + GitLabDropdown.prototype.resetRows = function resetRows() { + currentIndex = -1; + $('.is-focused', this.dropdown).removeClass('is-focused'); + }; - return GitLabDropdown; - })(); + GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { + var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; + // Remove the class for the previously focused row + $('.is-focused', this.dropdown).removeClass('is-focused'); + // Update the class for the row at the specific index + $listItem = $listItems.eq(index); + $listItem.find('a:first-child').addClass("is-focused"); + // Dropdown content scroll area + $dropdownContent = $listItem.closest('.dropdown-content'); + dropdownScrollTop = $dropdownContent.scrollTop(); + dropdownContentHeight = $dropdownContent.outerHeight(); + dropdownContentTop = $dropdownContent.prop('offsetTop'); + dropdownContentBottom = dropdownContentTop + dropdownContentHeight; + // Get the offset bottom of the list item + listItemHeight = $listItem.outerHeight(); + listItemTop = $listItem.prop('offsetTop'); + listItemBottom = listItemTop + listItemHeight; + if (!index) { + // Scroll the dropdown content to the top + $dropdownContent.scrollTop(0); + } else if (index === ($listItems.length - 1)) { + // Scroll the dropdown content to the bottom + $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); + } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { + // Scroll the dropdown content down + $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); + } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { + // Scroll the dropdown content up + return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); + } + }; - $.fn.glDropdown = function(opts) { - return this.each(function() { - if (!$.data(this, 'glDropdown')) { - return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); - } - }); + GitLabDropdown.prototype.updateLabel = function(selected, el, instance) { + if (selected == null) { + selected = null; + } + if (el == null) { + el = null; + } + if (instance == null) { + instance = null; + } + return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); + }; + + GitLabDropdown.prototype.clearField = function(field, isInput) { + return isInput ? field.val('') : field.remove(); }; -}).call(window); + + return GitLabDropdown; +})(); + +$.fn.glDropdown = function(opts) { + return this.each(function() { + if (!$.data(this, 'glDropdown')) { + return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); + } + }); +}; diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index f7cbecc0385..76de249ac3b 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -1,164 +1,162 @@ -/* eslint-disable no-param-reassign */ -((global) => { - /* - * This class overrides the browser's validation error bubbles, displaying custom - * error messages for invalid fields instead. To begin validating any form, add the - * class `gl-show-field-errors` to the form element, and ensure error messages are - * declared in each inputs' `title` attribute. If no title is declared for an invalid - * field the user attempts to submit, "This field is required." will be shown by default. - * - * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. - * - * Set a custom error anchor for error message to be injected after with the - * class `gl-field-error-anchor` - * - * Examples: - * - * Basic: - * - *
      - * - *
      - * - * Ignore specific inputs (e.g. UsernameValidator): - * - *
      - *
      - * - *
      - *
      - * - * Custom Error Anchor (allows error message to be injected after specified element): - * - *
      - *
      - * - * // Error message typically injected here - *
      - * // Error message now injected here - *
      - * - * */ - - /* - * Regex Patterns in use: - * - * Only alphanumeric: : "[a-zA-Z0-9]+" - * No special characters : "[a-zA-Z0-9-_]+", - * - * */ - - const errorMessageClass = 'gl-field-error'; - const inputErrorClass = 'gl-field-error-outline'; - const errorAnchorSelector = '.gl-field-error-anchor'; - const ignoreInputSelector = '.gl-field-error-ignore'; - - class GlFieldError { - constructor({ input, formErrors }) { - this.inputElement = $(input); - this.inputDomElement = this.inputElement.get(0); - this.form = formErrors; - this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; - this.fieldErrorElement = $(`

      ${this.errorMessage}

      `); - - this.state = { - valid: false, - empty: true, - }; - - this.initFieldValidation(); - } +/** + * This class overrides the browser's validation error bubbles, displaying custom + * error messages for invalid fields instead. To begin validating any form, add the + * class `gl-show-field-errors` to the form element, and ensure error messages are + * declared in each inputs' `title` attribute. If no title is declared for an invalid + * field the user attempts to submit, "This field is required." will be shown by default. + * + * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. + * + * Set a custom error anchor for error message to be injected after with the + * class `gl-field-error-anchor` + * + * Examples: + * + * Basic: + * + *
      + * + *
      + * + * Ignore specific inputs (e.g. UsernameValidator): + * + *
      + *
      + * + *
      + *
      + * + * Custom Error Anchor (allows error message to be injected after specified element): + * + *
      + *
      + * + * // Error message typically injected here + *
      + * // Error message now injected here + *
      + * + */ + +/** + * Regex Patterns in use: + * + * Only alphanumeric: : "[a-zA-Z0-9]+" + * No special characters : "[a-zA-Z0-9-_]+", + * + */ + +const errorMessageClass = 'gl-field-error'; +const inputErrorClass = 'gl-field-error-outline'; +const errorAnchorSelector = '.gl-field-error-anchor'; +const ignoreInputSelector = '.gl-field-error-ignore'; + +class GlFieldError { + constructor({ input, formErrors }) { + this.inputElement = $(input); + this.inputDomElement = this.inputElement.get(0); + this.form = formErrors; + this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; + this.fieldErrorElement = $(`

      ${this.errorMessage}

      `); + + this.state = { + valid: false, + empty: true, + }; + + this.initFieldValidation(); + } - initFieldValidation() { - const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); - const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; + initFieldValidation() { + const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); + const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; - // hidden when injected into DOM - errorAnchor.after(this.fieldErrorElement); - this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); - this.scopedSiblings = this.safelySelectSiblings(); - } + // hidden when injected into DOM + errorAnchor.after(this.fieldErrorElement); + this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); + this.scopedSiblings = this.safelySelectSiblings(); + } - safelySelectSiblings() { - // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled - const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); - const parentContainer = this.inputElement.parent('.form-group'); + safelySelectSiblings() { + // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled + const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); + const parentContainer = this.inputElement.parent('.form-group'); - // Only select siblings when they're scoped within a form-group with one input - const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; + // Only select siblings when they're scoped within a form-group with one input + const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; - return safelyScoped ? unignoredSiblings : this.fieldErrorElement; - } + return safelyScoped ? unignoredSiblings : this.fieldErrorElement; + } - renderValidity() { - this.renderClear(); + renderValidity() { + this.renderClear(); - if (this.state.valid) { - this.renderValid(); - } else if (this.state.empty) { - this.renderEmpty(); - } else if (!this.state.valid) { - this.renderInvalid(); - } + if (this.state.valid) { + this.renderValid(); + } else if (this.state.empty) { + this.renderEmpty(); + } else if (!this.state.valid) { + this.renderInvalid(); } + } - handleInvalidSubmit(event) { - event.preventDefault(); - const currentValue = this.accessCurrentValue(); - this.state.valid = false; - this.state.empty = currentValue === ''; - - this.renderValidity(); - this.form.focusOnFirstInvalid.apply(this.form); - // For UX, wait til after first invalid submission to check each keyup - this.inputElement.off('keyup.fieldValidator') - .on('keyup.fieldValidator', this.updateValidity.bind(this)); - } + handleInvalidSubmit(event) { + event.preventDefault(); + const currentValue = this.accessCurrentValue(); + this.state.valid = false; + this.state.empty = currentValue === ''; + + this.renderValidity(); + this.form.focusOnFirstInvalid.apply(this.form); + // For UX, wait til after first invalid submission to check each keyup + this.inputElement.off('keyup.fieldValidator') + .on('keyup.fieldValidator', this.updateValidity.bind(this)); + } - /* Get or set current input value */ - accessCurrentValue(newVal) { - return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); - } + /* Get or set current input value */ + accessCurrentValue(newVal) { + return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); + } - getInputValidity() { - return this.inputDomElement.validity.valid; - } + getInputValidity() { + return this.inputDomElement.validity.valid; + } - updateValidity() { - const inputVal = this.accessCurrentValue(); - this.state.empty = !inputVal.length; - this.state.valid = this.getInputValidity(); - this.renderValidity(); - } + updateValidity() { + const inputVal = this.accessCurrentValue(); + this.state.empty = !inputVal.length; + this.state.valid = this.getInputValidity(); + this.renderValidity(); + } - renderValid() { - return this.renderClear(); - } + renderValid() { + return this.renderClear(); + } - renderEmpty() { - return this.renderInvalid(); - } + renderEmpty() { + return this.renderInvalid(); + } - renderInvalid() { - this.inputElement.addClass(inputErrorClass); - this.scopedSiblings.hide(); - return this.fieldErrorElement.show(); - } + renderInvalid() { + this.inputElement.addClass(inputErrorClass); + this.scopedSiblings.hide(); + return this.fieldErrorElement.show(); + } - renderClear() { - const inputVal = this.accessCurrentValue(); - if (!inputVal.split(' ').length) { - const trimmedInput = inputVal.trim(); - this.accessCurrentValue(trimmedInput); - } - this.inputElement.removeClass(inputErrorClass); - this.scopedSiblings.hide(); - this.fieldErrorElement.hide(); + renderClear() { + const inputVal = this.accessCurrentValue(); + if (!inputVal.split(' ').length) { + const trimmedInput = inputVal.trim(); + this.accessCurrentValue(trimmedInput); } + this.inputElement.removeClass(inputErrorClass); + this.scopedSiblings.hide(); + this.fieldErrorElement.hide(); } +} - global.GlFieldError = GlFieldError; -})(window.gl || (window.gl = {})); +window.gl = window.gl || {}; +window.gl.GlFieldError = GlFieldError; diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index e9add115429..636258ec555 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -2,47 +2,46 @@ require('./gl_field_error'); -((global) => { - const customValidationFlag = 'gl-field-error-ignore'; - - class GlFieldErrors { - constructor(form) { - this.form = $(form); - this.state = { - inputs: [], - valid: false - }; - this.initValidators(); - } +const customValidationFlag = 'gl-field-error-ignore'; + +class GlFieldErrors { + constructor(form) { + this.form = $(form); + this.state = { + inputs: [], + valid: false + }; + this.initValidators(); + } - initValidators () { - // register selectors here as needed - const validateSelectors = [':text', ':password', '[type=email]'] - .map((selector) => `input${selector}`).join(','); + initValidators () { + // register selectors here as needed + const validateSelectors = [':text', ':password', '[type=email]'] + .map((selector) => `input${selector}`).join(','); - this.state.inputs = this.form.find(validateSelectors).toArray() - .filter((input) => !input.classList.contains(customValidationFlag)) - .map((input) => new global.GlFieldError({ input, formErrors: this })); + this.state.inputs = this.form.find(validateSelectors).toArray() + .filter((input) => !input.classList.contains(customValidationFlag)) + .map((input) => new window.gl.GlFieldError({ input, formErrors: this })); - this.form.on('submit', this.catchInvalidFormSubmit); - } + this.form.on('submit', this.catchInvalidFormSubmit); + } - /* Neccessary to prevent intercept and override invalid form submit - * because Safari & iOS quietly allow form submission when form is invalid - * and prevents disabling of invalid submit button by application.js */ + /* Neccessary to prevent intercept and override invalid form submit + * because Safari & iOS quietly allow form submission when form is invalid + * and prevents disabling of invalid submit button by application.js */ - catchInvalidFormSubmit (event) { - if (!event.currentTarget.checkValidity()) { - event.preventDefault(); - event.stopPropagation(); - } + catchInvalidFormSubmit (event) { + if (!event.currentTarget.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); } + } - focusOnFirstInvalid () { - const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; - firstInvalid.inputElement.focus(); - } + focusOnFirstInvalid () { + const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; + firstInvalid.inputElement.focus(); } +} - global.GlFieldErrors = GlFieldErrors; -})(window.gl || (window.gl = {})); +window.gl = window.gl || {}; +window.gl.GlFieldErrors = GlFieldErrors; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 0b446ff364a..e7c98e16581 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -3,90 +3,88 @@ /* global DropzoneInput */ /* global autosize */ -(() => { - const global = window.gl || (window.gl = {}); +window.gl = window.gl || {}; - function GLForm(form) { - this.form = form; - this.textarea = this.form.find('textarea.js-gfm-input'); - // Before we start, we should clean up any previous data for this form - this.destroy(); - // Setup the form - this.setupForm(); - this.form.data('gl-form', this); - } +function GLForm(form) { + this.form = form; + this.textarea = this.form.find('textarea.js-gfm-input'); + // Before we start, we should clean up any previous data for this form + this.destroy(); + // Setup the form + this.setupForm(); + this.form.data('gl-form', this); +} - GLForm.prototype.destroy = function() { - // Clean form listeners - this.clearEventListeners(); - return this.form.data('gl-form', null); - }; +GLForm.prototype.destroy = function() { + // Clean form listeners + this.clearEventListeners(); + return this.form.data('gl-form', null); +}; - GLForm.prototype.setupForm = function() { - var isNewForm; - isNewForm = this.form.is(':not(.gfm-form)'); - this.form.removeClass('js-new-note-form'); - if (isNewForm) { - this.form.find('.div-dropzone').remove(); - this.form.addClass('gfm-form'); - // remove notify commit author checkbox for non-commit notes - gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); - gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); - new DropzoneInput(this.form); - autosize(this.textarea); - // form and textarea event listeners - this.addEventListeners(); - } - gl.text.init(this.form); - // hide discard button - this.form.find('.js-note-discard').hide(); - this.form.show(); - if (this.isAutosizeable) this.setupAutosize(); - }; +GLForm.prototype.setupForm = function() { + var isNewForm; + isNewForm = this.form.is(':not(.gfm-form)'); + this.form.removeClass('js-new-note-form'); + if (isNewForm) { + this.form.find('.div-dropzone').remove(); + this.form.addClass('gfm-form'); + // remove notify commit author checkbox for non-commit notes + gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); + gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); + new DropzoneInput(this.form); + autosize(this.textarea); + // form and textarea event listeners + this.addEventListeners(); + } + gl.text.init(this.form); + // hide discard button + this.form.find('.js-note-discard').hide(); + this.form.show(); + if (this.isAutosizeable) this.setupAutosize(); +}; - GLForm.prototype.setupAutosize = function () { - this.textarea.off('autosize:resized') - .on('autosize:resized', this.setHeightData.bind(this)); +GLForm.prototype.setupAutosize = function () { + this.textarea.off('autosize:resized') + .on('autosize:resized', this.setHeightData.bind(this)); - this.textarea.off('mouseup.autosize') - .on('mouseup.autosize', this.destroyAutosize.bind(this)); + this.textarea.off('mouseup.autosize') + .on('mouseup.autosize', this.destroyAutosize.bind(this)); - setTimeout(() => { - autosize(this.textarea); - this.textarea.css('resize', 'vertical'); - }, 0); - }; + setTimeout(() => { + autosize(this.textarea); + this.textarea.css('resize', 'vertical'); + }, 0); +}; - GLForm.prototype.setHeightData = function () { - this.textarea.data('height', this.textarea.outerHeight()); - }; +GLForm.prototype.setHeightData = function () { + this.textarea.data('height', this.textarea.outerHeight()); +}; - GLForm.prototype.destroyAutosize = function () { - const outerHeight = this.textarea.outerHeight(); +GLForm.prototype.destroyAutosize = function () { + const outerHeight = this.textarea.outerHeight(); - if (this.textarea.data('height') === outerHeight) return; + if (this.textarea.data('height') === outerHeight) return; - autosize.destroy(this.textarea); + autosize.destroy(this.textarea); - this.textarea.data('height', outerHeight); - this.textarea.outerHeight(outerHeight); - this.textarea.css('max-height', window.outerHeight); - }; + this.textarea.data('height', outerHeight); + this.textarea.outerHeight(outerHeight); + this.textarea.css('max-height', window.outerHeight); +}; - GLForm.prototype.clearEventListeners = function() { - this.textarea.off('focus'); - this.textarea.off('blur'); - return gl.text.removeListeners(this.form); - }; +GLForm.prototype.clearEventListeners = function() { + this.textarea.off('focus'); + this.textarea.off('blur'); + return gl.text.removeListeners(this.form); +}; - GLForm.prototype.addEventListeners = function() { - this.textarea.on('focus', function() { - return $(this).closest('.md-area').addClass('is-focused'); - }); - return this.textarea.on('blur', function() { - return $(this).closest('.md-area').removeClass('is-focused'); - }); - }; +GLForm.prototype.addEventListeners = function() { + this.textarea.on('focus', function() { + return $(this).closest('.md-area').addClass('is-focused'); + }); + return this.textarea.on('blur', function() { + return $(this).closest('.md-area').removeClass('is-focused'); + }); +}; - global.GLForm = GLForm; -})(); +window.gl.GLForm = GLForm; diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index c5cb273c5b2..f03b47b1c1d 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,20 +1,19 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ -(function() { - this.GroupAvatar = (function() { - function GroupAvatar() { - $('.js-choose-group-avatar-button').on("click", function() { - var form; - form = $(this).closest("form"); - return form.find(".js-group-avatar-input").click(); - }); - $('.js-group-avatar-input').on("change", function() { - var filename, form; - form = $(this).closest("form"); - filename = $(this).val().replace(/^.*[\\\/]/, ''); - return form.find(".js-avatar-filename").text(filename); - }); - } - return GroupAvatar; - })(); -}).call(window); +window.GroupAvatar = (function() { + function GroupAvatar() { + $('.js-choose-group-avatar-button').on("click", function() { + var form; + form = $(this).closest("form"); + return form.find(".js-group-avatar-input").click(); + }); + $('.js-group-avatar-input').on("change", function() { + var filename, form; + form = $(this).closest("form"); + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find(".js-avatar-filename").text(filename); + }); + } + + return GroupAvatar; +})(); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index 15e695e81cf..7dc9ce898e8 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,53 +1,52 @@ /* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ -(function(global) { - class GroupLabelSubscription { - constructor(container) { - const $container = $(container); - this.$dropdown = $container.find('.dropdown'); - this.$subscribeButtons = $container.find('.js-subscribe-button'); - this.$unsubscribeButtons = $container.find('.js-unsubscribe-button'); - - this.$subscribeButtons.on('click', this.subscribe.bind(this)); - this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this)); - } - - unsubscribe(event) { - event.preventDefault(); - - const url = this.$unsubscribeButtons.attr('data-url'); - - $.ajax({ - type: 'POST', - url: url - }).done(() => { - this.toggleSubscriptionButtons(); - this.$unsubscribeButtons.removeAttr('data-url'); - }); - } - - subscribe(event) { - event.preventDefault(); - - const $btn = $(event.currentTarget); - const url = $btn.attr('data-url'); - - this.$unsubscribeButtons.attr('data-url', url); - - $.ajax({ - type: 'POST', - url: url - }).done(() => { - this.toggleSubscriptionButtons(); - }); - } - - toggleSubscriptionButtons() { - this.$dropdown.toggleClass('hidden'); - this.$subscribeButtons.toggleClass('hidden'); - this.$unsubscribeButtons.toggleClass('hidden'); - } +class GroupLabelSubscription { + constructor(container) { + const $container = $(container); + this.$dropdown = $container.find('.dropdown'); + this.$subscribeButtons = $container.find('.js-subscribe-button'); + this.$unsubscribeButtons = $container.find('.js-unsubscribe-button'); + + this.$subscribeButtons.on('click', this.subscribe.bind(this)); + this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this)); } - global.GroupLabelSubscription = GroupLabelSubscription; -})(window.gl || (window.gl = {})); + unsubscribe(event) { + event.preventDefault(); + + const url = this.$unsubscribeButtons.attr('data-url'); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + this.$unsubscribeButtons.removeAttr('data-url'); + }); + } + + subscribe(event) { + event.preventDefault(); + + const $btn = $(event.currentTarget); + const url = $btn.attr('data-url'); + + this.$unsubscribeButtons.attr('data-url', url); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + }); + } + + toggleSubscriptionButtons() { + this.$dropdown.toggleClass('hidden'); + this.$subscribeButtons.toggleClass('hidden'); + this.$unsubscribeButtons.toggleClass('hidden'); + } +} + +window.gl = window.gl || {}; +window.gl.GroupLabelSubscription = GroupLabelSubscription; diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 6b937e7fa0f..e5dfa30edab 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,71 +1,69 @@ /* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */ /* global Api */ -(function() { - var slice = [].slice; +var slice = [].slice; - this.GroupsSelect = (function() { - function GroupsSelect() { - $('.ajax-groups-select').each((function(_this) { - return function(i, select) { - var all_available, skip_groups; - all_available = $(select).data('all-available'); - skip_groups = $(select).data('skip-groups') || []; - return $(select).select2({ - placeholder: "Search for a group", - multiple: $(select).hasClass('multiselect'), - minimumInputLength: 0, - query: function(query) { - var options = { all_available: all_available, skip_groups: skip_groups }; - return Api.groups(query.term, options, function(groups) { - var data; - data = { - results: groups - }; - return query.callback(data); - }); - }, - initSelection: function(element, callback) { - var id; - id = $(element).val(); - if (id !== "") { - return Api.group(id, callback); - } - }, - formatResult: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatResult.apply(_this, args); - }, - formatSelection: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatSelection.apply(_this, args); - }, - dropdownCssClass: "ajax-groups-dropdown", - // we do not want to escape markup since we are displaying html in results - escapeMarkup: function(m) { - return m; +window.GroupsSelect = (function() { + function GroupsSelect() { + $('.ajax-groups-select').each((function(_this) { + return function(i, select) { + var all_available, skip_groups; + all_available = $(select).data('all-available'); + skip_groups = $(select).data('skip-groups') || []; + return $(select).select2({ + placeholder: "Search for a group", + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + var options = { all_available: all_available, skip_groups: skip_groups }; + return Api.groups(query.term, options, function(groups) { + var data; + data = { + results: groups + }; + return query.callback(data); + }); + }, + initSelection: function(element, callback) { + var id; + id = $(element).val(); + if (id !== "") { + return Api.group(id, callback); } - }); - }; - })(this)); - } + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: "ajax-groups-dropdown", + // we do not want to escape markup since we are displaying html in results + escapeMarkup: function(m) { + return m; + } + }); + }; + })(this)); + } - GroupsSelect.prototype.formatResult = function(group) { - var avatar; - if (group.avatar_url) { - avatar = group.avatar_url; - } else { - avatar = gon.default_avatar_url; - } - return "
      " + group.full_name + "
      " + group.full_path + "
      "; - }; + GroupsSelect.prototype.formatResult = function(group) { + var avatar; + if (group.avatar_url) { + avatar = group.avatar_url; + } else { + avatar = gon.default_avatar_url; + } + return "
      " + group.full_name + "
      " + group.full_path + "
      "; + }; - GroupsSelect.prototype.formatSelection = function(group) { - return group.full_name; - }; + GroupsSelect.prototype.formatSelection = function(group) { + return group.full_name; + }; - return GroupsSelect; - })(); -}).call(window); + return GroupsSelect; +})(); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index a853c3aeb1f..34f44dad7a5 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,8 +1,7 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */ -(function() { - $(document).on('todo:toggle', function(e, count) { - var $todoPendingCount = $('.todos-pending-count'); - $todoPendingCount.text(gl.text.highCountTrim(count)); - $todoPendingCount.toggleClass('hidden', count === 0); - }); -})(); +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */ + +$(document).on('todo:toggle', function(e, count) { + var $todoPendingCount = $('.todos-pending-count'); + $todoPendingCount.text(gl.text.highCountTrim(count)); + $todoPendingCount.toggleClass('hidden', count === 0); +}); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 604ed91627a..c078af23ef9 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -206,189 +206,187 @@ import './visibility_select'; import './wikis'; import './zen_mode'; -(function () { - document.addEventListener('beforeunload', function () { - // Unbind scroll events - $(document).off('scroll'); - // Close any open tooltips - $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); - }); - - window.addEventListener('hashchange', gl.utils.handleLocationHash); - window.addEventListener('load', function onLoad() { - window.removeEventListener('load', onLoad, false); - gl.utils.handleLocationHash(); - }, false); +document.addEventListener('beforeunload', function () { + // Unbind scroll events + $(document).off('scroll'); + // Close any open tooltips + $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); +}); - $(function () { - var $body = $('body'); - var $document = $(document); - var $window = $(window); - var $sidebarGutterToggle = $('.js-sidebar-toggle'); - var $flash = $('.flash-container'); - var bootstrapBreakpoint = bp.getBreakpointSize(); - var fitSidebarForSize; +window.addEventListener('hashchange', gl.utils.handleLocationHash); +window.addEventListener('load', function onLoad() { + window.removeEventListener('load', onLoad, false); + gl.utils.handleLocationHash(); +}, false); - // Set the default path for all cookies to GitLab's root directory - Cookies.defaults.path = gon.relative_url_root || '/'; +$(function () { + var $body = $('body'); + var $document = $(document); + var $window = $(window); + var $sidebarGutterToggle = $('.js-sidebar-toggle'); + var $flash = $('.flash-container'); + var bootstrapBreakpoint = bp.getBreakpointSize(); + var fitSidebarForSize; - // `hashchange` is not triggered when link target is already in window.location - $body.on('click', 'a[href^="#"]', function() { - var href = this.getAttribute('href'); - if (href.substr(1) === gl.utils.getLocationHash()) { - setTimeout(gl.utils.handleLocationHash, 1); - } - }); + // Set the default path for all cookies to GitLab's root directory + Cookies.defaults.path = gon.relative_url_root || '/'; - // prevent default action for disabled buttons - $('.btn').click(function(e) { - if ($(this).hasClass('disabled')) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }); + // `hashchange` is not triggered when link target is already in window.location + $body.on('click', 'a[href^="#"]', function() { + var href = this.getAttribute('href'); + if (href.substr(1) === gl.utils.getLocationHash()) { + setTimeout(gl.utils.handleLocationHash, 1); + } + }); - $('.js-select-on-focus').on('focusin', function () { - return $(this).select().one('mouseup', function (e) { - return e.preventDefault(); - }); - // Click a .js-select-on-focus field, select the contents - // Prevent a mouseup event from deselecting the input - }); - $('.remove-row').bind('ajax:success', function () { - $(this).tooltip('destroy') - .closest('li') - .fadeOut(); - }); - $('.js-remove-tr').bind('ajax:before', function () { - return $(this).hide(); - }); - $('.js-remove-tr').bind('ajax:success', function () { - return $(this).closest('tr').fadeOut(); - }); - $('select.select2').select2({ - width: 'resolve', - // Initialize select2 selects - dropdownAutoWidth: true - }); - $('.js-select2').bind('select2-close', function () { - return setTimeout((function () { - $('.select2-container-active').removeClass('select2-container-active'); - return $(':focus').blur(); - }), 1); - // Close select2 on escape - }); - // Initialize tooltips - $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; - $body.tooltip({ - selector: '.has-tooltip, [data-toggle="tooltip"]', - placement: function (tip, el) { - return $(el).data('placement') || 'bottom'; - } - }); - $('.trigger-submit').on('change', function () { - return $(this).parents('form').submit(); - // Form submitter - }); - gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); - // Flash - if ($flash.length > 0) { - $flash.click(function () { - return $(this).fadeOut(); - }); - $flash.show(); + // prevent default action for disabled buttons + $('.btn').click(function(e) { + if ($(this).hasClass('disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; } - // Disable form buttons while a form is submitting - $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { - var buttons; - buttons = $('[type="submit"]', this); - switch (e.type) { - case 'ajax:beforeSend': - case 'submit': - return buttons.disable(); - default: - return buttons.enable(); - } - }); - $(document).ajaxError(function (e, xhrObj) { - var ref = xhrObj.status; - if (xhrObj.status === 401) { - return new Flash('You need to be logged in.', 'alert'); - } else if (ref === 404 || ref === 500) { - return new Flash('Something went wrong on our end.', 'alert'); - } - }); - $('.account-box').hover(function () { - // Show/Hide the profile menu when hovering the account box - return $(this).toggleClass('hover'); - }); - $document.on('click', '.diff-content .js-show-suppressed-diff', function () { - var $container; - $container = $(this).parent(); - $container.next('table').show(); - return $container.remove(); - // Commit show suppressed diff - }); - $('.navbar-toggle').on('click', function () { - $('.header-content .title').toggle(); - $('.header-content .header-logo').toggle(); - $('.header-content .navbar-collapse').toggle(); - return $('.navbar-toggle').toggleClass('active'); - }); - // Show/hide comments on diff - $body.on('click', '.js-toggle-diff-comments', function (e) { - var $this = $(this); - var notesHolders = $this.closest('.diff-file').find('.notes_holder'); - $this.toggleClass('active'); - if ($this.hasClass('active')) { - notesHolders.show().find('.hide, .content').show(); - } else { - notesHolders.hide().find('.content').hide(); - } - $(document).trigger('toggle.comments'); + }); + + $('.js-select-on-focus').on('focusin', function () { + return $(this).select().one('mouseup', function (e) { return e.preventDefault(); }); - $document.off('click', '.js-confirm-danger'); - $document.on('click', '.js-confirm-danger', function (e) { - var btn = $(e.target); - var form = btn.closest('form'); - var text = btn.data('confirm-danger-message'); - e.preventDefault(); - return new ConfirmDangerModal(form, text); - }); - $('input[type="search"]').each(function () { - var $this = $(this); - $this.attr('value', $this.val()); - }); - $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () { - var $this; - $this = $(this); - return $this.attr('value', $this.val()); - }); - $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) { - var $gutterIcon; - if (breakpoint === 'sm' || breakpoint === 'xs') { - $gutterIcon = $sidebarGutterToggle.find('i'); - if ($gutterIcon.hasClass('fa-angle-double-right')) { - return $sidebarGutterToggle.trigger('click'); - } - } + // Click a .js-select-on-focus field, select the contents + // Prevent a mouseup event from deselecting the input + }); + $('.remove-row').bind('ajax:success', function () { + $(this).tooltip('destroy') + .closest('li') + .fadeOut(); + }); + $('.js-remove-tr').bind('ajax:before', function () { + return $(this).hide(); + }); + $('.js-remove-tr').bind('ajax:success', function () { + return $(this).closest('tr').fadeOut(); + }); + $('select.select2').select2({ + width: 'resolve', + // Initialize select2 selects + dropdownAutoWidth: true + }); + $('.js-select2').bind('select2-close', function () { + return setTimeout((function () { + $('.select2-container-active').removeClass('select2-container-active'); + return $(':focus').blur(); + }), 1); + // Close select2 on escape + }); + // Initialize tooltips + $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; + $body.tooltip({ + selector: '.has-tooltip, [data-toggle="tooltip"]', + placement: function (tip, el) { + return $(el).data('placement') || 'bottom'; + } + }); + $('.trigger-submit').on('change', function () { + return $(this).parents('form').submit(); + // Form submitter + }); + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); + // Flash + if ($flash.length > 0) { + $flash.click(function () { + return $(this).fadeOut(); }); - fitSidebarForSize = function () { - var oldBootstrapBreakpoint; - oldBootstrapBreakpoint = bootstrapBreakpoint; - bootstrapBreakpoint = bp.getBreakpointSize(); - if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { - return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + $flash.show(); + } + // Disable form buttons while a form is submitting + $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { + var buttons; + buttons = $('[type="submit"]', this); + switch (e.type) { + case 'ajax:beforeSend': + case 'submit': + return buttons.disable(); + default: + return buttons.enable(); + } + }); + $(document).ajaxError(function (e, xhrObj) { + var ref = xhrObj.status; + if (xhrObj.status === 401) { + return new Flash('You need to be logged in.', 'alert'); + } else if (ref === 404 || ref === 500) { + return new Flash('Something went wrong on our end.', 'alert'); + } + }); + $('.account-box').hover(function () { + // Show/Hide the profile menu when hovering the account box + return $(this).toggleClass('hover'); + }); + $document.on('click', '.diff-content .js-show-suppressed-diff', function () { + var $container; + $container = $(this).parent(); + $container.next('table').show(); + return $container.remove(); + // Commit show suppressed diff + }); + $('.navbar-toggle').on('click', function () { + $('.header-content .title').toggle(); + $('.header-content .header-logo').toggle(); + $('.header-content .navbar-collapse').toggle(); + return $('.navbar-toggle').toggleClass('active'); + }); + // Show/hide comments on diff + $body.on('click', '.js-toggle-diff-comments', function (e) { + var $this = $(this); + var notesHolders = $this.closest('.diff-file').find('.notes_holder'); + $this.toggleClass('active'); + if ($this.hasClass('active')) { + notesHolders.show().find('.hide, .content').show(); + } else { + notesHolders.hide().find('.content').hide(); + } + $(document).trigger('toggle.comments'); + return e.preventDefault(); + }); + $document.off('click', '.js-confirm-danger'); + $document.on('click', '.js-confirm-danger', function (e) { + var btn = $(e.target); + var form = btn.closest('form'); + var text = btn.data('confirm-danger-message'); + e.preventDefault(); + return new ConfirmDangerModal(form, text); + }); + $('input[type="search"]').each(function () { + var $this = $(this); + $this.attr('value', $this.val()); + }); + $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () { + var $this; + $this = $(this); + return $this.attr('value', $this.val()); + }); + $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) { + var $gutterIcon; + if (breakpoint === 'sm' || breakpoint === 'xs') { + $gutterIcon = $sidebarGutterToggle.find('i'); + if ($gutterIcon.hasClass('fa-angle-double-right')) { + return $sidebarGutterToggle.trigger('click'); } - }; - $window.off('resize.app').on('resize.app', function () { - return fitSidebarForSize(); - }); - gl.awardsHandler = new AwardsHandler(); - new Aside(); - - gl.utils.initTimeagoTimeout(); + } }); -}).call(window); + fitSidebarForSize = function () { + var oldBootstrapBreakpoint; + oldBootstrapBreakpoint = bootstrapBreakpoint; + bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { + return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + } + }; + $window.off('resize.app').on('resize.app', function () { + return fitSidebarForSize(); + }); + gl.awardsHandler = new AwardsHandler(); + new Aside(); + + gl.utils.initTimeagoTimeout(); +}); -- cgit v1.2.1 From c8f986fe115d19c3f6ec014e20c7399918914d10 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 13 Mar 2017 14:08:49 -0500 Subject: fix broken variable reference --- app/assets/javascripts/abuse_reports.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js index 2934565caec..346de4ad11e 100644 --- a/app/assets/javascripts/abuse_reports.js +++ b/app/assets/javascripts/abuse_reports.js @@ -15,7 +15,7 @@ class AbuseReports { if (reportMessage.length > MAX_MESSAGE_LENGTH) { $messageCellElement.data('original-message', reportMessage); $messageCellElement.data('message-truncated', 'true'); - $messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); + $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); } } -- cgit v1.2.1 From 985af1a6707af531a242051e46a54c16dc31b9bc Mon Sep 17 00:00:00 2001 From: mhasbini Date: Mon, 13 Mar 2017 22:09:43 +0200 Subject: take nonewline context into account in diff parser --- app/views/projects/diffs/_line.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 4 +- lib/gitlab/diff/line.rb | 6 +-- lib/gitlab/diff/parser.rb | 6 ++- spec/lib/gitlab/diff/parser_spec.rb | 48 +++++++++++++++++++++++ 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 62135d3ae32..c09c7b87e24 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -9,7 +9,7 @@ - case type - when 'match' = diff_match_line line.old_pos, line.new_pos, text: line.text - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.old_line.diff-line-num %td.new_line.diff-line-num %td.line_content.match= line.text diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index e7758c8bdfa..b7346f27ddb 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -12,7 +12,7 @@ - case left.type - when 'match' = diff_match_line left.old_pos, nil, text: left.text, view: :parallel - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.old_line.diff-line-num %td.line_content.match= left.text - else @@ -31,7 +31,7 @@ - case right.type - when 'match' = diff_match_line nil, right.new_pos, text: left.text, view: :parallel - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.new_line.diff-line-num %td.line_content.match= right.text - else diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 80a146b4a5a..114656958e3 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -38,11 +38,11 @@ module Gitlab end def added? - type == 'new' + type == 'new' || type == 'new-nonewline' end def removed? - type == 'old' + type == 'old' || type == 'old-nonewline' end def rich_text @@ -52,7 +52,7 @@ module Gitlab end def meta? - type == 'match' || type == 'nonewline' + type == 'match' end def as_json(opts = nil) diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 8f844224a7a..742f989c50b 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -11,6 +11,7 @@ module Gitlab line_old = 1 line_new = 1 type = nil + context = nil # By returning an Enumerator we make it possible to search for a single line (with #find) # without having to instantiate all the others that come after it. @@ -31,7 +32,8 @@ module Gitlab line_obj_index += 1 next elsif line[0] == '\\' - type = 'nonewline' + type = "#{context}-nonewline" + yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 else @@ -43,8 +45,10 @@ module Gitlab case line[0] when "+" line_new += 1 + context = :new when "-" line_old += 1 + context = :old when "\\" # rubocop:disable Lint/EmptyWhen # No increment else diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index b983d73f8be..e76128ecd87 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -91,6 +91,54 @@ eos end end + describe '\ No newline at end of file' do + it "parses nonewline in one file correctly" do + first_nonewline_diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + +ipsum + lorem + -ipsum + \\ No newline at end of file + END + lines = parser.parse(first_nonewline_diff.lines).to_a + + expect(lines[0].type).to eq('new') + expect(lines[0].text).to eq('+ipsum') + expect(lines[2].type).to eq('old') + expect(lines[3].type).to eq('old-nonewline') + expect(lines[1].old_pos).to eq(1) + expect(lines[1].new_pos).to eq(2) + end + + it "parses nonewline in two files correctly" do + both_nonewline_diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + -lorem + -ipsum + \\ No newline at end of file + +ipsum + +lorem + \\ No newline at end of file + END + lines = parser.parse(both_nonewline_diff.lines).to_a + + expect(lines[0].type).to eq('old') + expect(lines[1].type).to eq('old') + expect(lines[2].type).to eq('old-nonewline') + expect(lines[5].type).to eq('new-nonewline') + expect(lines[3].text).to eq('+ipsum') + expect(lines[3].old_pos).to eq(3) + expect(lines[3].new_pos).to eq(1) + expect(lines[4].text).to eq('+lorem') + expect(lines[4].old_pos).to eq(3) + expect(lines[4].new_pos).to eq(2) + end + end + context 'when lines is empty' do it { expect(parser.parse([])).to eq([]) } it { expect(parser.parse(nil)).to eq([]) } -- cgit v1.2.1 From 37ce638ccd476144fe9235a4d091be4135a3e00a Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Tue, 14 Mar 2017 07:24:30 +1100 Subject: Use dig --- app/views/projects/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 560e3439fc2..d129da943f8 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,6 +1,6 @@ - page_title 'New Project' - header_title "Projects", dashboard_projects_path -- visibility_level = params.try(:[], :project).try(:[], :visibility_level) || default_project_visibility +- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility .project-edit-container .project-edit-errors -- cgit v1.2.1 From 11dfad3e3a3209532184d828c1f70e6b774ade71 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 20 Jan 2017 15:13:27 -0800 Subject: Add performance/scalability concerns to CONTRIBUTING.md [ci skip] --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fd29fef4f0..9a6e3feec4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -399,6 +399,12 @@ There are a few rules to get your merge request accepted: 1. Contains functionality we think other users will benefit from too 1. Doesn't add configuration options or settings options since they complicate making and testing future changes +1. Changes do not adversely degrade performance. + - Avoid repeated polling of endpoints that require a significant amount of overhead + - Check for N+1 queries via the SQL log or [`QueryRecorder`](https://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) + - Avoid repeated access of filesystem +1. If you need polling to support real-time features, consider using this [described long + polling approach](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926). 1. Changes after submitting the merge request should be in separate commits (no squashing). If necessary, you will be asked to squash when the review is over, before merging. @@ -434,6 +440,7 @@ the feature you contribute through all of these steps. 1. Description explaining the relevancy (see following item) 1. Working and clean code that is commented where needed 1. Unit and integration tests that pass on the CI server +1. Performance/scalability implications have been considered, addressed, and tested 1. [Documented][doc-styleguide] in the /doc directory 1. Changelog entry added 1. Reviewed and any concerns are addressed -- cgit v1.2.1 From 29e0cb4b91b3800ef4974c54b8473e6e4fb28e16 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 13 Mar 2017 21:48:32 +0000 Subject: Organize our polyfills and standardize on core-js --- app/assets/javascripts/behaviors/quick_submit.js | 2 +- app/assets/javascripts/behaviors/requires_input.js | 2 +- app/assets/javascripts/commons/bootstrap.js | 8 ++++- app/assets/javascripts/commons/index.js | 1 + app/assets/javascripts/commons/polyfills.js | 10 ++++++ .../javascripts/commons/polyfills/custom_event.js | 9 +++++ .../javascripts/commons/polyfills/element.js | 20 +++++++++++ app/assets/javascripts/droplab/droplab_ajax.js | 3 ++ .../javascripts/droplab/droplab_ajax_filter.js | 3 ++ app/assets/javascripts/extensions/array.js | 28 ++++----------- app/assets/javascripts/extensions/custom_event.js | 12 ------- app/assets/javascripts/extensions/element.js | 20 ----------- app/assets/javascripts/extensions/jquery.js | 16 --------- app/assets/javascripts/extensions/object.js | 26 -------------- app/assets/javascripts/extensions/string.js | 2 -- .../filtered_search_dropdown_manager.js | 4 +++ .../filtered_search/filtered_search_manager.js | 5 +-- app/assets/javascripts/main.js | 8 ----- .../javascripts/monitoring/prometheus_graph.js | 6 ++-- changelogs/unreleased/use-corejs-polyfills.yml | 4 +++ package.json | 4 +-- spec/javascripts/awards_handler_spec.js | 3 -- .../blob/create_branch_dropdown_spec.js | 2 -- .../blob/target_branch_dropdown_spec.js | 2 -- spec/javascripts/boards/board_new_issue_spec.js | 1 - spec/javascripts/boards/boards_store_spec.js | 1 - spec/javascripts/bootstrap_jquery_spec.js | 42 ++++++++++++++++++++++ spec/javascripts/extensions/array_spec.js | 23 ------------ spec/javascripts/extensions/element_spec.js | 38 -------------------- spec/javascripts/extensions/jquery_spec.js | 42 ---------------------- spec/javascripts/extensions/object_spec.js | 25 ------------- .../filtered_search_manager_spec.js | 5 ++- spec/javascripts/gl_emoji_spec.js | 3 -- .../monitoring/prometheus_graph_spec.js | 3 -- spec/javascripts/polyfills/element_spec.js | 36 +++++++++++++++++++ spec/javascripts/right_sidebar_spec.js | 4 +-- spec/javascripts/shortcuts_issuable_spec.js | 8 ++--- yarn.lock | 12 ++----- 38 files changed, 165 insertions(+), 278 deletions(-) create mode 100644 app/assets/javascripts/commons/polyfills.js create mode 100644 app/assets/javascripts/commons/polyfills/custom_event.js create mode 100644 app/assets/javascripts/commons/polyfills/element.js delete mode 100644 app/assets/javascripts/extensions/custom_event.js delete mode 100644 app/assets/javascripts/extensions/element.js delete mode 100644 app/assets/javascripts/extensions/jquery.js delete mode 100644 app/assets/javascripts/extensions/object.js delete mode 100644 app/assets/javascripts/extensions/string.js create mode 100644 changelogs/unreleased/use-corejs-polyfills.yml create mode 100644 spec/javascripts/bootstrap_jquery_spec.js delete mode 100644 spec/javascripts/extensions/element_spec.js delete mode 100644 spec/javascripts/extensions/jquery_spec.js delete mode 100644 spec/javascripts/extensions/object_spec.js create mode 100644 spec/javascripts/polyfills/element_spec.js diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index a7e68ae5cb9..626f3503c91 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -6,7 +6,7 @@ // "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form // is submitted. // -require('../extensions/jquery'); +import '../commons/bootstrap'; // // ### Example Markup diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 6b21695d082..eb7143f5b1a 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -4,7 +4,7 @@ // When called on a form with input fields with the `required` attribute, the // form's submit button will be disabled until all required fields have values. // -require('../extensions/jquery'); +import '../commons/bootstrap'; // // ### Example Markup diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index db0cbfd87c3..36bfe457be9 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -1,4 +1,4 @@ -import 'jquery'; +import $ from 'jquery'; // bootstrap jQuery plugins import 'bootstrap-sass/assets/javascripts/bootstrap/affix'; @@ -8,3 +8,9 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; + +// custom jQuery functions +$.fn.extend({ + disable() { return $(this).attr('disabled', 'disabled').addClass('disabled'); }, + enable() { return $(this).removeAttr('disabled').removeClass('disabled'); }, +}); diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 72ede1d621a..7063f59d446 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -1,2 +1,3 @@ +import './polyfills'; import './jquery'; import './bootstrap'; diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js new file mode 100644 index 00000000000..fbd0db64ca7 --- /dev/null +++ b/app/assets/javascripts/commons/polyfills.js @@ -0,0 +1,10 @@ +// ECMAScript polyfills +import 'core-js/fn/array/find'; +import 'core-js/fn/object/assign'; +import 'core-js/fn/promise'; +import 'core-js/fn/string/code-point-at'; +import 'core-js/fn/string/from-code-point'; + +// Browser polyfills +import './polyfills/custom_event'; +import './polyfills/element'; diff --git a/app/assets/javascripts/commons/polyfills/custom_event.js b/app/assets/javascripts/commons/polyfills/custom_event.js new file mode 100644 index 00000000000..aea61b82d03 --- /dev/null +++ b/app/assets/javascripts/commons/polyfills/custom_event.js @@ -0,0 +1,9 @@ +if (typeof window.CustomEvent !== 'function') { + window.CustomEvent = function CustomEvent(event, params) { + const evt = document.createEvent('CustomEvent'); + const evtParams = params || { bubbles: false, cancelable: false, detail: undefined }; + evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail); + return evt; + }; + window.CustomEvent.prototype = Event; +} diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js new file mode 100644 index 00000000000..9a1f73bf2ac --- /dev/null +++ b/app/assets/javascripts/commons/polyfills/element.js @@ -0,0 +1,20 @@ +Element.prototype.closest = Element.prototype.closest || + function closest(selector, selectedElement = this) { + if (!selectedElement) return null; + return selectedElement.matches(selector) ? + selectedElement : + Element.prototype.closest(selector, selectedElement.parentElement); + }; + +Element.prototype.matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector || + function matches(selector) { + const elms = (this.document || this.ownerDocument).querySelectorAll(selector); + let i = elms.length - 1; + while (i >= 0 && elms.item(i) !== this) { i -= 1; } + return i > -1; + }; diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index f61be741b4a..020f8b4ac65 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -74,6 +74,9 @@ require('../window')(function(w){ this._loadUrlData(config.endpoint) .then(function(d) { self._loadData(d, config, self); + }, function(xhrError) { + // TODO: properly handle errors due to XHR cancellation + return; }).catch(function(e) { throw new droplabAjaxException(e.message || e); }); diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index b63d73066cb..05eba7aef56 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -82,6 +82,9 @@ require('../window')(function(w){ this._loadUrlData(url) .then(function(data) { self._loadData(data, config, self); + }, function(xhrError) { + // TODO: properly handle errors due to XHR cancellation + return; }); } }, diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js index f8256a8d26d..027222f804d 100644 --- a/app/assets/javascripts/extensions/array.js +++ b/app/assets/javascripts/extensions/array.js @@ -1,27 +1,11 @@ -/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */ +// TODO: remove this -'use strict'; - -Array.prototype.first = function() { +// eslint-disable-next-line no-extend-native +Array.prototype.first = function first() { return this[0]; }; -Array.prototype.last = function() { - return this[this.length-1]; -}; - -Array.prototype.find = Array.prototype.find || function(predicate, ...args) { - if (!this) throw new TypeError('Array.prototype.find called on null or undefined'); - if (typeof predicate !== 'function') throw new TypeError('predicate must be a function'); - - const list = Object(this); - const thisArg = args[1]; - let value = {}; - - for (let i = 0; i < list.length; i += 1) { - value = list[i]; - if (predicate.call(thisArg, value, i, list)) return value; - } - - return undefined; +// eslint-disable-next-line no-extend-native +Array.prototype.last = function last() { + return this[this.length - 1]; }; diff --git a/app/assets/javascripts/extensions/custom_event.js b/app/assets/javascripts/extensions/custom_event.js deleted file mode 100644 index abedae4c1c7..00000000000 --- a/app/assets/javascripts/extensions/custom_event.js +++ /dev/null @@ -1,12 +0,0 @@ -/* global CustomEvent */ -/* eslint-disable no-global-assign */ - -// Custom event support for IE -CustomEvent = function CustomEvent(event, parameters) { - const params = parameters || { bubbles: false, cancelable: false, detail: undefined }; - const evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; -}; - -CustomEvent.prototype = window.Event.prototype; diff --git a/app/assets/javascripts/extensions/element.js b/app/assets/javascripts/extensions/element.js deleted file mode 100644 index 90ab79305a7..00000000000 --- a/app/assets/javascripts/extensions/element.js +++ /dev/null @@ -1,20 +0,0 @@ -/* global Element */ -/* eslint-disable consistent-return, max-len, no-empty, func-names */ - -Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { - if (!selectedElement) return; - return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); -}; - -Element.prototype.matches = Element.prototype.matches || - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector || - function (s) { - const matches = (this.document || this.ownerDocument).querySelectorAll(s); - let i = matches.length - 1; - while (i >= 0 && matches.item(i) !== this) { i -= 1; } - return i > -1; - }; diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js deleted file mode 100644 index 1a489b859e8..00000000000 --- a/app/assets/javascripts/extensions/jquery.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */ -// Disable an element and add the 'disabled' Bootstrap class -(function() { - $.fn.extend({ - disable: function() { - return $(this).attr('disabled', 'disabled').addClass('disabled'); - } - }); - - // Enable an element and remove the 'disabled' Bootstrap class - $.fn.extend({ - enable: function() { - return $(this).removeAttr('disabled').removeClass('disabled'); - } - }); -}).call(window); diff --git a/app/assets/javascripts/extensions/object.js b/app/assets/javascripts/extensions/object.js deleted file mode 100644 index 70a2d765abd..00000000000 --- a/app/assets/javascripts/extensions/object.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable no-restricted-syntax */ - -// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill -if (typeof Object.assign !== 'function') { - Object.assign = function assign(target, ...args) { - if (target == null) { // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - const to = Object(target); - - for (let index = 0; index < args.length; index += 1) { - const nextSource = args[index]; - - if (nextSource != null) { // Skip over if undefined or null - for (const nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - return to; - }; -} diff --git a/app/assets/javascripts/extensions/string.js b/app/assets/javascripts/extensions/string.js deleted file mode 100644 index ae9662444b0..00000000000 --- a/app/assets/javascripts/extensions/string.js +++ /dev/null @@ -1,2 +0,0 @@ -import 'string.prototype.codepointat'; -import 'string.fromcodepoint'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index e1a97070439..d37c812c1f7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -162,6 +162,10 @@ } resetDropdowns() { + if (!this.currentDropdown) { + return; + } + // Force current dropdown to hide this.mapping[this.currentDropdown].reference.hideDropdown(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 638fe744668..835e87a28d7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -38,7 +38,8 @@ this.editTokenWrapper = this.editToken.bind(this); this.tokenChange = this.tokenChange.bind(this); - this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); + this.filteredSearchInputForm = this.filteredSearchInput.form; + this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper); @@ -56,7 +57,7 @@ } unbindEvents() { - this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); + this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 604ed91627a..cf3e4ee77b6 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -16,17 +16,9 @@ import Sortable from 'vendor/Sortable'; import 'mousetrap'; import 'mousetrap/plugins/pause/mousetrap-pause'; import 'vendor/fuzzaldrin-plus'; -import promisePolyfill from 'es6-promise'; // extensions -import './extensions/string'; import './extensions/array'; -import './extensions/custom_event'; -import './extensions/element'; -import './extensions/jquery'; -import './extensions/object'; - -promisePolyfill.polyfill(); // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index 9384fe3f276..71eb746edac 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -1,9 +1,11 @@ -/* eslint-disable no-new*/ +/* eslint-disable no-new */ +/* global Flash */ + import d3 from 'd3'; import _ from 'underscore'; import statusCodes from '~/lib/utils/http_status'; import '~/lib/utils/common_utils'; -import Flash from '~/flash'; +import '~/flash'; const prometheusGraphsContainer = '.prometheus-graph'; const metricsEndpoint = 'metrics.json'; diff --git a/changelogs/unreleased/use-corejs-polyfills.yml b/changelogs/unreleased/use-corejs-polyfills.yml new file mode 100644 index 00000000000..381f80c5c0d --- /dev/null +++ b/changelogs/unreleased/use-corejs-polyfills.yml @@ -0,0 +1,4 @@ +--- +title: Standardize on core-js for es2015 polyfills +merge_request: 9749 +author: diff --git a/package.json b/package.json index efa3a63e693..9652dd8f972 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "babel-preset-stage-2": "^6.22.0", "bootstrap-sass": "^3.3.6", "compression-webpack-plugin": "^0.3.2", + "core-js": "^2.4.1", "d3": "^3.5.11", "document-register-element": "^1.3.0", "dropzone": "^4.2.0", "emoji-unicode-version": "^0.2.1", - "es6-promise": "^4.0.5", "jquery": "^2.2.1", "jquery-ujs": "^1.2.1", "js-cookie": "^2.1.3", @@ -31,8 +31,6 @@ "raw-loader": "^0.5.1", "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.3", - "string.fromcodepoint": "^0.2.1", - "string.prototype.codepointat": "^0.2.0", "timeago.js": "^2.0.5", "underscore": "^1.8.3", "vue": "^2.1.10", diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 9a2978006aa..0a6e042b700 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,11 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ -import promisePolyfill from 'es6-promise'; import Cookies from 'js-cookie'; import AwardsHandler from '~/awards_handler'; -promisePolyfill.polyfill(); - (function() { var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js index dafb43761e0..c1179e572ae 100644 --- a/spec/javascripts/blob/create_branch_dropdown_spec.js +++ b/spec/javascripts/blob/create_branch_dropdown_spec.js @@ -1,5 +1,3 @@ -require('jquery'); -require('~/extensions/jquery.js'); require('~/gl_dropdown'); require('~/lib/utils/type_utility'); require('~/blob/create_branch_dropdown'); diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js index 6f3eb4cc7eb..4fb79663c51 100644 --- a/spec/javascripts/blob/target_branch_dropdown_spec.js +++ b/spec/javascripts/blob/target_branch_dropdown_spec.js @@ -1,5 +1,3 @@ -require('jquery'); -require('~/extensions/jquery.js'); require('~/gl_dropdown'); require('~/lib/utils/type_utility'); require('~/blob/create_branch_dropdown'); diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 22c9f12951b..4999933c0c1 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -8,7 +8,6 @@ import boardNewIssue from '~/boards/components/board_new_issue'; require('~/boards/models/list'); require('./mock_data'); -require('es6-promise').polyfill(); describe('Issue boards new issue form', () => { let vm; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 49a2ca4a78f..1d1069600fc 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -15,7 +15,6 @@ require('~/boards/models/user'); require('~/boards/services/board_service'); require('~/boards/stores/boards_store'); require('./mock_data'); -require('es6-promise').polyfill(); describe('Store', () => { beforeEach(() => { diff --git a/spec/javascripts/bootstrap_jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js new file mode 100644 index 00000000000..48994b7c523 --- /dev/null +++ b/spec/javascripts/bootstrap_jquery_spec.js @@ -0,0 +1,42 @@ +/* eslint-disable space-before-function-paren, no-var */ + +import '~/commons/bootstrap'; + +(function() { + describe('Bootstrap jQuery extensions', function() { + describe('disable', function() { + beforeEach(function() { + return setFixtures(''); + }); + it('adds the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveAttr('disabled', 'disabled'); + }); + return it('adds the disabled class', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveClass('disabled'); + }); + }); + return describe('enable', function() { + beforeEach(function() { + return setFixtures(''); + }); + it('removes the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveAttr('disabled'); + }); + return it('removes the disabled class', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveClass('disabled'); + }); + }); + }); +}).call(window); diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js index 60f6b9b78e3..4b871fe967d 100644 --- a/spec/javascripts/extensions/array_spec.js +++ b/spec/javascripts/extensions/array_spec.js @@ -18,28 +18,5 @@ require('~/extensions/array'); return expect(arr.last()).toBe(5); }); }); - - describe('find', function () { - beforeEach(() => { - this.arr = [0, 1, 2, 3, 4, 5]; - }); - - it('returns the item that first passes the predicate function', () => { - expect(this.arr.find(item => item === 2)).toBe(2); - }); - - it('returns undefined if no items pass the predicate function', () => { - expect(this.arr.find(item => item === 6)).not.toBeDefined(); - }); - - it('error when called on undefined or null', () => { - expect(Array.prototype.find.bind(undefined, item => item === 1)).toThrow(); - expect(Array.prototype.find.bind(null, item => item === 1)).toThrow(); - }); - - it('error when predicate is not a function', () => { - expect(Array.prototype.find.bind(this.arr, 1)).toThrow(); - }); - }); }); }).call(window); diff --git a/spec/javascripts/extensions/element_spec.js b/spec/javascripts/extensions/element_spec.js deleted file mode 100644 index 2d8a128ed33..00000000000 --- a/spec/javascripts/extensions/element_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -require('~/extensions/element'); - -(() => { - describe('Element extensions', function () { - beforeEach(() => { - this.element = document.createElement('ul'); - }); - - describe('matches', () => { - it('returns true if element matches the selector', () => { - expect(this.element.matches('ul')).toBeTruthy(); - }); - - it("returns false if element doesn't match the selector", () => { - expect(this.element.matches('.not-an-element')).toBeFalsy(); - }); - }); - - describe('closest', () => { - beforeEach(() => { - this.childElement = document.createElement('li'); - this.element.appendChild(this.childElement); - }); - - it('returns the closest parent that matches the selector', () => { - expect(this.childElement.closest('ul').toString()).toBe(this.element.toString()); - }); - - it('returns itself if it matches the selector', () => { - expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString()); - }); - - it('returns undefined if nothing matches the selector', () => { - expect(this.childElement.closest('.no-an-element')).toBeFalsy(); - }); - }); - }); -})(); diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js deleted file mode 100644 index 096d3272eac..00000000000 --- a/spec/javascripts/extensions/jquery_spec.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable space-before-function-paren, no-var */ - -require('~/extensions/jquery'); - -(function() { - describe('jQuery extensions', function() { - describe('disable', function() { - beforeEach(function() { - return setFixtures(''); - }); - it('adds the disabled attribute', function() { - var $input; - $input = $('input').first(); - $input.disable(); - return expect($input).toHaveAttr('disabled', 'disabled'); - }); - return it('adds the disabled class', function() { - var $input; - $input = $('input').first(); - $input.disable(); - return expect($input).toHaveClass('disabled'); - }); - }); - return describe('enable', function() { - beforeEach(function() { - return setFixtures(''); - }); - it('removes the disabled attribute', function() { - var $input; - $input = $('input').first(); - $input.enable(); - return expect($input).not.toHaveAttr('disabled'); - }); - return it('removes the disabled class', function() { - var $input; - $input = $('input').first(); - $input.enable(); - return expect($input).not.toHaveClass('disabled'); - }); - }); - }); -}).call(window); diff --git a/spec/javascripts/extensions/object_spec.js b/spec/javascripts/extensions/object_spec.js deleted file mode 100644 index 2467ed78459..00000000000 --- a/spec/javascripts/extensions/object_spec.js +++ /dev/null @@ -1,25 +0,0 @@ -require('~/extensions/object'); - -describe('Object extensions', () => { - describe('assign', () => { - it('merges source object into target object', () => { - const targetObj = {}; - const sourceObj = { - foo: 'bar', - }; - Object.assign(targetObj, sourceObj); - expect(targetObj.foo).toBe('bar'); - }); - - it('merges object with the same properties', () => { - const targetObj = { - foo: 'bar', - }; - const sourceObj = { - foo: 'baz', - }; - Object.assign(targetObj, sourceObj); - expect(targetObj.foo).toBe('baz'); - }); - }); -}); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 81c1d81d181..ae9c263d1d7 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -41,7 +41,6 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper `); - spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); @@ -54,6 +53,10 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper manager = new gl.FilteredSearchManager(); }); + afterEach(() => { + manager.cleanup(); + }); + describe('search', () => { const defaultParams = '?scope=all&utf8=✓&state=opened'; diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index 7ab0b37f2ec..9b44b25980c 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -1,6 +1,3 @@ -import '~/extensions/string'; -import '~/extensions/array'; - import { glEmojiTag } from '~/behaviors/gl_emoji'; import { isEmojiUnicodeSupported, diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index 823b4bab7fc..a3c1c5e1b7c 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -1,11 +1,8 @@ import 'jquery'; -import es6Promise from 'es6-promise'; import '~/lib/utils/common_utils'; import PrometheusGraph from '~/monitoring/prometheus_graph'; import { prometheusMockData } from './prometheus_mock_data'; -es6Promise.polyfill(); - describe('PrometheusGraph', () => { const fixtureName = 'static/environments/metrics.html.raw'; const prometheusGraphContainer = '.prometheus-graph'; diff --git a/spec/javascripts/polyfills/element_spec.js b/spec/javascripts/polyfills/element_spec.js new file mode 100644 index 00000000000..ecaaf1907ea --- /dev/null +++ b/spec/javascripts/polyfills/element_spec.js @@ -0,0 +1,36 @@ +import '~/commons/polyfills/element'; + +describe('Element polyfills', function () { + beforeEach(() => { + this.element = document.createElement('ul'); + }); + + describe('matches', () => { + it('returns true if element matches the selector', () => { + expect(this.element.matches('ul')).toBeTruthy(); + }); + + it("returns false if element doesn't match the selector", () => { + expect(this.element.matches('.not-an-element')).toBeFalsy(); + }); + }); + + describe('closest', () => { + beforeEach(() => { + this.childElement = document.createElement('li'); + this.element.appendChild(this.childElement); + }); + + it('returns the closest parent that matches the selector', () => { + expect(this.childElement.closest('ul').toString()).toBe(this.element.toString()); + }); + + it('returns itself if it matches the selector', () => { + expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString()); + }); + + it('returns undefined if nothing matches the selector', () => { + expect(this.childElement.closest('.no-an-element')).toBeFalsy(); + }); + }); +}); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 4ac7e911740..285b7940174 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,8 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */ /* global Sidebar */ -require('~/right_sidebar'); -require('~/extensions/jquery.js'); +import '~/commons/bootstrap'; +import '~/right_sidebar'; (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index ffff643e371..9e19dabd0e3 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -31,13 +31,9 @@ require('~/shortcuts_issuable'); this.shortcut.replyWithSelectedText(); expect($(this.selector).val()).toBe(''); }); - it('triggers `input`', function() { - var focused = false; - $(this.selector).on('focus', function() { - focused = true; - }); + it('triggers `focus`', function() { this.shortcut.replyWithSelectedText(); - expect(focused).toBe(true); + expect(document.activeElement).toBe(document.querySelector(this.selector)); }); }); describe('with any selection', function() { diff --git a/yarn.lock b/yarn.lock index 55b8f1566ee..391b1c7eccf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1213,7 +1213,7 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" -core-js@^2.2.0, core-js@^2.4.0: +core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" @@ -1553,7 +1553,7 @@ es6-map@^0.1.3: es6-symbol "~3.1.0" event-emitter "~0.3.4" -es6-promise@^4.0.5, es6-promise@~4.0.3: +es6-promise@~4.0.3: version "4.0.5" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42" @@ -4123,14 +4123,6 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" -string.fromcodepoint@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz#8d978333c0bc92538f50f383e4888f3e5619d653" - -string.prototype.codepointat@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" - string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -- cgit v1.2.1 From 0f9e3e2b58f172590d7e8664ebc16b6d143b588c Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Tue, 14 Mar 2017 09:13:03 +1100 Subject: Add quick submit for snippet forms --- app/views/shared/snippets/_form.html.haml | 2 +- changelogs/unreleased/add_quick_submit_for_snippets_form.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/add_quick_submit_for_snippets_form.yml diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index e7f7db73223..0296597b294 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('snippet') .snippet-form-holder - = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| + = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit" } do |f| = form_errors(@snippet) .form-group diff --git a/changelogs/unreleased/add_quick_submit_for_snippets_form.yml b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml new file mode 100644 index 00000000000..088f1335796 --- /dev/null +++ b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml @@ -0,0 +1,4 @@ +--- +title: Add quick submit for snippet forms +merge_request: 9911 +author: blackst0ne -- cgit v1.2.1 From e10810ded8457e45921934359bc1e4fcb35fa785 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Mar 2017 17:46:21 -0500 Subject: Fix missing blob line permalink updater on blob:show See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9461#note_25288831 --- app/assets/javascripts/dispatcher.js | 44 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f0967d4f470..6d8174e199e 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -67,6 +67,25 @@ const UserCallout = require('./user_callout'); } path = page.split(':'); shortcut_handler = null; + + function initBlob() { + new LineHighlighter(); + + new BlobLinePermalinkUpdater( + document.querySelector('#blob-content-holder'), + '.diff-line-num[data-line-number]', + document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), + ); + + shortcut_handler = new ShortcutsNavigation(); + fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); + fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + new ShortcutsBlob({ + skipResetBindings: true, + fileBlobPermalinkUrl, + }); + } + switch (page) { case 'sessions:new': new UsernameValidator(); @@ -259,34 +278,13 @@ const UserCallout = require('./user_callout'); break; case 'projects:blob:show': gl.TargetBranchDropDown.bootstrap(); - new LineHighlighter(); - shortcut_handler = new ShortcutsNavigation(); - fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - new ShortcutsBlob({ - skipResetBindings: true, - fileBlobPermalinkUrl, - }); + initBlob(); break; case 'projects:blob:edit': gl.TargetBranchDropDown.bootstrap(); break; case 'projects:blame:show': - new LineHighlighter(); - - new BlobLinePermalinkUpdater( - document.querySelector('#blob-content-holder'), - '.diff-line-num[data-line-number]', - document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), - ); - - shortcut_handler = new ShortcutsNavigation(); - fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - new ShortcutsBlob({ - skipResetBindings: true, - fileBlobPermalinkUrl, - }); + initBlob(); break; case 'groups:labels:new': case 'groups:labels:edit': -- cgit v1.2.1 From 5111525a4a1dc6a4e5498875b9198c265d6ea288 Mon Sep 17 00:00:00 2001 From: Lee Matos Date: Mon, 13 Mar 2017 23:18:45 +0000 Subject: Update markdown.md example with asterisks and underscores for clarity --- doc/user/markdown.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/markdown.md b/doc/user/markdown.md index db06224bac2..97de428d11d 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -431,7 +431,7 @@ Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. -Combined emphasis with **asterisks and _underscores_**. +Combined emphasis with **_asterisks and underscores_**. Strikethrough uses two tildes. ~~Scratch this.~~ ``` -- cgit v1.2.1 From db59e735ae9c30bfa1e9d0800b6edfaaf6981f2a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 28 Feb 2017 21:59:55 -0500 Subject: Toggle project name if too long --- app/assets/javascripts/dispatcher.js | 5 +++ app/assets/javascripts/group_name.js | 40 ++++++++++++++++++++ app/assets/stylesheets/framework/header.scss | 22 +++++++++++ app/helpers/groups_helper.rb | 11 +++--- app/views/layouts/header/_default.html.haml | 2 +- ...187-project-name-cut-off-with-nested-groups.yml | 4 ++ spec/features/groups/group_name_toggle.rb | 44 ++++++++++++++++++++++ 7 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/group_name.js create mode 100644 changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml create mode 100644 spec/features/groups/group_name_toggle.rb diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 7b9b9123c31..fcf3cb05e63 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -37,6 +37,7 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; +import GroupName from './group_name'; import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; @@ -342,6 +343,9 @@ const UserCallout = require('./user_callout'); shortcut_handler = new ShortcutsDashboardNavigation(); new UserCallout(); break; + case 'groups': + new GroupName(); + break; case 'profiles': new NotificationsForm(); new NotificationsDropdown(); @@ -349,6 +353,7 @@ const UserCallout = require('./user_callout'); case 'projects': new Project(); new ProjectAvatar(); + new GroupName(); switch (path[1]) { case 'compare': new CompareAutocomplete(); diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js new file mode 100644 index 00000000000..6a028f299b1 --- /dev/null +++ b/app/assets/javascripts/group_name.js @@ -0,0 +1,40 @@ +const GROUP_LIMIT = 2; + +export default class GroupName { + constructor() { + this.titleContainer = document.querySelector('.title'); + this.groups = document.querySelectorAll('.group-path'); + this.groupTitle = document.querySelector('.group-title'); + this.toggle = null; + this.isHidden = false; + this.init(); + } + + init() { + if (this.groups.length > GROUP_LIMIT) { + this.groups[this.groups.length - 1].classList.remove('hidable'); + this.addToggle(); + } + this.render(); + } + + addToggle() { + const header = document.querySelector('.header-content'); + this.toggle = document.createElement('button'); + this.toggle.className = 'text-expander group-name-toggle'; + this.toggle.setAttribute('aria-label', 'Toggle full path'); + this.toggle.innerHTML = '...'; + this.toggle.addEventListener('click', this.toggleGroups.bind(this)); + header.insertBefore(this.toggle, this.titleContainer); + this.toggleGroups(); + } + + toggleGroups() { + this.isHidden = !this.isHidden; + this.groupTitle.classList.toggle('is-hidden'); + } + + render() { + this.titleContainer.classList.remove('initializing'); + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 5d1aba4e529..6660a022260 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -164,11 +164,25 @@ header { } } + .group-name-toggle { + margin: 0 5px; + vertical-align: sub; + } + + .group-title { + &.is-hidden { + .hidable:not(:last-of-type) { + display: none; + } + } + } + .title { position: relative; padding-right: 20px; margin: 0; font-size: 18px; + max-width: 385px; display: inline-block; line-height: $header-height; font-weight: normal; @@ -178,6 +192,14 @@ header { vertical-align: top; white-space: nowrap; + &.initializing { + display: none; + } + + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + max-width: 300px; + } + @media (max-width: $screen-xs-max) { max-width: 190px; } diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 926c9703628..a6014088e92 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -12,17 +12,18 @@ module GroupsHelper end def group_title(group, name = nil, url = nil) + @has_group_title = true full_title = '' group.ancestors.each do |parent| - full_title += link_to(simple_sanitize(parent.name), group_path(parent)) - full_title += ' / '.html_safe + full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') + full_title += ' / '.html_safe end - full_title += link_to(simple_sanitize(group.name), group_path(group)) - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name + full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') + full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name - content_tag :span do + content_tag :span, class: 'group-title' do full_title.html_safe end end diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 6f4f2dbea3a..5fde5c2613e 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -67,7 +67,7 @@ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo - %h1.title= title + %h1.title{ class: ('initializing' if @has_group_title) }= title = yield :header_content diff --git a/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml new file mode 100644 index 00000000000..feca38ff083 --- /dev/null +++ b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml @@ -0,0 +1,4 @@ +--- +title: Use toggle button to expand / collapse mulit-nested groups +merge_request: 9501 +author: diff --git a/spec/features/groups/group_name_toggle.rb b/spec/features/groups/group_name_toggle.rb new file mode 100644 index 00000000000..ada4ac66e04 --- /dev/null +++ b/spec/features/groups/group_name_toggle.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +feature 'Group name toggle', js: true do + let(:group) { create(:group) } + let(:nested_group_1) { create(:group, parent: group) } + let(:nested_group_2) { create(:group, parent: nested_group_1) } + let(:nested_group_3) { create(:group, parent: nested_group_2) } + + before do + login_as :user + end + + it 'is not present for less than 3 groups' do + visit group_path(group) + expect(page).not_to have_css('.group-name-toggle') + + visit group_path(nested_group_1) + expect(page).not_to have_css('.group-name-toggle') + end + + it 'is present for nested group of 3 or more in the namespace' do + visit group_path(nested_group_2) + expect(page).to have_css('.group-name-toggle') + + visit group_path(nested_group_3) + expect(page).to have_css('.group-name-toggle') + end + + context 'for group with at least 3 groups' do + before do + visit group_path(nested_group_2) + end + + it 'should show the full group namespace when toggled' do + expect(page).not_to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: false) + + click_button '...' + + expect(page).to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: true) + end + end +end -- cgit v1.2.1 From d48dda3c2ddbd260a828a1145f22834846353bc5 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 14 Mar 2017 00:52:23 +0000 Subject: Fix 'ExecJS disabled' error on issues index Occurred in production when an issue had an associated MR --- app/views/shared/_issuable_meta_data.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 66310da5cd6..1d4fd71522d 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -6,7 +6,7 @@ - if issuable_mr > 0 %li - = image_tag('icon-merge-request-unmerged', class: 'icon-merge-request-unmerged') + = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged') = issuable_mr - if upvotes > 0 -- cgit v1.2.1 From cbf1b656a464e0e544f7e559efed6851616e377f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 14 Mar 2017 00:58:26 +0000 Subject: Use a button and a post request instead of UJS links - part 1 - Environments --- .../environments/components/environment.js | 76 ++++++++++++---------- .../environments/components/environment_actions.js | 56 ++++++++++++---- .../components/environment_external_url.js | 14 ++-- .../environments/components/environment_item.js | 34 ++++++---- .../components/environment_rollback.js | 49 ++++++++++++-- .../environments/components/environment_stop.js | 50 +++++++++++--- .../components/environment_terminal_button.js | 9 +-- .../environments/components/environments_table.js | 16 +++-- .../environments/environments_bundle.js | 2 +- app/assets/javascripts/environments/event_hub.js | 3 + .../folder/environments_folder_bundle.js | 2 +- .../folder/environments_folder_view.js | 19 +++--- .../environments/services/environments_service.js | 15 +++-- .../environments/stores/environments_store.js | 7 +- .../vue_shared/vue_resource_interceptor.js | 4 -- app/assets/stylesheets/pages/environments.scss | 8 +++ .../projects/environments/environments_spec.rb | 10 +-- .../environments/environment_actions_spec.js | 35 ++++++---- .../environments/environment_external_url_spec.js | 11 ++-- .../environments/environment_item_spec.js | 18 ++--- .../environments/environment_rollback_spec.js | 42 +++++++----- spec/javascripts/environments/environment_spec.js | 8 +-- .../environments/environment_stop_spec.js | 30 +++++---- .../environments/environment_table_spec.js | 8 ++- .../environment_terminal_button_spec.js | 24 +++++++ .../environments/environments_store_spec.js | 4 +- .../folder/environments_folder_view_spec.js | 8 +-- spec/javascripts/environments/mock_data.js | 12 +--- 28 files changed, 375 insertions(+), 199 deletions(-) create mode 100644 app/assets/javascripts/environments/event_hub.js create mode 100644 spec/javascripts/environments/environment_terminal_button_spec.js diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js index 2cb48dde628..0923ce6b550 100644 --- a/app/assets/javascripts/environments/components/environment.js +++ b/app/assets/javascripts/environments/components/environment.js @@ -1,16 +1,17 @@ /* eslint-disable no-param-reassign, no-new */ /* global Flash */ +import EnvironmentsService from '../services/environments_service'; +import EnvironmentTable from './environments_table'; +import EnvironmentsStore from '../stores/environments_store'; +import eventHub from '../event_hub'; const Vue = window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -const EnvironmentsService = require('../services/environments_service'); -const EnvironmentTable = require('./environments_table'); -const EnvironmentsStore = require('../stores/environments_store'); require('../../vue_shared/components/table_pagination'); require('../../lib/utils/common_utils'); require('../../vue_shared/vue_resource_interceptor'); -module.exports = Vue.component('environment-component', { +export default Vue.component('environment-component', { components: { 'environment-table': EnvironmentTable, @@ -66,33 +67,15 @@ module.exports = Vue.component('environment-component', { * Toggles loading property. */ created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; - - const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; - - const service = new EnvironmentsService(endpoint); - - this.isLoading = true; - - return service.get() - .then(resp => ({ - headers: resp.headers, - body: resp.json(), - })) - .then((response) => { - this.store.storeAvailableCount(response.body.available_count); - this.store.storeStoppedCount(response.body.stopped_count); - this.store.storeEnvironments(response.body.environments); - this.store.setPagination(response.headers); - }) - .then(() => { - this.isLoading = false; - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the environments.', 'alert'); - }); + this.service = new EnvironmentsService(this.endpoint); + + this.fetchEnvironments(); + + eventHub.$on('refreshEnvironments', this.fetchEnvironments); + }, + + beforeDestroyed() { + eventHub.$off('refreshEnvironments'); }, methods: { @@ -112,6 +95,32 @@ module.exports = Vue.component('environment-component', { gl.utils.visitUrl(param); return param; }, + + fetchEnvironments() { + const scope = gl.utils.getParameterByName('scope') || this.visibility; + const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; + + this.isLoading = true; + + return this.service.get(scope, pageNumber) + .then(resp => ({ + headers: resp.headers, + body: resp.json(), + })) + .then((response) => { + this.store.storeAvailableCount(response.body.available_count); + this.store.storeStoppedCount(response.body.stopped_count); + this.store.storeEnvironments(response.body.environments); + this.store.setPagination(response.headers); + }) + .then(() => { + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the environments.'); + }); + }, }, template: ` @@ -144,7 +153,7 @@ module.exports = Vue.component('environment-component', {
      - +
      + :can-read-environment="canReadEnvironmentParsed" + :service="service"/>
      [], }, + + service: { + type: Object, + required: true, + }, }, data() { - return { playIconSvg }; + return { + playIconSvg, + isLoading: false, + }; + }, + + methods: { + onClickAction(endpoint) { + this.isLoading = true; + + this.service.postAction(endpoint) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshEnvironments'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, }, template: `
    `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js index 2599bba3c59..a554998f52c 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js +++ b/app/assets/javascripts/environments/components/environment_external_url.js @@ -1,9 +1,7 @@ /** * Renders the external url link in environments table. */ -const Vue = require('vue'); - -module.exports = Vue.component('external-url-component', { +export default { props: { externalUrl: { type: String, @@ -12,8 +10,12 @@ module.exports = Vue.component('external-url-component', { }, template: ` - - + + `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js index 7f4e070b229..93919d41c60 100644 --- a/app/assets/javascripts/environments/components/environment_item.js +++ b/app/assets/javascripts/environments/components/environment_item.js @@ -1,13 +1,11 @@ -const Vue = require('vue'); -const Timeago = require('timeago.js'); - -require('../../lib/utils/text_utility'); -require('../../vue_shared/components/commit'); -const ActionsComponent = require('./environment_actions'); -const ExternalUrlComponent = require('./environment_external_url'); -const StopComponent = require('./environment_stop'); -const RollbackComponent = require('./environment_rollback'); -const TerminalButtonComponent = require('./environment_terminal_button'); +import Timeago from 'timeago.js'; +import ActionsComponent from './environment_actions'; +import ExternalUrlComponent from './environment_external_url'; +import StopComponent from './environment_stop'; +import RollbackComponent from './environment_rollback'; +import TerminalButtonComponent from './environment_terminal_button'; +import '../../lib/utils/text_utility'; +import '../../vue_shared/components/commit'; /** * Envrionment Item Component @@ -17,7 +15,7 @@ const TerminalButtonComponent = require('./environment_terminal_button'); const timeagoInstance = new Timeago(); -module.exports = Vue.component('environment-item', { +export default { components: { 'commit-component': gl.CommitComponent, @@ -46,6 +44,11 @@ module.exports = Vue.component('environment-item', { required: false, default: false, }, + + service: { + type: Object, + required: true, + }, }, computed: { @@ -489,22 +492,25 @@ module.exports = Vue.component('environment-item', {
    + :stop-url="model.stop_path" + :service="service"/> + :retry-url="retryUrl" + :service="service"/>
    `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.js index daf126eb4e8..baa15d9e5b5 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js +++ b/app/assets/javascripts/environments/components/environment_rollback.js @@ -1,10 +1,14 @@ +/* global Flash */ +/* eslint-disable no-new */ /** * Renders Rollback or Re deploy button in environments table depending - * of the provided property `isLastDeployment` + * of the provided property `isLastDeployment`. + * + * Makes a post request when the button is clicked. */ -const Vue = require('vue'); +import eventHub from '../event_hub'; -module.exports = Vue.component('rollback-component', { +export default { props: { retryUrl: { type: String, @@ -15,16 +19,49 @@ module.exports = Vue.component('rollback-component', { type: Boolean, default: true, }, + + service: { + type: Object, + required: true, + }, + }, + + data() { + return { + isLoading: false, + }; + }, + + methods: { + onClick() { + this.isLoading = true; + + this.service.postAction(this.retryUrl) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshEnvironments'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, }, template: ` - + `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js index 96983a19568..5404d647745 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js +++ b/app/assets/javascripts/environments/components/environment_stop.js @@ -1,24 +1,56 @@ +/* global Flash */ +/* eslint-disable no-new, no-alert */ /** * Renders the stop "button" that allows stop an environment. * Used in environments table. */ -const Vue = require('vue'); +import eventHub from '../event_hub'; -module.exports = Vue.component('stop-component', { +export default { props: { stopUrl: { type: String, default: '', }, + + service: { + type: Object, + required: true, + }, + }, + + data() { + return { + isLoading: false, + }; + }, + + methods: { + onClick() { + if (confirm('Are you sure you want to stop this environment?')) { + this.isLoading = true; + + this.service.postAction(this.retryUrl) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshEnvironments'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.', 'alert'); + }); + } + }, }, template: ` - + `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js index e86607e78f4..66a71faa02f 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js @@ -2,13 +2,13 @@ * Renders a terminal button to open a web terminal. * Used in environments table. */ -const Vue = require('vue'); -const terminalIconSvg = require('icons/_icon_terminal.svg'); +import terminalIconSvg from 'icons/_icon_terminal.svg'; -module.exports = Vue.component('terminal-button-component', { +export default { props: { terminalPath: { type: String, + required: false, default: '', }, }, @@ -19,8 +19,9 @@ module.exports = Vue.component('terminal-button-component', { template: ` ${terminalIconSvg} `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js index 4088d63be80..5f07b612b91 100644 --- a/app/assets/javascripts/environments/components/environments_table.js +++ b/app/assets/javascripts/environments/components/environments_table.js @@ -1,11 +1,9 @@ /** * Render environments table. */ -const Vue = require('vue'); -const EnvironmentItem = require('./environment_item'); - -module.exports = Vue.component('environment-table-component', { +import EnvironmentItem from './environment_item'; +export default { components: { 'environment-item': EnvironmentItem, }, @@ -28,6 +26,11 @@ module.exports = Vue.component('environment-table-component', { required: false, default: false, }, + + service: { + type: Object, + required: true, + }, }, template: ` @@ -48,9 +51,10 @@ module.exports = Vue.component('environment-table-component', { + :can-read-environment="canReadEnvironment" + :service="service"> `, -}); +}; diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js index 7bbba91bc10..8d963b335cf 100644 --- a/app/assets/javascripts/environments/environments_bundle.js +++ b/app/assets/javascripts/environments/environments_bundle.js @@ -1,4 +1,4 @@ -const EnvironmentsComponent = require('./components/environment'); +import EnvironmentsComponent from './components/environment'; $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/event_hub.js b/app/assets/javascripts/environments/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/environments/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index d2ca465351a..f939eccf246 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -1,4 +1,4 @@ -const EnvironmentsFolderComponent = require('./environments_folder_view'); +import EnvironmentsFolderComponent from './environments_folder_view'; $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js index 2a9d0492d7a..7abcf6dbbea 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.js +++ b/app/assets/javascripts/environments/folder/environments_folder_view.js @@ -1,16 +1,16 @@ /* eslint-disable no-param-reassign, no-new */ /* global Flash */ +import EnvironmentsService from '../services/environments_service'; +import EnvironmentTable from '../components/environments_table'; +import EnvironmentsStore from '../stores/environments_store'; const Vue = window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -const EnvironmentsService = require('../services/environments_service'); -const EnvironmentTable = require('../components/environments_table'); -const EnvironmentsStore = require('../stores/environments_store'); require('../../vue_shared/components/table_pagination'); require('../../lib/utils/common_utils'); require('../../vue_shared/vue_resource_interceptor'); -module.exports = Vue.component('environment-folder-view', { +export default Vue.component('environment-folder-view', { components: { 'environment-table': EnvironmentTable, @@ -88,11 +88,11 @@ module.exports = Vue.component('environment-folder-view', { const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; - const service = new EnvironmentsService(endpoint); + this.service = new EnvironmentsService(endpoint); this.isLoading = true; - return service.get() + return this.service.get() .then(resp => ({ headers: resp.headers, body: resp.json(), @@ -168,13 +168,12 @@ module.exports = Vue.component('environment-folder-view', { :can-read-environment="canReadEnvironmentParsed" :play-icon-svg="playIconSvg" :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg"> - + :commit-icon-svg="commitIconSvg" + :service="service"/> - + :pageInfo="state.paginationInformation"/> diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index effc6c4c838..76296c83d11 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -1,13 +1,16 @@ -const Vue = require('vue'); +/* eslint-disable class-methods-use-this */ +import Vue from 'vue'; -class EnvironmentsService { +export default class EnvironmentsService { constructor(endpoint) { this.environments = Vue.resource(endpoint); } - get() { - return this.environments.get(); + get(scope, page) { + return this.environments.get({ scope, page }); } -} -module.exports = EnvironmentsService; + postAction(endpoint) { + return Vue.http.post(endpoint, {}, { emulateJSON: true }); + } +} diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 15cd9bde08e..d3fe3872c56 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -1,11 +1,12 @@ -require('~/lib/utils/common_utils'); +import '~/lib/utils/common_utils'; + /** * Environments Store. * * Stores received environments, count of stopped environments and count of * available environments. */ -class EnvironmentsStore { +export default class EnvironmentsStore { constructor() { this.state = {}; this.state.environments = []; @@ -86,5 +87,3 @@ class EnvironmentsStore { return count; } } - -module.exports = EnvironmentsStore; diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index d3229f9f730..4157fefddc9 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -6,10 +6,6 @@ Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; next((response) => { - if (typeof response.data === 'string') { - response.data = JSON.parse(response.data); - } - Vue.activeResources--; }); }); diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 0e2b8dba780..73a5da715f2 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -141,6 +141,14 @@ margin-right: 0; } } + + .no-btn { + border: none; + background: none; + outline: none; + width: 100%; + text-align: left; + } } } diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 25f31b423b8..641e2cf7402 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -111,10 +111,8 @@ feature 'Environments page', :feature, :js do find('.js-dropdown-play-icon-container').click expect(page).to have_content(action.name.humanize) - expect { click_link(action.name.humanize) } + expect { find('.js-manual-action-link').click } .not_to change { Ci::Pipeline.count } - - expect(action.reload).to be_pending end scenario 'does show build name and id' do @@ -158,12 +156,6 @@ feature 'Environments page', :feature, :js do expect(page).to have_selector('.stop-env-link') end - scenario 'starts build when stop button clicked' do - find('.stop-env-link').click - - expect(page).to have_content('close_app') - end - context 'for reporter' do let(:role) { :reporter } diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index d50d45d295e..85b73f1d4e2 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -1,14 +1,16 @@ -const ActionsComponent = require('~/environments/components/environment_actions'); +import Vue from 'vue'; +import actionsComp from '~/environments/components/environment_actions'; describe('Actions Component', () => { - preloadFixtures('static/environments/element.html.raw'); + let ActionsComponent; + let actionsMock; + let spy; + let component; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); - }); + ActionsComponent = Vue.extend(actionsComp); - it('should render a dropdown with the provided actions', () => { - const actionsMock = [ + actionsMock = [ { name: 'bar', play_path: 'https://gitlab.com/play', @@ -19,18 +21,27 @@ describe('Actions Component', () => { }, ]; - const component = new ActionsComponent({ - el: document.querySelector('.test-dom-element'), + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + component = new ActionsComponent({ propsData: { actions: actionsMock, + service: { + postAction: spy, + }, }, - }); + }).$mount(); + }); + it('should render a dropdown with the provided actions', () => { expect( component.$el.querySelectorAll('.dropdown-menu li').length, ).toEqual(actionsMock.length); - expect( - component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), - ).toEqual(actionsMock[0].play_path); + }); + + it('should call the service when an action is clicked', () => { + component.$el.querySelector('.dropdown').click(); + component.$el.querySelector('.js-manual-action-link').click(); + + expect(spy).toHaveBeenCalledWith(actionsMock[0].play_path); }); }); diff --git a/spec/javascripts/environments/environment_external_url_spec.js b/spec/javascripts/environments/environment_external_url_spec.js index 393dbb5aae0..9af218a27ff 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js +++ b/spec/javascripts/environments/environment_external_url_spec.js @@ -1,19 +1,20 @@ -const ExternalUrlComponent = require('~/environments/components/environment_external_url'); +import Vue from 'vue'; +import externalUrlComp from '~/environments/components/environment_external_url'; describe('External URL Component', () => { - preloadFixtures('static/environments/element.html.raw'); + let ExternalUrlComponent; + beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + ExternalUrlComponent = Vue.extend(externalUrlComp); }); it('should link to the provided externalUrl prop', () => { const externalURL = 'https://gitlab.com'; const component = new ExternalUrlComponent({ - el: document.querySelector('.test-dom-element'), propsData: { externalUrl: externalURL, }, - }); + }).$mount(); expect(component.$el.getAttribute('href')).toEqual(externalURL); expect(component.$el.querySelector('fa-external-link')).toBeDefined(); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 7fea80ed799..4d42de4d549 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -1,10 +1,12 @@ -window.timeago = require('timeago.js'); -const EnvironmentItem = require('~/environments/components/environment_item'); +import 'timeago.js'; +import Vue from 'vue'; +import environmentItemComp from '~/environments/components/environment_item'; describe('Environment item', () => { - preloadFixtures('static/environments/table.html.raw'); + let EnvironmentItem; + beforeEach(() => { - loadFixtures('static/environments/table.html.raw'); + EnvironmentItem = Vue.extend(environmentItemComp); }); describe('When item is folder', () => { @@ -21,13 +23,13 @@ describe('Environment item', () => { }; component = new EnvironmentItem({ - el: document.querySelector('tr#environment-row'), propsData: { model: mockItem, canCreateDeployment: false, canReadEnvironment: true, + service: {}, }, - }); + }).$mount(); }); it('Should render folder icon and name', () => { @@ -109,13 +111,13 @@ describe('Environment item', () => { }; component = new EnvironmentItem({ - el: document.querySelector('tr#environment-row'), propsData: { model: environment, canCreateDeployment: true, canReadEnvironment: true, + service: {}, }, - }); + }).$mount(); }); it('should render environment name', () => { diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js index 4a596baad09..7cb39d9df03 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js +++ b/spec/javascripts/environments/environment_rollback_spec.js @@ -1,47 +1,59 @@ -const RollbackComponent = require('~/environments/components/environment_rollback'); +import Vue from 'vue'; +import rollbackComp from '~/environments/components/environment_rollback'; describe('Rollback Component', () => { - preloadFixtures('static/environments/element.html.raw'); - const retryURL = 'https://gitlab.com/retry'; + let RollbackComponent; + let spy; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + RollbackComponent = Vue.extend(rollbackComp); + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); }); - it('Should link to the provided retryUrl', () => { + it('Should render Re-deploy label when isLastDeployment is true', () => { const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, isLastDeployment: true, + service: { + postAction: spy, + }, }, - }); + }).$mount(); - expect(component.$el.getAttribute('href')).toEqual(retryURL); + expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); }); - it('Should render Re-deploy label when isLastDeployment is true', () => { + it('Should render Rollback label when isLastDeployment is false', () => { const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, - isLastDeployment: true, + isLastDeployment: false, + service: { + postAction: spy, + }, }, - }); + }).$mount(); - expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); + expect(component.$el.querySelector('span').textContent).toContain('Rollback'); }); - it('Should render Rollback label when isLastDeployment is false', () => { + it('should call the service when the button is clicked', () => { const component = new RollbackComponent({ - el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, isLastDeployment: false, + service: { + postAction: spy, + }, }, - }); + }).$mount(); - expect(component.$el.querySelector('span').textContent).toContain('Rollback'); + component.$el.click(); + + expect(spy).toHaveBeenCalledWith(retryURL); }); }); diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js index edd0cad32d0..9601575577e 100644 --- a/spec/javascripts/environments/environment_spec.js +++ b/spec/javascripts/environments/environment_spec.js @@ -1,7 +1,7 @@ -const Vue = require('vue'); -require('~/flash'); -const EnvironmentsComponent = require('~/environments/components/environment'); -const { environment } = require('./mock_data'); +import Vue from 'vue'; +import '~/flash'; +import EnvironmentsComponent from '~/environments/components/environment'; +import { environment } from './mock_data'; describe('Environment', () => { preloadFixtures('static/environments/environments.html.raw'); diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js index 5ca65b1debc..8f79b88f3df 100644 --- a/spec/javascripts/environments/environment_stop_spec.js +++ b/spec/javascripts/environments/environment_stop_spec.js @@ -1,28 +1,34 @@ -const StopComponent = require('~/environments/components/environment_stop'); +import Vue from 'vue'; +import stopComp from '~/environments/components/environment_stop'; describe('Stop Component', () => { - preloadFixtures('static/environments/element.html.raw'); - - let stopURL; + let StopComponent; let component; + let spy; + const stopURL = '/stop'; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + StopComponent = Vue.extend(stopComp); + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + spyOn(window, 'confirm').and.returnValue(true); - stopURL = '/stop'; component = new StopComponent({ - el: document.querySelector('.test-dom-element'), propsData: { stopUrl: stopURL, + service: { + postAction: spy, + }, }, - }); + }).$mount(); }); - it('should link to the provided URL', () => { - expect(component.$el.getAttribute('href')).toEqual(stopURL); + it('should render a button to stop the environment', () => { + expect(component.$el.tagName).toEqual('BUTTON'); + expect(component.$el.getAttribute('title')).toEqual('Stop Environment'); }); - it('should have a data-confirm attribute', () => { - expect(component.$el.getAttribute('data-confirm')).toEqual('Are you sure you want to stop this environment?'); + it('should call the service when an action is clicked', () => { + component.$el.click(); + expect(spy).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index be4330b5012..3df967848a7 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -1,4 +1,5 @@ -const EnvironmentTable = require('~/environments/components/environments_table'); +import Vue from 'vue'; +import environmentTableComp from '~/environments/components/environments_table'; describe('Environment item', () => { preloadFixtures('static/environments/element.html.raw'); @@ -16,14 +17,17 @@ describe('Environment item', () => { }, }; + const EnvironmentTable = Vue.extend(environmentTableComp); + const component = new EnvironmentTable({ el: document.querySelector('.test-dom-element'), propsData: { environments: [{ mockItem }], canCreateDeployment: false, canReadEnvironment: true, + service: {}, }, - }); + }).$mount(); expect(component.$el.tagName).toEqual('TABLE'); }); diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js new file mode 100644 index 00000000000..b07aa4e1745 --- /dev/null +++ b/spec/javascripts/environments/environment_terminal_button_spec.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import terminalComp from '~/environments/components/environment_terminal_button'; + +describe('Stop Component', () => { + let TerminalComponent; + let component; + const terminalPath = '/path'; + + beforeEach(() => { + TerminalComponent = Vue.extend(terminalComp); + + component = new TerminalComponent({ + propsData: { + terminalPath, + }, + }).$mount(); + }); + + it('should render a link to open a web terminal with the provided path', () => { + expect(component.$el.tagName).toEqual('A'); + expect(component.$el.getAttribute('title')).toEqual('Open web terminal'); + expect(component.$el.getAttribute('href')).toEqual(terminalPath); + }); +}); diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js index 77e182b3830..115d84b50f5 100644 --- a/spec/javascripts/environments/environments_store_spec.js +++ b/spec/javascripts/environments/environments_store_spec.js @@ -1,5 +1,5 @@ -const Store = require('~/environments/stores/environments_store'); -const { environmentsList, serverData } = require('./mock_data'); +import Store from '~/environments/stores/environments_store'; +import { environmentsList, serverData } from './mock_data'; (() => { describe('Store', () => { diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index d1335b5b304..43a217a67f5 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,7 +1,7 @@ -const Vue = require('vue'); -require('~/flash'); -const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view'); -const { environmentsList } = require('../mock_data'); +import Vue from 'vue'; +import '~/flash'; +import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view'; +import { environmentsList } from '../mock_data'; describe('Environments Folder View', () => { preloadFixtures('static/environments/environments_folder_view.html.raw'); diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js index 5c395c6b2d8..30861481cc5 100644 --- a/spec/javascripts/environments/mock_data.js +++ b/spec/javascripts/environments/mock_data.js @@ -1,4 +1,4 @@ -const environmentsList = [ +export const environmentsList = [ { name: 'DEV', size: 1, @@ -30,7 +30,7 @@ const environmentsList = [ }, ]; -const serverData = [ +export const serverData = [ { name: 'DEV', size: 1, @@ -67,7 +67,7 @@ const serverData = [ }, ]; -const environment = { +export const environment = { name: 'DEV', size: 1, latest: { @@ -84,9 +84,3 @@ const environment = { updated_at: '2017-01-31T10:53:46.894Z', }, }; - -module.exports = { - environmentsList, - environment, - serverData, -}; -- cgit v1.2.1 From 461d2672a495b7990dc578fa0bcba8bfdd781fd2 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 14 Mar 2017 01:14:44 +0000 Subject: Set 'config.assets.compile = false' in production --- config/environments/production.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index a9d8ac4b6d4..82a19085b1d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -16,7 +16,7 @@ Rails.application.configure do # config.assets.css_compressor = :sass # Don't fallback to assets pipeline if a precompiled asset is missed - config.assets.compile = true + config.assets.compile = false # Generate digests for assets URLs config.assets.digest = true -- cgit v1.2.1 From 29c5b31c192925e2a73e789ab6904168801ab260 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 13 Mar 2017 22:16:29 -0700 Subject: Remove unused satellites config Note that the old migrations depend on 1_settings.rb, so we can't quite remove those completely. --- config/gitlab.yml.example | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 2bc39ea3f65..954809a882c 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -456,14 +456,6 @@ production: &base # 4. Advanced settings # ========================== - # GitLab Satellites - # - # Note for maintainers: keep the satellites.path setting until GitLab 9.0 at - # least. This setting is fed to 'rm -rf' in - # db/migrate/20151023144219_remove_satellites.rb - satellites: - path: /home/git/gitlab-satellites/ - ## Repositories settings repositories: # Paths where repositories can be stored. Give the canonicalized absolute pathname. @@ -581,8 +573,6 @@ test: # In order to setup it correctly you need to specify # your system username you use to run GitLab # user: YOUR_USERNAME - satellites: - path: tmp/tests/gitlab-satellites/ repositories: storages: default: -- cgit v1.2.1 From 0b20d850cb964da89f8b0df3c9e78a1cc2a86e1b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 14:36:33 +0800 Subject: Fix for postgresql --- db/migrate/20161201160452_migrate_project_statistics.rb | 4 ++-- db/migrate/20161212142807_add_lower_path_index_to_routes.rb | 2 +- db/migrate/20170130204620_add_index_to_project_authorizations.rb | 4 ++++ db/migrate/20170305203726_add_owner_id_foreign_key.rb | 6 +++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb index 3ae3f2c159b..8386f7f9d4f 100644 --- a/db/migrate/20161201160452_migrate_project_statistics.rb +++ b/db/migrate/20161201160452_migrate_project_statistics.rb @@ -17,7 +17,7 @@ class MigrateProjectStatistics < ActiveRecord::Migration end def down - add_column_with_default :projects, :repository_size, :float, default: 0.0 - add_column_with_default :projects, :commit_count, :integer, default: 0 + add_column :projects, :repository_size, :float, default: 0.0 + add_column :projects, :commit_count, :integer, default: 0 end end diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb index 6958500306f..53f4c6bbb18 100644 --- a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb +++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb @@ -17,6 +17,6 @@ class AddLowerPathIndexToRoutes < ActiveRecord::Migration def down return unless Gitlab::Database.postgresql? - remove_index :routes, name: :index_on_routes_lower_path + remove_index :routes, name: :index_on_routes_lower_path if index_exists?(:routes, name: :index_on_routes_lower_path) end end diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb index e9a0aee4d6a..a8c504f265a 100644 --- a/db/migrate/20170130204620_add_index_to_project_authorizations.rb +++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb @@ -8,4 +8,8 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration def up add_concurrent_index(:project_authorizations, :project_id) end + + def down + remove_index(:project_authorizations, :project_id) + end end diff --git a/db/migrate/20170305203726_add_owner_id_foreign_key.rb b/db/migrate/20170305203726_add_owner_id_foreign_key.rb index 3eece0e2eb5..5fbdc45f1a7 100644 --- a/db/migrate/20170305203726_add_owner_id_foreign_key.rb +++ b/db/migrate/20170305203726_add_owner_id_foreign_key.rb @@ -5,7 +5,11 @@ class AddOwnerIdForeignKey < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade end + + def down + remove_foreign_key :ci_triggers, column: :owner_id + end end -- cgit v1.2.1 From 77d93d33820b9771b906c2d2bd708b4b910aa9e9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 14 Mar 2017 09:53:28 +0100 Subject: Add missing steps of Pages source installation [ci skip] --- doc/administration/pages/source.md | 82 ++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index f6f50e2c571..b4588f8b43c 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -1,5 +1,9 @@ # GitLab Pages administration for source installations +>**Note:** +Before attempting to enable GitLab Pages, first make sure you have +[installed GitLab](../../install/installation.md) successfully. + This is the documentation for configuring a GitLab Pages when you have installed GitLab from source and not using the Omnibus packages. @@ -13,7 +17,33 @@ Pages to the latest supported version. ## Overview -[Read the Omnibus overview section.](index.md#overview) +GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server +written in Go that can listen on an external IP address and provide support for +custom domains and custom certificates. It supports dynamic certificates through +SNI and exposes pages using HTTP2 by default. +You are encouraged to read its [README][pages-readme] to fully understand how +it works. + +--- + +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 + [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 + 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. If you are not +supporting custom domains a secondary IP is not needed. ## Prerequisites @@ -75,7 +105,7 @@ The Pages daemon doesn't listen to the outside world. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.4 + sudo -u git -H git checkout v0.3.2 sudo -u git -H make ``` @@ -100,14 +130,21 @@ The Pages daemon doesn't listen to the outside world. https: false ``` -1. Copy the `gitlab-pages-ssl` Nginx configuration file: +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain` must match the `host` setting that you set above. - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 + ``` + +1. Copy the `gitlab-pages` Nginx configuration file: - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + ```bash + sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf + ``` 1. Restart NGINX 1. [Restart GitLab][restart] @@ -131,7 +168,7 @@ outside world. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.4 + sudo -u git -H git checkout v0.3.2 sudo -u git -H make ``` @@ -149,6 +186,17 @@ outside world. https: true ``` +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain` must match the `host` setting that you set above. + The `-root-cert` and `-root-key` settings are the wildcard TLS certificates + of the `example.io` domain: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + ``` + 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash @@ -156,12 +204,9 @@ outside world. sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - 1. Restart NGINX 1. [Restart GitLab][restart] - ## Advanced configuration In addition to the wildcard domains, you can also have the option to configure @@ -189,7 +234,7 @@ world. Custom domains are supported, but no TLS. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.4 + sudo -u git -H git checkout v0.3.2 sudo -u git -H make ``` @@ -224,12 +269,10 @@ world. Custom domains are supported, but no TLS. 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf ``` - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - 1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab listens to. @@ -257,7 +300,7 @@ world. Custom domains and TLS are supported. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.4 + sudo -u git -H git checkout v0.3.2 sudo -u git -H make ``` @@ -300,8 +343,6 @@ world. Custom domains and TLS are supported. sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - 1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab listens to. @@ -392,5 +433,6 @@ than GitLab to prevent XSS attacks. [pages-userguide]: ../../user/project/pages/index.md [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../restart_gitlab.md#installations-from-source -[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.3.2 +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab.default.example [shared runners]: ../../ci/runners/README.md -- cgit v1.2.1 From 4cbe05e8029c32e68719b4b8445c6ab5677c519d Mon Sep 17 00:00:00 2001 From: Xiaogang Wen Date: Tue, 14 Mar 2017 09:09:56 +0000 Subject: Patch 15 --- doc/integration/github.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/github.md b/doc/integration/github.md index cea85f073cc..4b0d33334bd 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -19,7 +19,7 @@ GitHub will generate an application ID and secret key for you to use. - Application name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}' + - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'. Please make sure the port is included if your Gitlab instance is not configured on default port. 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). -- cgit v1.2.1 From e8c8ea8408de8241f003d76a25cdc773877e98e1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 14 Mar 2017 10:28:11 +0100 Subject: Include instructions to update /etc/default/gitlab We were missing some info on updating /etc/default/gitlab In particular, changes needed to be made in order for Pages to work, see https://gitlab.com/gitlab-org/gitlab-ce/issues/29372 --- doc/update/8.16-to-8.17.md | 14 +++++++++++++- doc/update/8.17-to-9.0.md | 13 +++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md index 954109ba18f..74ffe0bc846 100644 --- a/doc/update/8.16-to-8.17.md +++ b/doc/update/8.16-to-8.17.md @@ -139,7 +139,7 @@ sudo -u git -H git checkout v4.1.1 #### New configuration options for `gitlab.yml` -There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: +There might be new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: ```sh cd /home/git/gitlab @@ -195,6 +195,16 @@ See [smtp_settings.rb.sample] as an example. #### Init script +There might be new configuration options available for [`gitlab.default.example`][gl-example]. +You need to update this file if you want to [enable GitLab Pages][pages-admin]. +View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/8-16-stable:lib/support/init.d/gitlab.default.example origin/8-17-stable:lib/support/init.d/gitlab.default.example +``` + Ensure you're still up-to-date with the latest init script changes: ```bash @@ -254,3 +264,5 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. [yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/lib/support/init.d/gitlab.default.example +[pages-admin]: ../administration/pages/source.md diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 1fe38cf8d2a..626507c0482 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -149,7 +149,7 @@ sudo -u git -H git checkout v5.0.0 #### New configuration options for `gitlab.yml` -There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: ```sh cd /home/git/gitlab @@ -189,7 +189,7 @@ Update your current configuration as follows, replacing with your storages names **For Omnibus installations** -1. Upate your `/etc/gitlab/gitlab.rb`, from +1. Update your `/etc/gitlab/gitlab.rb`, from ```ruby git_data_dirs({ @@ -260,6 +260,14 @@ See [smtp_settings.rb.sample] as an example. #### Init script +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/8-17-stable:lib/support/init.d/gitlab.default.example origin/9-0-stable:lib/support/init.d/gitlab.default.example +``` + Ensure you're still up-to-date with the latest init script changes: ```bash @@ -319,3 +327,4 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. [yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example -- cgit v1.2.1 From 435458d2b14eadd1768b7b0a14f5966633f02f83 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 6 Mar 2017 00:03:21 -0500 Subject: Update API on frontend to use v4 Use options object to pass params for project endpoint --- app/assets/javascripts/api.js | 10 +++++----- app/assets/javascripts/project_select.js | 4 ++-- app/assets/javascripts/search.js | 2 +- app/views/help/ui.html.haml | 2 +- lib/gitlab/gon_helper.rb | 2 +- spec/javascripts/project_title_spec.js | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index a0946eb392a..e5f36c84987 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -51,15 +51,15 @@ var Api = { }); }, // Return projects list. Filtered by query - projects: function(query, order, callback) { + projects: function(query, options, callback) { var url = Api.buildUrl(Api.projectsPath); return $.ajax({ url: url, - data: { + data: $.extend({ search: query, - order_by: order, - per_page: 20 - }, + per_page: 20, + membership: true + }, options), dataType: "json" }).done(function(projects) { return callback(projects); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index f80e765ce30..3c1c1e7dceb 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -35,7 +35,7 @@ if (this.groupId) { return Api.groupProjects(this.groupId, term, projectsCallback); } else { - return Api.projects(term, orderBy, projectsCallback); + return Api.projects(term, { order_by: orderBy }, projectsCallback); } }, url: function(project) { @@ -84,7 +84,7 @@ if (_this.groupId) { return Api.groupProjects(_this.groupId, query.term, projectsCallback); } else { - return Api.projects(query.term, _this.orderBy, projectsCallback); + return Api.projects(query.term, { order_by: _this.orderBy }, projectsCallback); } }; })(this), diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index e66418beeab..15f5963353a 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -47,7 +47,7 @@ fields: ['name'] }, data: function(term, callback) { - return Api.projects(term, 'id', function(data) { + return Api.projects(term, { order_by: 'id' }, function(data) { data.unshift({ name_with_namespace: 'Any' }); diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 87f9b503989..1fb2c6271ad 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -410,7 +410,7 @@ :javascript $('#js-project-dropdown').glDropdown({ data: function (term, callback) { - Api.projects(term, "last_activity_at", function (data) { + Api.projects(term, { order_by: 'last_activity_at' }, function (data) { callback(data); }); }, diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 6c275a8d5de..5ab84266b7d 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -1,7 +1,7 @@ module Gitlab module GonHelper def add_gon_variables - gon.api_version = 'v3' # v4 Is not officially released yet, therefore can't be considered as "frozen" + gon.api_version = 'v4' gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s gon.max_file_size = current_application_settings.max_attachment_size gon.asset_host = ActionController::Base.asset_host diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 69d9587771f..3a1d4e2440f 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -26,7 +26,7 @@ require('~/project'); var fakeAjaxResponse = function fakeAjaxResponse(req) { var d; expect(req.url).toBe('/api/v3/projects.json?simple=true'); - expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 }); + expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20, membership: true }); d = $.Deferred(); d.resolve(this.projects_data); return d.promise(); -- cgit v1.2.1 From 6bffc17f7d04f6ecd99d896ed299060724032a3b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 14 Mar 2017 12:24:53 +0100 Subject: Add changelog entry for project status caching fix --- changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml diff --git a/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml new file mode 100644 index 00000000000..4db684c40b2 --- /dev/null +++ b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml @@ -0,0 +1,4 @@ +--- +title: Resolve project pipeline status caching problem on dashboard +merge_request: 9895 +author: -- cgit v1.2.1 From c7cecae616702a46430ed41e283912ddc22f2612 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 14 Mar 2017 11:32:58 +0000 Subject: added eventhub to emit update tokens event --- app/assets/javascripts/boards/boards_bundle.js | 5 +++-- app/assets/javascripts/boards/components/issue_card_inner.js | 4 +++- app/assets/javascripts/boards/eventhub.js | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/boards/eventhub.js diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 4d60fedaeb8..3874c2819a5 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -3,6 +3,7 @@ /* global BoardService */ import FilteredSearchBoards from './filtered_search_boards'; +import eventHub from './eventhub'; window.Vue = require('vue'); window.Vue.use(require('vue-resource')); @@ -65,10 +66,10 @@ $(() => { this.filterManager = new FilteredSearchBoards(Store.filter, true); // Listen for updateTokens event - this.$on('updateTokens', this.updateTokens); + eventHub.$on('updateTokens', this.updateTokens); }, beforeDestroy() { - this.$off('updateTokens', this.updateTokens); + eventHub.$off('updateTokens', this.updateTokens); }, mounted () { Store.disabled = this.disabled; diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 3d57ec429c6..69e30cec4c5 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -1,4 +1,6 @@ /* global Vue */ +import eventHub from '../eventhub'; + (() => { const Store = gl.issueBoards.BoardsStore; @@ -54,7 +56,7 @@ Store.updateFiltersUrl(); - gl.IssueBoardsApp.$emit('updateTokens'); + eventHub.$emit('updateTokens'); }, labelStyle(label) { return { diff --git a/app/assets/javascripts/boards/eventhub.js b/app/assets/javascripts/boards/eventhub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/boards/eventhub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); -- cgit v1.2.1 From c9fbbb3ae2c7f0eb44b0f973155d68e678149544 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 19:56:37 +0800 Subject: Disable rubocop for down method --- db/migrate/20161201160452_migrate_project_statistics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb index 8386f7f9d4f..82fbdf02444 100644 --- a/db/migrate/20161201160452_migrate_project_statistics.rb +++ b/db/migrate/20161201160452_migrate_project_statistics.rb @@ -16,6 +16,7 @@ class MigrateProjectStatistics < ActiveRecord::Migration remove_column :projects, :commit_count end + # rubocop: disable Migration/AddColumn def down add_column :projects, :repository_size, :float, default: 0.0 add_column :projects, :commit_count, :integer, default: 0 -- cgit v1.2.1 From 1324a6b3050a121eae722258302ce46bf2636ac1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 14 Mar 2017 13:03:17 +0100 Subject: Use google-protobuf 3.2.0.1 This sub-patch release extends support in pre-compiled libraries down to glibc 2.12. https://github.com/google/protobuf/issues/2783 Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/29084 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c60c045a4c2..e38f45b8e98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -304,7 +304,7 @@ GEM multi_json (~> 1.10) retriable (~> 1.4) signet (~> 0.6) - google-protobuf (3.2.0) + google-protobuf (3.2.0.1) googleauth (0.5.1) faraday (~> 0.9) jwt (~> 1.4) -- cgit v1.2.1 From f67d8eb1da269150764224cea1807195cdf2ffb5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 20:03:22 +0800 Subject: Drop the index only for postgresql, because mysql cannot simply drop the index without dropping the corresponding foreign key, and we certainly don't want to drop the foreign key here. --- db/migrate/20170130204620_add_index_to_project_authorizations.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb index a8c504f265a..629b49436e3 100644 --- a/db/migrate/20170130204620_add_index_to_project_authorizations.rb +++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb @@ -10,6 +10,7 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration end def down - remove_index(:project_authorizations, :project_id) + remove_index(:project_authorizations, :project_id) if + Gitlab::Database.postgresql? end end -- cgit v1.2.1 From 96fe1856da46517b028fe0ddac89f314f25c8855 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 9 Mar 2017 16:51:20 +0200 Subject: Fix relative position calculation --- app/models/concerns/relative_positioning.rb | 20 ++++++++++------- spec/models/concerns/relative_positioning_spec.rb | 27 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 603f2dd7e5d..ec23c8a08fb 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -3,6 +3,7 @@ module RelativePositioning MIN_POSITION = 0 MAX_POSITION = Gitlab::Database::MAX_INT_VALUE + DISTANCE = 500 included do after_save :save_positionable_neighbours @@ -49,7 +50,9 @@ module RelativePositioning pos_before = before.relative_position pos_after = after.relative_position - if pos_after && (pos_before == pos_after) + # We can't insert an issue between two other if the distance is 1 or 0 + # so we need to handle this collision properly + if pos_after && (pos_after - pos_before).abs <= 1 self.relative_position = pos_before before.move_before(self) after.move_after(self) @@ -75,19 +78,20 @@ module RelativePositioning private # This method takes two integer values (positions) and - # calculates some random position between them. The range is huge as - # the maximum integer value is 2147483647. Ideally, the calculated value would be - # exactly between those terminating values, but this will introduce possibility of a race condition - # so two or more issues can get the same value, we want to avoid that and we also want to avoid - # using a lock here. If we have two issues with distance more than one thousand, we are OK. - # Given the huge range of possible values that integer can fit we shoud never face a problem. + # calculates the position between them. The range is huge as + # the maximum integer value is 2147483647. We are incrementing position by 1000 every time + # when we have enough space. If distance is less then 500 we are calculating an average number def position_between(pos_before, pos_after) pos_before ||= MIN_POSITION pos_after ||= MAX_POSITION pos_before, pos_after = [pos_before, pos_after].sort - rand(pos_before.next..pos_after.pred) + if pos_after - pos_before > DISTANCE * 2 + pos_before + DISTANCE + else + pos_before + (pos_after - pos_before) / 2 + end end def save_positionable_neighbours diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index 69906382545..12f44bfe0a5 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -100,5 +100,32 @@ describe Issue, 'RelativePositioning' do expect(new_issue.relative_position).to be > issue.relative_position expect(issue.relative_position).to be < issue1.relative_position end + + it 'positions issues between other two if distance is 1' do + issue1.update relative_position: issue.relative_position + 1 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be > issue.relative_position + expect(issue.relative_position).to be < issue1.relative_position + end + + it 'positions issue closer to before-issue if distance is big enough' do + issue.update relative_position: 100 + issue1.update relative_position: 6000 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to eq(100 + RelativePositioning::DISTANCE) + end + + it 'positions issue in the middle of other two if distance is not big enough' do + issue.update relative_position: 100 + issue1.update relative_position: 400 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to eq(250) + end end end -- cgit v1.2.1 From 67686e38fdb1d9c427a39ff1862af691ccd4e598 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 9 Mar 2017 19:23:36 +0200 Subject: Added migration to reset existing relative_position for issues --- ...20170309171644_reset_relative_position_for_issue.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 db/post_migrate/20170309171644_reset_relative_position_for_issue.rb diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb new file mode 100644 index 00000000000..ce4be131d40 --- /dev/null +++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ResetRelativePositionForIssue < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute <<-EOS + UPDATE issues SET relative_position = NULL + WHERE issues.relative_position IS NOT NULL; + EOS + end + + def down + end +end -- cgit v1.2.1 From 5670777735a615b511c3282e8fc79b67c74669bc Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 10 Mar 2017 17:12:31 +0200 Subject: [Issue sorting] Filling positions preferable in the middle --- app/models/concerns/relative_positioning.rb | 23 +++++++------ spec/models/concerns/relative_positioning_spec.rb | 40 +++++++++++++++-------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index ec23c8a08fb..f8ab16a9f4c 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -2,6 +2,7 @@ module RelativePositioning extend ActiveSupport::Concern MIN_POSITION = 0 + START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2 MAX_POSITION = Gitlab::Database::MAX_INT_VALUE DISTANCE = 500 @@ -9,10 +10,6 @@ module RelativePositioning after_save :save_positionable_neighbours end - def min_relative_position - self.class.in_projects(project.id).minimum(:relative_position) - end - def max_relative_position self.class.in_projects(project.id).maximum(:relative_position) end @@ -27,7 +24,7 @@ module RelativePositioning maximum(:relative_position) end - prev_pos || MIN_POSITION + prev_pos end def next_relative_position @@ -40,7 +37,7 @@ module RelativePositioning minimum(:relative_position) end - next_pos || MAX_POSITION + next_pos end def move_between(before, after) @@ -72,7 +69,7 @@ module RelativePositioning end def move_to_end - self.relative_position = position_between(max_relative_position, MAX_POSITION) + self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) end private @@ -87,10 +84,16 @@ module RelativePositioning pos_before, pos_after = [pos_before, pos_after].sort - if pos_after - pos_before > DISTANCE * 2 - pos_before + DISTANCE + if pos_after - pos_before < DISTANCE * 2 + (pos_after + pos_before) / 2 else - pos_before + (pos_after - pos_before) / 2 + if pos_before == MIN_POSITION + pos_after - DISTANCE + elsif pos_after == MAX_POSITION + pos_before + DISTANCE + else + (pos_after + pos_before) / 2 + end end end diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index 12f44bfe0a5..fbae9efcd98 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -12,12 +12,6 @@ describe Issue, 'RelativePositioning' do end end - describe '#min_relative_position' do - it 'returns maximum position' do - expect(issue.min_relative_position).to eq issue.relative_position - end - end - describe '#max_relative_position' do it 'returns maximum position' do expect(issue.max_relative_position).to eq issue1.relative_position @@ -29,8 +23,8 @@ describe Issue, 'RelativePositioning' do expect(issue1.prev_relative_position).to eq issue.relative_position end - it 'returns minimum position if there is no issue above' do - expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION + it 'returns nil if there is no issue above' do + expect(issue.prev_relative_position).to eq nil end end @@ -39,8 +33,8 @@ describe Issue, 'RelativePositioning' do expect(issue.next_relative_position).to eq issue1.relative_position end - it 'returns next position if there is no issue below' do - expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION + it 'returns nil if there is no issue below' do + expect(issue1.next_relative_position).to eq nil end end @@ -110,15 +104,33 @@ describe Issue, 'RelativePositioning' do expect(issue.relative_position).to be < issue1.relative_position end - it 'positions issue closer to before-issue if distance is big enough' do - issue.update relative_position: 100 - issue1.update relative_position: 6000 + it 'positions issue in the middle of other two if distance is big enough' do + issue.update relative_position: 6000 + issue1.update relative_position: 10000 new_issue.move_between(issue, issue1) - expect(new_issue.relative_position).to eq(100 + RelativePositioning::DISTANCE) + expect(new_issue.relative_position).to eq(8000) + end + + it 'positions issue closer to the middle if we are at the very top' do + issue1.update relative_position: 6000 + + new_issue.move_between(nil, issue1) + + expect(new_issue.relative_position).to eq(6000 - RelativePositioning::DISTANCE) + end + + it 'positions issue closer to the middle if we are at the very bottom' do + issue.update relative_position: 6000 + issue1.update relative_position: nil + + new_issue.move_between(issue, nil) + + expect(new_issue.relative_position).to eq(6000 + RelativePositioning::DISTANCE) end + it 'positions issue in the middle of other two if distance is not big enough' do issue.update relative_position: 100 issue1.update relative_position: 400 -- cgit v1.2.1 From b84723ac8bf8572c3d261980ab053dda52bc78dd Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 10 Mar 2017 17:17:55 +0200 Subject: [Issue Sorting] Improve migration --- .../20170309171644_reset_relative_position_for_issue.rb | 7 +++---- spec/models/concerns/relative_positioning_spec.rb | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb index ce4be131d40..b61dd7cfc61 100644 --- a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb +++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb @@ -7,10 +7,9 @@ class ResetRelativePositionForIssue < ActiveRecord::Migration DOWNTIME = false def up - execute <<-EOS - UPDATE issues SET relative_position = NULL - WHERE issues.relative_position IS NOT NULL; - EOS + update_column_in_batches(:issues, :relative_position, nil) do |table, query| + query.where(table[:relative_position].not_eq(nil)) + end end def down diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index fbae9efcd98..ebbf14fb5ba 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -130,7 +130,6 @@ describe Issue, 'RelativePositioning' do expect(new_issue.relative_position).to eq(6000 + RelativePositioning::DISTANCE) end - it 'positions issue in the middle of other two if distance is not big enough' do issue.update relative_position: 100 issue1.update relative_position: 400 -- cgit v1.2.1 From e752d6d157321f9f10d70cdef1ce1992e263634f Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 10 Mar 2017 19:04:37 +0200 Subject: [Issue sorting]Addressed review comments --- .flayignore | 1 + app/models/concerns/relative_positioning.rb | 77 ++++++++++++++++------- spec/models/concerns/relative_positioning_spec.rb | 66 ++++++++++++++++++- 3 files changed, 119 insertions(+), 25 deletions(-) diff --git a/.flayignore b/.flayignore index fc64b0b5892..47597025115 100644 --- a/.flayignore +++ b/.flayignore @@ -2,3 +2,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb lib/gitlab/diff/position_tracer.rb app/policies/project_policy.rb +app/models/concerns/relative_positioning.rb diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index f8ab16a9f4c..f1d8532a6d6 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -4,7 +4,7 @@ module RelativePositioning MIN_POSITION = 0 START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2 MAX_POSITION = Gitlab::Database::MAX_INT_VALUE - DISTANCE = 500 + IDEAL_DISTANCE = 500 included do after_save :save_positionable_neighbours @@ -44,55 +44,86 @@ module RelativePositioning return move_after(before) unless after return move_before(after) unless before + # If there is no place to insert an issue we need to create one by moving the before issue closer + # to its predecessor. This process will recursively move all the predecessors until we have a place + if (after.relative_position - before.relative_position) < 2 + before.move_before + @positionable_neighbours = [before] + end + + self.relative_position = position_between(before.relative_position, after.relative_position) + end + + def move_after(before = self) pos_before = before.relative_position - pos_after = after.relative_position + pos_after = before.next_relative_position - # We can't insert an issue between two other if the distance is 1 or 0 - # so we need to handle this collision properly - if pos_after && (pos_after - pos_before).abs <= 1 - self.relative_position = pos_before - before.move_before(self) - after.move_after(self) + if before.shift_after? + issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after) + issue_to_move.move_after + @positionable_neighbours = [issue_to_move] - @positionable_neighbours = [before, after] - else - self.relative_position = position_between(pos_before, pos_after) + pos_after = issue_to_move.relative_position end - end - def move_before(after) - self.relative_position = position_between(after.prev_relative_position, after.relative_position) + self.relative_position = position_between(pos_before, pos_after) end - def move_after(before) - self.relative_position = position_between(before.relative_position, before.next_relative_position) + def move_before(after = self) + pos_after = after.relative_position + pos_before = after.prev_relative_position + + if after.shift_before? + issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before) + issue_to_move.move_before + @positionable_neighbours = [issue_to_move] + + pos_before = issue_to_move.relative_position + end + + self.relative_position = position_between(pos_before, pos_after) end def move_to_end self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) end + # Indicates if there is an issue that should be shifted to free the place + def shift_after? + next_pos = next_relative_position + next_pos && (next_pos - relative_position) == 1 + end + + # Indicates if there is an issue that should be shifted to free the place + def shift_before? + prev_pos = prev_relative_position + prev_pos && (relative_position - prev_pos) == 1 + end + private # This method takes two integer values (positions) and # calculates the position between them. The range is huge as - # the maximum integer value is 2147483647. We are incrementing position by 1000 every time - # when we have enough space. If distance is less then 500 we are calculating an average number + # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time + # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number def position_between(pos_before, pos_after) pos_before ||= MIN_POSITION pos_after ||= MAX_POSITION pos_before, pos_after = [pos_before, pos_after].sort - if pos_after - pos_before < DISTANCE * 2 - (pos_after + pos_before) / 2 + halfway = (pos_after + pos_before) / 2 + distance_to_halfway = pos_after - halfway + + if distance_to_halfway < IDEAL_DISTANCE + halfway else if pos_before == MIN_POSITION - pos_after - DISTANCE + pos_after - IDEAL_DISTANCE elsif pos_after == MAX_POSITION - pos_before + DISTANCE + pos_before + IDEAL_DISTANCE else - (pos_after + pos_before) / 2 + halfway end end end diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index ebbf14fb5ba..255b584a85e 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -66,6 +66,34 @@ describe Issue, 'RelativePositioning' do end end + describe '#shift_after?' do + it 'returns true' do + issue.update(relative_position: issue1.relative_position - 1) + + expect(issue.shift_after?).to be_truthy + end + + it 'returns false' do + issue.update(relative_position: issue1.relative_position - 2) + + expect(issue.shift_after?).to be_falsey + end + end + + describe '#shift_before?' do + it 'returns true' do + issue.update(relative_position: issue1.relative_position + 1) + + expect(issue.shift_before?).to be_truthy + end + + it 'returns false' do + issue.update(relative_position: issue1.relative_position + 2) + + expect(issue.shift_before?).to be_falsey + end + end + describe '#move_between' do it 'positions issue between two other' do new_issue.move_between(issue, issue1) @@ -118,7 +146,7 @@ describe Issue, 'RelativePositioning' do new_issue.move_between(nil, issue1) - expect(new_issue.relative_position).to eq(6000 - RelativePositioning::DISTANCE) + expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE) end it 'positions issue closer to the middle if we are at the very bottom' do @@ -127,7 +155,7 @@ describe Issue, 'RelativePositioning' do new_issue.move_between(issue, nil) - expect(new_issue.relative_position).to eq(6000 + RelativePositioning::DISTANCE) + expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE) end it 'positions issue in the middle of other two if distance is not big enough' do @@ -138,5 +166,39 @@ describe Issue, 'RelativePositioning' do expect(new_issue.relative_position).to eq(250) end + + it 'positions issue in the middle of other two is there is no place' do + issue.update relative_position: 100 + issue1.update relative_position: 101 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position) + end + + it 'uses rebalancing if there is no place' do + issue.update relative_position: 100 + issue1.update relative_position: 101 + issue2 = create(:issue, relative_position: 102, project: project) + new_issue.update relative_position: 103 + + new_issue.move_between(issue1, issue2) + new_issue.save! + + expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) + expect(issue.reload.relative_position).not_to eq(100) + end + + it 'positions issue right if we pass none-sequential parameters' do + issue.update relative_position: 99 + issue1.update relative_position: 101 + issue2 = create(:issue, relative_position: 102, project: project) + new_issue.update relative_position: 103 + + new_issue.move_between(issue, issue2) + new_issue.save! + + expect(new_issue.relative_position).to be(100) + end end end -- cgit v1.2.1 From af8cc2e064bb97a8a1801521735d5403b189bfb5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 20:13:36 +0800 Subject: Use `remove_foreign_key :timelogs, name: '...'` Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9908#note_25324225 --- db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb index 676e18cddd3..a7d4e141a1a 100644 --- a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb +++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb @@ -49,15 +49,8 @@ class AddForeignKeysToTimelogs < ActiveRecord::Migration Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'") Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'") - constraint = - if Gitlab::Database.postgresql? - 'CONSTRAINT' - else - 'FOREIGN KEY' - end - - execute "ALTER TABLE timelogs DROP #{constraint} fk_timelogs_issues_issue_id" - execute "ALTER TABLE timelogs DROP #{constraint} fk_timelogs_merge_requests_merge_request_id" + remove_foreign_key :timelogs, name: 'fk_timelogs_issues_issue_id' + remove_foreign_key :timelogs, name: 'fk_timelogs_merge_requests_merge_request_id' remove_columns :timelogs, :issue_id, :merge_request_id end -- cgit v1.2.1 From 82f61410b65c6044aa630d5f5b27fada207b870e Mon Sep 17 00:00:00 2001 From: Chris Peressini Date: Tue, 14 Mar 2017 12:23:44 +0000 Subject: Change Canccel button class Used to be `btn-default`, change it to `btn-cancel` --- app/views/admin/applications/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index c689b26d6e6..061f8991b11 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -26,4 +26,4 @@ .form-actions = f.submit 'Submit', class: "btn btn-save wide" - = link_to "Cancel", admin_applications_path, class: "btn btn-default" + = link_to "Cancel", admin_applications_path, class: "btn btn-cancel" -- cgit v1.2.1 From 83e36064998f77f40c534bad531b6cea19ec198b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 21:40:58 +0800 Subject: Split to two commands, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9908#note_25331127 --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b1ca61604d5..406a0f3dcad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -227,7 +227,8 @@ rake db:rollback: <<: *use-db <<: *dedicated-runner script: - - bundle exec rake db:rollback db:migrate STEP=120 + - bundle exec rake db:rollback STEP=120 + - bundle exec rake db:migrate rake db:seed_fu: stage: test -- cgit v1.2.1 From 84561349ffa7aa079f5bd371ba51bef02ee8f6df Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Tue, 14 Mar 2017 14:45:38 +0100 Subject: Describe polling with ETag caching --- CONTRIBUTING.md | 5 +++-- doc/development/polling.md | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 doc/development/polling.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a6e3feec4c..ae143c58290 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -403,8 +403,8 @@ There are a few rules to get your merge request accepted: - Avoid repeated polling of endpoints that require a significant amount of overhead - Check for N+1 queries via the SQL log or [`QueryRecorder`](https://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - Avoid repeated access of filesystem -1. If you need polling to support real-time features, consider using this [described long - polling approach](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926). +1. If you need polling to support real-time features, please use + [polling with ETag caching][polling-etag]. 1. Changes after submitting the merge request should be in separate commits (no squashing). If necessary, you will be asked to squash when the review is over, before merging. @@ -547,6 +547,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/ [license-finder-doc]: doc/development/licensing.md [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues +[polling-etag]: https://docs.gitlab.com/ce/development/polling.html [^1]: Specs other than JavaScript specs are considered backend code. Haml changes are considered backend code if they include Ruby code other than just diff --git a/doc/development/polling.md b/doc/development/polling.md new file mode 100644 index 00000000000..a086aca6697 --- /dev/null +++ b/doc/development/polling.md @@ -0,0 +1,41 @@ +# Polling with ETag caching + +Polling for changes (repeatedly asking server if there are any new changes) +introduces high load on a GitLab instance, because it usually requires +executing at least a few SQL queries. This makes scaling large GitLab +instances (like GitLab.com) very difficult so we do not allow adding new +features that require polling and hit the database. + +Instead you should use polling mechanism with ETag caching in Redis. + +## How to use it + +1. Add the path of the endpoint which you want to poll to + `Gitlab::EtagCaching::Middleware`. +1. Implement cache invalidation for the path of your endpoint using + `Gitlab::EtagCaching::Store`. Whenever a resource changes you + have to invalidate the ETag for the path that depends on this + resource. +1. Check that the mechanism works: + - requests should return status code 304 + - there should be no SQL queries logged in `log/development.log` + +## How it works + +1. Whenever a resource changes we generate a random value and store it in + Redis. +1. When a client makes a request we set the `ETag` response header to the value + from Redis. +1. The client caches the response (client-side caching) and sends the ETag as + the `If-None-Modified` header with every subsequent request for the same + resource. +1. If the `If-None-Modified` header matches the current value in Redis we know + that the resource did not change so we can send 304 response immediately, + without querying the database at all. The client's browser will use the + cached response. +1. If the `If-None-Modified` header does not match the current value in Redis + we have to generate a new response, because the resource changed. + +For more information see: +- [RFC 7232](https://tools.ietf.org/html/rfc7232) +- [ETag proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926) -- cgit v1.2.1 From 1cba12d35f0e2bad8b2903a58c883437a701fee4 Mon Sep 17 00:00:00 2001 From: szymon Date: Tue, 14 Mar 2017 14:03:28 +0000 Subject: Update using_docker_images.md --- doc/ci/docker/using_docker_images.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 00787323b6b..f025a7e3496 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -170,13 +170,17 @@ services: ``` When the job is run, `tutum/wordpress` will be started and you will have -access to it from your build container under the hostname `tutum__wordpress`. +access to it from your build container under the hostnames `tutum-wordpress` +(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`. -The alias hostname for the service is made from the image name following these +*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.* + +The alias hostnames for the service are made from the image name following these rules: 1. Everything after `:` is stripped -2. Slash (`/`) is replaced with double underscores (`__`) +2. Slash (`/`) is replaced with double underscores (`__`) - primary alias +3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer ## Configuring services -- cgit v1.2.1 From 283789c50913ec95725e83b4d270151347d205cc Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 14 Mar 2017 12:56:07 +0000 Subject: Fix intermittent spec failures in notify_spec.rb --- spec/mailers/notify_spec.rb | 231 +++++++++++++++++++++++--------------------- 1 file changed, 120 insertions(+), 111 deletions(-) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index b692142713f..e822d7eb348 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -8,6 +8,15 @@ describe Notify do include_context 'gitlab email notification' + def have_referable_subject(referable, reply: false) + prefix = referable.project.name if referable.project + prefix = "Re: #{prefix}" if reply + + suffix = "#{referable.title} (#{referable.to_reference})" + + have_subject [prefix, suffix].compact.join(' | ') + end + context 'for a project' do describe 'items that are assignable, the email' do let(:current_user) { create(:user, email: "current@email.com") } @@ -41,11 +50,11 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue) end it 'contains a link to the new issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end context 'when enabled email_author_in_body' do @@ -55,7 +64,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text issue.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -66,7 +75,7 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Issue link' it 'contains the description' do - is_expected.to have_body_text /#{issue_with_description.description}/ + is_expected.to have_body_text issue_with_description.description end end @@ -87,19 +96,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue, reply: true) end it 'contains the name of the previous assignee' do - is_expected.to have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text previous_assignee.name end it 'contains the name of the new assignee' do - is_expected.to have_body_text /#{assignee.name}/ + is_expected.to have_body_text assignee.name end it 'contains a link to the issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end end @@ -121,15 +130,15 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue, reply: true) end it 'contains the names of the added labels' do - is_expected.to have_body_text /foo, bar, and baz/ + is_expected.to have_body_text 'foo, bar, and baz' end it 'contains a link to the issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end end @@ -150,19 +159,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i + is_expected.to have_referable_subject(issue, reply: true) end it 'contains the new status' do - is_expected.to have_body_text /#{status}/i + is_expected.to have_body_text status end it 'contains the user name' do - is_expected.to have_body_text /#{current_user.name}/i + is_expected.to have_body_text current_user.name end it 'contains a link to the issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text(namespace_project_issue_path project.namespace, project, issue) end end @@ -181,7 +190,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i + is_expected.to have_referable_subject(issue, reply: true) end it 'contains link to new issue' do @@ -191,7 +200,7 @@ describe Notify do end it 'contains a link to the original issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end end end @@ -212,19 +221,19 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request) end it 'contains a link to the new merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path(project.namespace, project, merge_request)}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end it 'contains the source branch for the merge request' do - is_expected.to have_body_text /#{merge_request.source_branch}/ + is_expected.to have_body_text merge_request.source_branch end it 'contains the target branch for the merge request' do - is_expected.to have_body_text /#{merge_request.target_branch}/ + is_expected.to have_body_text merge_request.target_branch end context 'when enabled email_author_in_body' do @@ -234,7 +243,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text merge_request.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -246,7 +255,7 @@ describe Notify do it_behaves_like "an unsubscribeable thread" it 'contains the description' do - is_expected.to have_body_text /#{merge_request_with_description.description}/ + is_expected.to have_body_text merge_request_with_description.description end end @@ -267,19 +276,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the name of the previous assignee' do - is_expected.to have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text previous_assignee.name end it 'contains the name of the new assignee' do - is_expected.to have_body_text /#{assignee.name}/ + is_expected.to have_body_text assignee.name end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end @@ -301,15 +310,15 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the names of the added labels' do - is_expected.to have_body_text /foo, bar, and baz/ + is_expected.to have_body_text 'foo, bar, and baz' end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end @@ -330,19 +339,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/i + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the new status' do - is_expected.to have_body_text /#{status}/i + is_expected.to have_body_text status end it 'contains the user name' do - is_expected.to have_body_text /#{current_user.name}/i + is_expected.to have_body_text current_user.name end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end @@ -363,15 +372,15 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the new status' do - is_expected.to have_body_text /merged/i + is_expected.to have_body_text 'merged' end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end end @@ -387,15 +396,15 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do - is_expected.to have_subject /Project was moved/ + is_expected.to have_subject "#{project.name} | Project was moved" end it 'contains name of project' do - is_expected.to have_body_text /#{project.name_with_namespace}/ + is_expected.to have_body_text project.name_with_namespace end it 'contains new user role' do - is_expected.to have_body_text /#{project.ssh_url_to_repo}/ + is_expected.to have_body_text project.ssh_url_to_repo end end @@ -424,9 +433,9 @@ describe Notify do expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email) is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ - is_expected.to have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project) + is_expected.to have_body_text project_member.human_access end end @@ -451,9 +460,9 @@ describe Notify do expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email) is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ - is_expected.to have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project) + is_expected.to have_body_text project_member.human_access end end end @@ -473,8 +482,8 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url end end @@ -490,9 +499,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.human_access end end @@ -521,10 +530,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.human_access}/ - is_expected.to have_body_text /#{project_member.invite_token}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.human_access + is_expected.to have_body_text project_member.invite_token end end @@ -546,10 +555,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.invite_email}/ - is_expected.to have_body_text /#{invited_user.name}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.invite_email + is_expected.to have_body_text invited_user.name end end @@ -570,9 +579,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.invite_email}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.invite_email end end @@ -598,11 +607,11 @@ describe Notify do end it 'contains the message from the note' do - is_expected.to have_body_text /#{note.note}/ + is_expected.to have_body_text note.note end it 'does not contain note author' do - is_expected.not_to have_body_text /wrote\:/ + is_expected.not_to have_body_text 'wrote:' end context 'when enabled email_author_in_body' do @@ -612,7 +621,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text note.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -632,7 +641,7 @@ describe Notify do it_behaves_like 'a user cannot unsubscribe through footer link' it 'has the correct subject' do - is_expected.to have_subject /Re: #{project.name} | #{commit.title} \(#{commit.short_id}\)/ + is_expected.to have_subject "Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})" end it 'contains a link to the commit' do @@ -655,11 +664,11 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains a link to the merge request note' do - is_expected.to have_body_text /#{note_on_merge_request_path}/ + is_expected.to have_body_text note_on_merge_request_path end end @@ -678,11 +687,11 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue, reply: true) end it 'contains a link to the issue note' do - is_expected.to have_body_text /#{note_on_issue_path}/ + is_expected.to have_body_text note_on_issue_path end end end @@ -698,11 +707,11 @@ describe Notify do let(:note) { create(model, project: project, author: note_author) } it "includes diffs with character-level highlighting" do - is_expected.to have_body_text /}<\/span><\/span>/ + is_expected.to have_body_text '}' end it 'contains a link to the diff file' do - is_expected.to have_body_text /#{note.diff_file.file_path}/ + is_expected.to have_body_text note.diff_file.file_path end it_behaves_like 'it should have Gmail Actions links' @@ -718,11 +727,11 @@ describe Notify do end it 'contains the message from the note' do - is_expected.to have_body_text /#{note.note}/ + is_expected.to have_body_text note.note end it 'does not contain note author' do - is_expected.not_to have_body_text /wrote\:/ + is_expected.not_to have_body_text 'wrote:' end context 'when enabled email_author_in_body' do @@ -732,7 +741,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text note.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -777,9 +786,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Request to join the #{group.name} group" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group_group_members_url(group)}/ - is_expected.to have_body_text /#{group_member.human_access}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group_group_members_url(group) + is_expected.to have_body_text group_member.human_access end end @@ -798,8 +807,8 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{group.name} group was denied" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url end end @@ -816,9 +825,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{group.name} group was granted" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.human_access}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.human_access end end @@ -847,10 +856,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{group.name} group" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.human_access}/ - is_expected.to have_body_text /#{group_member.invite_token}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.human_access + is_expected.to have_body_text group_member.invite_token end end @@ -872,10 +881,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.invite_email}/ - is_expected.to have_body_text /#{invited_user.name}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.invite_email + is_expected.to have_body_text invited_user.name end end @@ -896,9 +905,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.invite_email}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.invite_email end end end @@ -925,11 +934,11 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /^Confirmation instructions/ + is_expected.to have_subject 'Confirmation instructions | A Nice Suffix' end it 'includes a link to the site' do - is_expected.to have_body_text /#{example_site_path}/ + is_expected.to have_body_text example_site_path end end @@ -952,11 +961,11 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Pushed new branch master/ + is_expected.to have_subject "[Git][#{project.full_path}] Pushed new branch master" end it 'contains a link to the branch' do - is_expected.to have_body_text /#{tree_path}/ + is_expected.to have_body_text tree_path end end @@ -979,11 +988,11 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Pushed new tag v1\.0/ + is_expected.to have_subject "[Git][#{project.full_path}] Pushed new tag v1.0" end it 'contains a link to the tag' do - is_expected.to have_body_text /#{tree_path}/ + is_expected.to have_body_text tree_path end end @@ -1005,7 +1014,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Deleted branch master/ + is_expected.to have_subject "[Git][#{project.full_path}] Deleted branch master" end end @@ -1027,7 +1036,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Deleted tag v1\.0/ + is_expected.to have_subject "[Git][#{project.full_path}] Deleted tag v1.0" end end @@ -1055,23 +1064,23 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/ + is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.length} commits: Ruby files modified" end it 'includes commits list' do - is_expected.to have_body_text /Change some files/ + is_expected.to have_body_text 'Change some files' end it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text /def<\/span> archive_formats_regex/ + is_expected.to have_body_text 'def archive_formats_regex' end it 'contains a link to the diff' do - is_expected.to have_body_text /#{diff_path}/ + is_expected.to have_body_text diff_path end it 'does not contain the misleading footer' do - is_expected.not_to have_body_text /you are a member of/ + is_expected.not_to have_body_text 'you are a member of' end context "when set to send from committer email if domain matches" do @@ -1157,19 +1166,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{commits.first.title}/ + is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.first.title}" end it 'includes commits list' do - is_expected.to have_body_text /Change some files/ + is_expected.to have_body_text 'Change some files' end it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text /def<\/span> archive_formats_regex/ + is_expected.to have_body_text 'def archive_formats_regex' end it 'contains a link to the diff' do - is_expected.to have_body_text /#{diff_path}/ + is_expected.to have_body_text diff_path end end -- cgit v1.2.1 From 29e34c332687be9456578a9b5f60adb10f4e10b5 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 7 Mar 2017 18:04:44 +0200 Subject: Preserve order by priority on issues board --- app/models/issue.rb | 7 +++++++ app/services/boards/issues/list_service.rb | 2 +- spec/models/issue_spec.rb | 15 ++++++++++++++ spec/services/boards/issues/list_service_spec.rb | 26 ++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index 0f7a26ee3e1..dba9398a43c 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -96,6 +96,13 @@ class Issue < ActiveRecord::Base end end + def self.order_by_position_and_priority + order_labels_priority. + reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'), + Gitlab::Database.nulls_last_order('highest_priority', 'ASC'), + "id DESC") + end + # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 185838764c1..83f51947bd4 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -5,7 +5,7 @@ module Boards issues = IssuesFinder.new(current_user, filter_params).execute issues = without_board_labels(issues) unless movable_list? issues = with_list_label(issues) if movable_list? - issues.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC')) + issues.order_by_position_and_priority end private diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index bba9058f394..f67fbe79bde 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,6 +22,21 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end + describe '#order_by_position_and_priority' do + let(:project) { create :empty_project } + let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } + let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } + let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) } + let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) } + let!(:issue3) { create(:issue, project: project, relative_position: 100) } + let!(:issue4) { create(:issue, project: project, relative_position: 200) } + + it 'returns ordered list' do + expect(project.issues.order_by_position_and_priority). + to match [issue3, issue4, issue1, issue2] + end + end + describe '#to_reference' do let(:namespace) { build(:namespace, path: 'sample-namespace') } let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) } diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 01baedc4761..22115c6566d 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -43,6 +43,32 @@ describe Boards::Issues::ListService, services: true do described_class.new(project, user, params).execute end + context 'issues are ordered by priority' do + it 'returns opened issues when list_id is missing' do + params = { board_id: board.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] + end + + it 'returns closed issues when listing issues from Done' do + params = { board_id: board.id, id: done.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] + end + + it 'returns opened issues that have label list applied when listing issues from a label list' do + params = { board_id: board.id, id: list1.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] + end + end + context 'with list that does not belong to the board' do it 'raises an error' do list = create(:list) -- cgit v1.2.1 From 3e29936a15eb2f6c9bc2d92ebf1a67a0cadb916e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 14 Mar 2017 01:24:56 -0500 Subject: Fix first line markdown helper for user profile activity stream Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/29425 --- app/helpers/events_helper.rb | 4 ++-- app/helpers/gitlab_markdown_helper.rb | 2 +- spec/helpers/gitlab_markdown_helper_spec.rb | 20 ++++++++++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 5605393c0c3..fb872a13f74 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -165,8 +165,8 @@ module EventsHelper sanitize( text, - tags: %w(a img b pre code p span), - attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style'] + tags: %w(a img gl-emoji b pre code p span), + attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-name', 'data-unicode-version'] ) end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 6d365ea9251..6226cfe25cf 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -172,7 +172,7 @@ module GitlabMarkdownHelper # text hasn't already been truncated, then append "..." to the node contents # and return true. Otherwise return false. def truncate_if_block(node, truncated) - if node.element? && node.description.block? && !truncated + if node.element? && node.description&.block? && !truncated node.inner_html = "#{node.inner_html}..." if node.next_sibling true else diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 9ffd4b9371c..6cf3f86680a 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -152,9 +152,8 @@ describe GitlabMarkdownHelper do end describe '#first_line_in_markdown' do - let(:text) { "@#{user.username}, can you look at this?\nHello world\n"} - it 'truncates Markdown properly' do + text = "@#{user.username}, can you look at this?\nHello world\n" actual = first_line_in_markdown(text, 100, project: project) doc = Nokogiri::HTML.parse(actual) @@ -169,6 +168,23 @@ describe GitlabMarkdownHelper do expect(doc.content).to eq "@#{user.username}, can you look at this?..." end + + it 'truncates Markdown with emoji properly' do + text = "foo :wink:\nbar :grinning:" + actual = first_line_in_markdown(text, 100, project: project) + + doc = Nokogiri::HTML.parse(actual) + + # Make sure we didn't create invalid markup + # But also account for the 2 errors caused by the unknown `gl-emoji` elements + expect(doc.errors.length).to eq(2) + + expect(doc.css('gl-emoji').length).to eq(2) + expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink' + expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning' + + expect(doc.content).to eq "foo 😉\nbar 😀" + end end describe '#cross_project_reference' do -- cgit v1.2.1 From 8daa641e4d6d5dd2fab78abd851b0e8899d4ea11 Mon Sep 17 00:00:00 2001 From: Raveesh Date: Tue, 14 Mar 2017 12:50:32 -0400 Subject: Switch to using milestone.to_reference when displaying milestone Fix #29214 --- app/views/projects/milestones/edit.html.haml | 2 +- app/views/projects/milestones/show.html.haml | 2 +- changelogs/unreleased/fix-milestone-name-on-show.yml | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/fix-milestone-name-on-show.yml diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index 11f41e75e63..55b0b837c6d 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -5,7 +5,7 @@ %div{ class: container_class } %h3.page-title - Edit Milestone ##{@milestone.iid} + Edit Milestone #{@milestone.to_reference} %hr diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index b4dde2c86c9..d16f49bd33a 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -20,7 +20,7 @@ .header-text-content %span.identifier %strong - Milestone %#{@milestone.iid} + Milestone #{@milestone.to_reference} - if @milestone.due_date || @milestone.start_date = milestone_date_range(@milestone) .milestone-buttons diff --git a/changelogs/unreleased/fix-milestone-name-on-show.yml b/changelogs/unreleased/fix-milestone-name-on-show.yml new file mode 100644 index 00000000000..bf17a758c80 --- /dev/null +++ b/changelogs/unreleased/fix-milestone-name-on-show.yml @@ -0,0 +1,4 @@ +--- +title: Fix Milestone name on show page +merge_request: +author: Raveesh -- cgit v1.2.1 From f97c1d1001a1c16ab51fb62723f30f6ffa467d4f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 14 Mar 2017 11:47:05 -0500 Subject: Fix link togglers jumping to top Fix #29414 --- app/assets/javascripts/behaviors/toggler_behavior.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 0726c6c9636..92f3bb3ff52 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -21,8 +21,13 @@ // %a.js-toggle-button // %div.js-toggle-content // - $('body').on('click', '.js-toggle-button', function() { + $('body').on('click', '.js-toggle-button', function(e) { toggleContainer($(this).closest('.js-toggle-container')); + + const targetTag = e.target.tagName.toLowerCase(); + if (targetTag === 'a' || targetTag === 'button') { + e.preventDefault(); + } }); // If we're accessing a permalink, ensure it is not inside a -- cgit v1.2.1 From 74ec81a4f3ba3a98946e00fd08bd72567e338271 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 14 Mar 2017 11:25:24 +0100 Subject: Bump pages daemon to 0.4.0 [ci skip] --- doc/administration/pages/source.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index b4588f8b43c..a45c3306457 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -105,7 +105,7 @@ The Pages daemon doesn't listen to the outside world. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.3.2 + sudo -u git -H git checkout v$( Date: Tue, 14 Mar 2017 11:56:15 -0500 Subject: Include time tracking attributes in webhooks payload --- app/models/issue.rb | 8 +++++++- app/models/merge_request.rb | 5 ++++- .../unreleased/27271-missing-time-spent-in-issue-webhook.yml | 4 ++++ spec/models/issue_spec.rb | 11 +++++++++++ spec/models/merge_request_spec.rb | 6 +++++- 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml diff --git a/app/models/issue.rb b/app/models/issue.rb index 0f7a26ee3e1..2cc237635f9 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -58,7 +58,13 @@ class Issue < ActiveRecord::Base end def hook_attrs - attributes + attrs = { + total_time_spent: total_time_spent, + human_total_time_spent: human_total_time_spent, + human_time_estimate: human_time_estimate + } + + attributes.merge!(attrs) end def self.reference_prefix diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0f7b8311588..4759829a15c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -523,7 +523,10 @@ class MergeRequest < ActiveRecord::Base source: source_project.try(:hook_attrs), target: target_project.hook_attrs, last_commit: nil, - work_in_progress: work_in_progress? + work_in_progress: work_in_progress?, + total_time_spent: total_time_spent, + human_total_time_spent: human_total_time_spent, + human_time_estimate: human_time_estimate } if diff_head_commit diff --git a/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml new file mode 100644 index 00000000000..4ea52a70e89 --- /dev/null +++ b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml @@ -0,0 +1,4 @@ +--- +title: Include time tracking attributes in webhooks payload +merge_request: 9942 +author: diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index bba9058f394..898a9c8da35 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -620,4 +620,15 @@ describe Issue, models: true do end end end + + describe '#hook_attrs' do + let(:attrs_hash) { subject.hook_attrs } + + it 'includes time tracking attrs' do + expect(attrs_hash).to include(:total_time_spent) + expect(attrs_hash).to include(:human_time_estimate) + expect(attrs_hash).to include(:human_total_time_spent) + expect(attrs_hash).to include('time_estimate') + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index fcaf4c71182..24e7c1b17d9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -542,7 +542,7 @@ describe MergeRequest, models: true do end describe "#hook_attrs" do - let(:attrs_hash) { subject.hook_attrs.to_h } + let(:attrs_hash) { subject.hook_attrs } [:source, :target].each do |key| describe "#{key} key" do @@ -558,6 +558,10 @@ describe MergeRequest, models: true do expect(attrs_hash).to include(:target) expect(attrs_hash).to include(:last_commit) expect(attrs_hash).to include(:work_in_progress) + expect(attrs_hash).to include(:total_time_spent) + expect(attrs_hash).to include(:human_time_estimate) + expect(attrs_hash).to include(:human_total_time_spent) + expect(attrs_hash).to include('time_estimate') end end -- cgit v1.2.1 From c9abdadd7a08f972d5a12472f9f5ac443e37a6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 14 Mar 2017 18:08:50 +0100 Subject: Ensure dots in project path is allowed in the commits API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/api/commits.rb | 2 +- lib/api/v3/commits.rb | 2 +- spec/requests/api/commits_spec.rb | 17 +++++++++-------- spec/requests/api/v3/commits_spec.rb | 15 ++++++++------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 42401abfe0f..48939798900 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -10,7 +10,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: /.+/ } do desc 'Get a project repository commits' do success Entities::RepoCommit end diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index d254d247042..6f36b2bc1c4 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -11,7 +11,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: /.+/ } do desc 'Get a project repository commits' do success ::API::Entities::RepoCommit end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 585449e62b6..7c0f2fb9fe9 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -178,7 +178,7 @@ describe API::Commits, api: true do end end - describe "Create a commit with multiple files and actions" do + describe "POST /projects/:id/repository/commits" do let!(:url) { "/projects/#{project.id}/repository/commits" } it 'returns a 403 unauthorized for user without permissions' do @@ -193,7 +193,7 @@ describe API::Commits, api: true do expect(response).to have_http_status(400) end - context :create do + describe 'create' do let(:message) { 'Created file' } let!(:invalid_c_params) do { @@ -237,8 +237,9 @@ describe API::Commits, api: true do 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" } + context 'with project path containing a dot in URL' do + let!(:user) { create(:user, username: 'foo.bar') } + let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } it 'a new file in project repo' do post api(url, user), valid_c_params @@ -248,7 +249,7 @@ describe API::Commits, api: true do end end - context :delete do + describe 'delete' do let(:message) { 'Deleted file' } let!(:invalid_d_params) do { @@ -289,7 +290,7 @@ describe API::Commits, api: true do end end - context :move do + describe 'move' do let(:message) { 'Moved file' } let!(:invalid_m_params) do { @@ -334,7 +335,7 @@ describe API::Commits, api: true do end end - context :update do + describe 'update' do let(:message) { 'Updated file' } let!(:invalid_u_params) do { @@ -377,7 +378,7 @@ describe API::Commits, api: true do end end - context "multiple operations" do + describe 'multiple operations' do let(:message) { 'Multiple actions' } let!(:invalid_mo_params) do { diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index e298ef055e1..adba3a787aa 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -88,7 +88,7 @@ describe API::V3::Commits, api: true do end end - describe "Create a commit with multiple files and actions" do + describe "POST /projects/:id/repository/commits" do let!(:url) { "/projects/#{project.id}/repository/commits" } it 'returns a 403 unauthorized for user without permissions' do @@ -103,7 +103,7 @@ describe API::V3::Commits, api: true do expect(response).to have_http_status(400) end - context :create do + describe 'create' do let(:message) { 'Created file' } let!(:invalid_c_params) do { @@ -147,8 +147,9 @@ describe API::V3::Commits, api: true do 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" } + context 'with project path containing a dot in URL' do + let!(:user) { create(:user, username: 'foo.bar') } + let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } it 'a new file in project repo' do post v3_api(url, user), valid_c_params @@ -158,7 +159,7 @@ describe API::V3::Commits, api: true do end end - context :delete do + describe 'delete' do let(:message) { 'Deleted file' } let!(:invalid_d_params) do { @@ -199,7 +200,7 @@ describe API::V3::Commits, api: true do end end - context :move do + describe 'move' do let(:message) { 'Moved file' } let!(:invalid_m_params) do { @@ -244,7 +245,7 @@ describe API::V3::Commits, api: true do end end - context :update do + describe 'update' do let(:message) { 'Updated file' } let!(:invalid_u_params) do { -- cgit v1.2.1 From 316e0edc5ae0ecec17aa7e15b3c8b0743a216195 Mon Sep 17 00:00:00 2001 From: gpongelli Date: Tue, 14 Mar 2017 17:14:35 +0000 Subject: Syshook documentation updated --- doc/system_hooks/system_hooks.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index ec13c2446ef..ad5ffc84473 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -313,8 +313,19 @@ X-Gitlab-Event: System Hook "git_ssh_url":"git@example.com:mike/diaspora.git", "visibility_level":0 }, - "commits": [], - "total_commits_count": 0 + "commits": [ + { + "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "message": "Add simple search to projects in public area", + "timestamp": "2013-05-13T18:18:08+00:00", + "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + } + ], + "total_commits_count": 1 } ``` -- cgit v1.2.1 From 464ca33747ec3f2a4063795e18ab5888e429b334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 14 Mar 2017 18:30:53 +0100 Subject: Allow to override GITLAB_GIT_TEST_REPO_URL to specify a different gitlab-git-test repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We will set this to the dev mirror in GitLab CE and EE on dev. Signed-off-by: Rémy Coutable --- spec/lib/gitlab/git/repository_spec.rb | 2 +- spec/support/seed_helper.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index bc139d5ef28..9c3a4571ce4 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -507,7 +507,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#remote_add" do before(:all) do @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) - @repo.remote_add("new_remote", SeedHelper::GITLAB_URL) + @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL) end it "should add the remote" do diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 07f81e9c4f3..f55fee28ff9 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -7,7 +7,7 @@ TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git") TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git") module SeedHelper - GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git".freeze + GITLAB_GIT_TEST_REPO_URL = ENV.fetch('GITLAB_GIT_TEST_REPO_URL', 'https://gitlab.com/gitlab-org/gitlab-git-test.git').freeze def ensure_seeds if File.exist?(SEED_REPOSITORY_PATH) @@ -25,7 +25,7 @@ module SeedHelper end def create_bare_seeds - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}), chdir: SEED_REPOSITORY_PATH, out: '/dev/null', err: '/dev/null') @@ -45,7 +45,7 @@ module SeedHelper system(git_env, *%w(git branch -t feature origin/feature), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') - system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_GIT_TEST_REPO_URL}), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') end -- cgit v1.2.1 From ee2ddd059520f2c9a875c888a2c4eb44af3643a5 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 10 Mar 2017 18:02:35 -0600 Subject: Moved the gear settings dropdown in the group view to a tab --- app/views/groups/_settings_head.html.haml | 14 ++++++++++++++ app/views/groups/edit.html.haml | 1 + app/views/groups/projects.html.haml | 1 + app/views/layouts/nav/_group.html.haml | 9 ++++++++- app/views/layouts/nav/_group_settings.html.haml | 18 ------------------ .../unreleased/group-gear-setting-dropdown-to-tab.yml | 4 ++++ 6 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 app/views/groups/_settings_head.html.haml delete mode 100644 app/views/layouts/nav/_group_settings.html.haml create mode 100644 changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml new file mode 100644 index 00000000000..dc11efeb0c4 --- /dev/null +++ b/app/views/groups/_settings_head.html.haml @@ -0,0 +1,14 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: container_class } + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects' do + %span + Projects + + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'Edit Group' do + %span + Edit Group \ No newline at end of file diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 2706e8692d1..80a77dab97f 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,3 +1,4 @@ += render "groups/settings_head" .panel.panel-default.prepend-top-default .panel-heading Group settings diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 2e7e5e5c309..1f4a3e2a829 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,4 +1,5 @@ - page_title "Projects" += render "groups/settings_head" .panel.panel-default.prepend-top-default .panel-heading diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index a6e96942021..9de0e344196 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,4 +1,5 @@ -= render 'layouts/nav/group_settings' +- can_admin_group = can?(current_user, :admin_group, @group) +- can_edit = can?(current_user, :admin_group, @group) .scrolling-tabs-container{ class: nav_control_class } .fade-left = icon('angle-left') @@ -25,3 +26,9 @@ = link_to group_group_members_path(@group), title: 'Members' do %span Members + - if current_user + - if can_admin_group || can_edit + = nav_link(path: %w[groups#projects groups#edit]) do + = link_to projects_group_path(@group), title: 'Settings' do + %span + Settings diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml deleted file mode 100644 index 30feb6813b4..00000000000 --- a/app/views/layouts/nav/_group_settings.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- if current_user - - can_admin_group = can?(current_user, :admin_group, @group) - - can_edit = can?(current_user, :admin_group, @group) - - - if can_admin_group || can_edit - .controls - .dropdown.group-settings-dropdown - %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' } - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - if can_admin_group - = nav_link(path: 'groups#projects') do - = link_to 'Projects', projects_group_path(@group), title: 'Projects' - - if can_edit && can_admin_group - %li.divider - %li - = link_to 'Edit Group', edit_group_path(@group) diff --git a/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml new file mode 100644 index 00000000000..aff1bdd957c --- /dev/null +++ b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml @@ -0,0 +1,4 @@ +--- +title: Moved the gear settings dropdown to a tab in the groups view +merge_request: +author: -- cgit v1.2.1 From f47946591a52536c7dd7d02d11ffb7390549470b Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 10 Mar 2017 18:40:33 -0600 Subject: Fixed haml_lint warning for the settings_head partial --- app/views/groups/_settings_head.html.haml | 2 +- app/views/groups/projects.html.haml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml index dc11efeb0c4..d225f7ed3c0 100644 --- a/app/views/groups/_settings_head.html.haml +++ b/app/views/groups/_settings_head.html.haml @@ -11,4 +11,4 @@ = nav_link(path: 'groups#edit') do = link_to edit_group_path(@group), title: 'Edit Group' do %span - Edit Group \ No newline at end of file + Edit Group diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 1f4a3e2a829..83bdd654f27 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,4 +1,3 @@ -- page_title "Projects" = render "groups/settings_head" .panel.panel-default.prepend-top-default -- cgit v1.2.1 From 30f99608ffa5a4ce3d403276df5d68a23ec9b338 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Tue, 14 Mar 2017 12:00:00 -0600 Subject: Fixed some missing permission conditions --- app/views/groups/_settings_head.html.haml | 11 +++++++---- app/views/layouts/nav/_group.html.haml | 12 +++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml index d225f7ed3c0..d99426bc2c1 100644 --- a/app/views/groups/_settings_head.html.haml +++ b/app/views/groups/_settings_head.html.haml @@ -1,3 +1,5 @@ +- can_admin_group = can?(current_user, :admin_group, @group) +- can_edit = can?(current_user, :admin_group, @group) = content_for :sub_nav do .scrolling-tabs-container.sub-nav-scroll = render 'shared/nav_scroll' @@ -8,7 +10,8 @@ %span Projects - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'Edit Group' do - %span - Edit Group + - if can_edit && can_admin_group + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'Edit Group' do + %span + Edit Group diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 9de0e344196..b2ecf6504e0 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,5 +1,4 @@ - can_admin_group = can?(current_user, :admin_group, @group) -- can_edit = can?(current_user, :admin_group, @group) .scrolling-tabs-container{ class: nav_control_class } .fade-left = icon('angle-left') @@ -26,9 +25,8 @@ = link_to group_group_members_path(@group), title: 'Members' do %span Members - - if current_user - - if can_admin_group || can_edit - = nav_link(path: %w[groups#projects groups#edit]) do - = link_to projects_group_path(@group), title: 'Settings' do - %span - Settings + - if current_user && can_admin_group + = nav_link(path: %w[groups#projects groups#edit]) do + = link_to projects_group_path(@group), title: 'Settings' do + %span + Settings -- cgit v1.2.1 From bb99fc2572a796b938bd67128f6482c180e3942b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 7 Mar 2017 21:32:36 +0100 Subject: Add nested groups documentation [ci skip] --- doc/user/group/subgroups/img/create_new_group.png | Bin 0 -> 18503 bytes .../group/subgroups/img/create_subgroup_button.png | Bin 0 -> 8402 bytes doc/user/group/subgroups/img/group_members.png | Bin 0 -> 48240 bytes doc/user/group/subgroups/img/mention_subgroups.png | Bin 0 -> 39666 bytes doc/user/group/subgroups/index.md | 153 +++++++++++++++++++++ doc/user/permissions.md | 1 + doc/workflow/README.md | 1 + 7 files changed, 155 insertions(+) create mode 100644 doc/user/group/subgroups/img/create_new_group.png create mode 100644 doc/user/group/subgroups/img/create_subgroup_button.png create mode 100644 doc/user/group/subgroups/img/group_members.png create mode 100644 doc/user/group/subgroups/img/mention_subgroups.png create mode 100644 doc/user/group/subgroups/index.md diff --git a/doc/user/group/subgroups/img/create_new_group.png b/doc/user/group/subgroups/img/create_new_group.png new file mode 100644 index 00000000000..9d011ec709a Binary files /dev/null and b/doc/user/group/subgroups/img/create_new_group.png differ diff --git a/doc/user/group/subgroups/img/create_subgroup_button.png b/doc/user/group/subgroups/img/create_subgroup_button.png new file mode 100644 index 00000000000..000b54c2855 Binary files /dev/null and b/doc/user/group/subgroups/img/create_subgroup_button.png differ diff --git a/doc/user/group/subgroups/img/group_members.png b/doc/user/group/subgroups/img/group_members.png new file mode 100644 index 00000000000..b95fe6263bf Binary files /dev/null and b/doc/user/group/subgroups/img/group_members.png differ diff --git a/doc/user/group/subgroups/img/mention_subgroups.png b/doc/user/group/subgroups/img/mention_subgroups.png new file mode 100644 index 00000000000..8e6bed0111b Binary files /dev/null and b/doc/user/group/subgroups/img/mention_subgroups.png differ diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md new file mode 100644 index 00000000000..9522e6fc4ba --- /dev/null +++ b/doc/user/group/subgroups/index.md @@ -0,0 +1,153 @@ +# Subgroups + +> [Introduced][ce-2772] in GitLab 9.0. + +With subgroups (also called nested groups or hierarchical groups) you can have +up to 20 levels of nested groups, which among other things can help you to: + +- **Separate internal / external organizations.** Since every group + can have its own visibility level, you are able to host groups for different + purposes under the same umbrella. +- **Organize large projects.** For large projects, subgroups makes it + potentially easier to separate permissions on parts of the source code. +- **Make it easier to manage people and control visibility.** Give people + different [permissions][] depending on their group [membership](#membership). + +## Overview + +A group can have many subgroups inside it, and at the same time a group can have +only 1 parent group. It resembles a directory behavior, like the one below: + +``` +group0 +└── subgroup01a +└── subgroup01b + └── subgroup02 + └── subgroup03 +``` + +In a real world example, imagine maintaining a GNU/Linux distribution with the +first group being the name of the distro and subsequent groups split like: + +``` +Organization Group - GNU/Linux distro + └── Category Subgroup - Packages + └── project - Package01 + └── project - Package02 + └── Category Subgroup - Software + └── project - Core + └── project - CLI + └── project - Android app + └── project - iOS app + └── Category Subgroup - Infra tools + └── project - Ansible playbooks +``` + +Another example of GitLab as a company would be the following: + +``` +Organization Group - GitLab + └── Category Subroup - Marketing + └── project - Design + └── project - General + └── Category Subgroup - Software + └── project - GitLab CE + └── project - GitLab EE + └── project - Omnibus GitLab + └── project - GitLab Runner + └── project - GitLab Pages daemon + └── Category Subgroup - Infra tools + └── project - Chef cookbooks + └── Category Subgroup - Executive team +``` + +--- + +The maximum nested groups a group can have, including the first one in the +hierarchy, is 21. + +Things like transferring or importing a project inside nested groups, work like +when performing these actions the traditional way with the `group/project` +structure. + +## Creating a subgroup + +>**Notes:** +- You need to be an Owner of a group in order to be able to create + a subgroup. For more information check the [permissions table][permissions]. +- For a list of words that are not allowed to be used as group names see the + [`namespace_validator.rb` file][reserved] under the `RESERVED` and + `WILDCARD_ROUTES` lists. + +To create a subgroup: + +1. In the group's dashboard go to the **Subgroups** page and click **Create subgroup**. + + ![Subgroups page](img/create_subgroup_button.png) + +1. Create a new group like you would normally do. Notice that the parent group + namespace is fixed under **Group path**. The visibility level can differ from + the parent group. + + ![Subgroups page](img/create_new_group.png) + +1. Click the **Create group** button and you will be taken to the new group's + dashboard page. + +--- + +You can follow the same process to create any subsequent groups. + +## Membership + +When you add a member to a subgroup, they inherit the membership and permission +level from the parent group. This model allows access to nested groups if you +have membership in one of its parents. + +You can tell if a member has inherited the permissions from a parent group by +looking at the group's **Members** page. + +![Group members page](img/group_members.png) + +From the image above, we can deduct the following things: + +- There are 5 members that have access to the group **four** +- Administrator is the Owner and member of all subgroups +- User0 is a Reporter and has inherited their permissions from group **one** + which is above the hierarchy of group **four** +- User1 is a Developer and has inherited their permissions from group + **one/two** which is above the hierarchy of group **four** +- User2 is a Developer and has inherited their permissions from group + **one/two/three** which is above the hierarchy of group **four** +- User3 is a Master of group **four**, there is no indication of a parent + group therefore they belong to group **four** + +The group permissions for a member can be changed only by Owners and only on +the **Members** page of the group the member was added. + +## Mentioning subgroups + +Mentioning groups (`@group`) in issues, commits and merge requests, would +mention all members of that group. Now with subgroups, there is a more granular +support if you want to split your group's structure. Mentioning works as before +and you can choose the group of people to be summoned. + +![Mentioning subgroups](img/mention_subgroups.png) + +## Limitations + +Here's a list of what you can't do with subgroups: + +- [GitLab Pages](../../project/pages/index.md) are not currently working for + projects hosted under a subgroup. That means that only projects hosted under + the first parent group will work. +- Group level labels don't work in subgroups / sub projects +- It is not possible to share a project with a group that's an ancestor of + the group the project is in. That means you can only share as you walk down + the hierarchy. For example, `group/subgroup01/project` **cannot** be shared + with `group`, but can be shared with `group/subgroup02` or + `group/subgroup01/subgroup03`. + +[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772 +[permissions]: ../../permissions.md#group +[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/namespace_validator.rb diff --git a/doc/user/permissions.md b/doc/user/permissions.md index b49a244160a..0ea6d01411f 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -81,6 +81,7 @@ group. |-------------------------|-------|----------|-----------|--------|-------| | Browse group | ✓ | ✓ | ✓ | ✓ | ✓ | | Edit group | | | | | ✓ | +| Create subgroup | | | | | ✓ | | Create project in group | | | | ✓ | ✓ | | Manage group members | | | | | ✓ | | Remove group | | | | | ✓ | diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 9e7ee47387c..a286a23765d 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -40,3 +40,4 @@ - [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md) - [Todos](todos.md) - [Snippets](../user/snippets.md) +- [Nested groups](../user/group/subgroups/index.md) -- cgit v1.2.1 From 1913f1ed9efced37cc597515ea4c7219eb17b4be Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 10 Mar 2017 14:15:28 +0100 Subject: Add info on group membership [ci skip] --- doc/user/group/subgroups/index.md | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 9522e6fc4ba..2338d8e9b42 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -104,6 +104,9 @@ When you add a member to a subgroup, they inherit the membership and permission level from the parent group. This model allows access to nested groups if you have membership in one of its parents. +The group permissions for a member can be changed only by Owners and only on +the **Members** page of the group the member was added. + You can tell if a member has inherited the permissions from a parent group by looking at the group's **Members** page. @@ -111,19 +114,35 @@ looking at the group's **Members** page. From the image above, we can deduct the following things: -- There are 5 members that have access to the group **four** -- Administrator is the Owner and member of all subgroups -- User0 is a Reporter and has inherited their permissions from group **one** - which is above the hierarchy of group **four** +- There are 5 members that have access to the group `four` +- User0 is a Reporter and has inherited their permissions from group `one` + which is above the hierarchy of group `four` - User1 is a Developer and has inherited their permissions from group - **one/two** which is above the hierarchy of group **four** + `one/two` which is above the hierarchy of group `four` - User2 is a Developer and has inherited their permissions from group - **one/two/three** which is above the hierarchy of group **four** -- User3 is a Master of group **four**, there is no indication of a parent - group therefore they belong to group **four** + `one/two/three` which is above the hierarchy of group `four` +- For User3 there is no indication of a parent group, therefore they belong to + group `four`, the one we're inspecting +- Administrator is the Owner and member of **all** subgroups and for that reason, + same as User3, there is no indication of an ancestor group -The group permissions for a member can be changed only by Owners and only on -the **Members** page of the group the member was added. +### Overriding the ancestor group membership + +>**Note:** +You need to be an Owner of a group in order to be able to add members to it. + +To override the membership of an ancestor group, simply add the user in the new +subgroup again, but with different permissions. + +For example, if User0 was first added to group `one/two` with Developer +permissions, then they will inherit those permissions in every other subgroup +of `one/two`. To give them Master access to `one/two/three`, you would add them +again in that group as Master. Removing them from that group, the permissions +will fallback to those of the ancestor group. + +Note that the higher permission wins, so if in the above example the permissions +where reversed, User0 would have Master access to all groups, even to the one +that was explicitly given Developer access. ## Mentioning subgroups -- cgit v1.2.1 From f35d7a16595c199add9a582b8a80f8da75e5544d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 10 Mar 2017 14:18:48 +0100 Subject: Fix wording [ci skip] --- doc/user/group/subgroups/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 2338d8e9b42..ff28f458f99 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -147,9 +147,9 @@ that was explicitly given Developer access. ## Mentioning subgroups Mentioning groups (`@group`) in issues, commits and merge requests, would -mention all members of that group. Now with subgroups, there is a more granular +notify all members of that group. Now with subgroups, there is a more granular support if you want to split your group's structure. Mentioning works as before -and you can choose the group of people to be summoned. +and you can choose the group of people to be notified. ![Mentioning subgroups](img/mention_subgroups.png) -- cgit v1.2.1 From b5142f92a03c1b2a58ee583e2d0632150e74d45a Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 14 Mar 2017 21:41:46 +0100 Subject: Address subgroups docs review [ci skip] --- doc/user/group/subgroups/index.md | 86 ++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index ff28f458f99..ce5da07c61a 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -2,7 +2,7 @@ > [Introduced][ce-2772] in GitLab 9.0. -With subgroups (also called nested groups or hierarchical groups) you can have +With subgroups (aka nested groups or hierarchical groups) you can have up to 20 levels of nested groups, which among other things can help you to: - **Separate internal / external organizations.** Since every group @@ -16,50 +16,45 @@ up to 20 levels of nested groups, which among other things can help you to: ## Overview A group can have many subgroups inside it, and at the same time a group can have -only 1 parent group. It resembles a directory behavior, like the one below: +only 1 parent group. It resembles a directory behavior or a nested items list: -``` -group0 -└── subgroup01a -└── subgroup01b - └── subgroup02 - └── subgroup03 -``` +- Group 1 + - Group 1.1 + - Group 1.2 + - Group 1.2.1 + - Group 1.2.2 + - Group 1.2.2.1 In a real world example, imagine maintaining a GNU/Linux distribution with the first group being the name of the distro and subsequent groups split like: -``` -Organization Group - GNU/Linux distro - └── Category Subgroup - Packages - └── project - Package01 - └── project - Package02 - └── Category Subgroup - Software - └── project - Core - └── project - CLI - └── project - Android app - └── project - iOS app - └── Category Subgroup - Infra tools - └── project - Ansible playbooks -``` +- Organization Group - GNU/Linux distro + - Category Subgroup - Packages + - (project) Package01 + - (project) Package02 + - Category Subgroup - Software + - (project) Core + - (project) CLI + - (project) Android app + - (project) iOS app + - Category Subgroup - Infra tools + - (project) Ansible playbooks Another example of GitLab as a company would be the following: -``` -Organization Group - GitLab - └── Category Subroup - Marketing - └── project - Design - └── project - General - └── Category Subgroup - Software - └── project - GitLab CE - └── project - GitLab EE - └── project - Omnibus GitLab - └── project - GitLab Runner - └── project - GitLab Pages daemon - └── Category Subgroup - Infra tools - └── project - Chef cookbooks - └── Category Subgroup - Executive team -``` +- Organization Group - GitLab + - Category Subroup - Marketing + - (project) Design + - (project) General + - Category Subgroup - Software + - (project) GitLab CE + - (project) GitLab EE + - (project) Omnibus GitLab + - (project) GitLab Runner + - (project) GitLab Pages daemon + - Category Subgroup - Infra tools + - (project) Chef cookbooks + - Category Subgroup - Executive team --- @@ -131,18 +126,15 @@ From the image above, we can deduct the following things: >**Note:** You need to be an Owner of a group in order to be able to add members to it. -To override the membership of an ancestor group, simply add the user in the new -subgroup again, but with different permissions. +To override a user's membership of an ancestor group (the first group they were +added to), simply add the user in the new subgroup again, but with different +permissions. -For example, if User0 was first added to group `one/two` with Developer +For example, if User0 was first added to group `group-1/group-1-1` with Developer permissions, then they will inherit those permissions in every other subgroup -of `one/two`. To give them Master access to `one/two/three`, you would add them -again in that group as Master. Removing them from that group, the permissions -will fallback to those of the ancestor group. - -Note that the higher permission wins, so if in the above example the permissions -where reversed, User0 would have Master access to all groups, even to the one -that was explicitly given Developer access. +of `group-1/group-1-1`. To give them Master access to `group-1/group-1-1/group1-1-1`, +you would add them again in that group as Master. Removing them from that group, +the permissions will fallback to those of the ancestor group. ## Mentioning subgroups -- cgit v1.2.1 From 6890327762eaeca572ada783804a9c7af01e6144 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 10 Mar 2017 15:34:29 -0600 Subject: Copy code as GFM from diffs, blobs and GFM code blocks --- app/assets/javascripts/copy_as_gfm.js | 72 ++- changelogs/unreleased/dm-copy-code-as-gfm.yml | 4 + lib/banzai/filter/syntax_highlight_filter.rb | 13 +- lib/gitlab/highlight.rb | 4 +- lib/rouge/formatters/html_gitlab.rb | 5 +- spec/features/copy_as_gfm_spec.rb | 782 +++++++++++++++----------- 6 files changed, 537 insertions(+), 343 deletions(-) create mode 100644 changelogs/unreleased/dm-copy-code-as-gfm.yml diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 0fb7bde1fd6..67f7226fe82 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -118,10 +118,10 @@ const gfmRules = { }, SyntaxHighlightFilter: { 'pre.code.highlight'(el, t) { - const text = t.trim(); + const text = t.trimRight(); let lang = el.getAttribute('lang'); - if (lang === 'plaintext') { + if (!lang || lang === 'plaintext') { lang = ''; } @@ -157,7 +157,7 @@ const gfmRules = { const backticks = Array(backtickCount + 1).join('`'); const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; - return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks; + return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks; }, 'blockquote'(el, text) { return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); @@ -273,28 +273,29 @@ const gfmRules = { class CopyAsGFM { constructor() { - $(document).on('copy', '.md, .wiki', this.handleCopy); - $(document).on('paste', '.js-gfm-input', this.handlePaste); + $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); + $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); + $(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this)); } - handleCopy(e) { + copyAsGFM(e, transformer) { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; const documentFragment = window.gl.utils.getSelectedFragment(); if (!documentFragment) return; - // If the documentFragment contains more than just Markdown, don't copy as GFM. - if (documentFragment.querySelector('.md, .wiki')) return; + const el = transformer(documentFragment.cloneNode(true)); + if (!el) return; e.preventDefault(); - clipboardData.setData('text/plain', documentFragment.textContent); + e.stopPropagation(); - const gfm = CopyAsGFM.nodeToGFM(documentFragment); - clipboardData.setData('text/x-gfm', gfm); + clipboardData.setData('text/plain', el.textContent); + clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el)); } - handlePaste(e) { + pasteGFM(e) { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; @@ -306,7 +307,54 @@ class CopyAsGFM { window.gl.utils.insertText(e.target, gfm); } + static transformGFMSelection(documentFragment) { + // If the documentFragment contains more than just Markdown, don't copy as GFM. + if (documentFragment.querySelector('.md, .wiki')) return null; + + return documentFragment; + } + + static transformCodeSelection(documentFragment) { + const lineEls = documentFragment.querySelectorAll('.line'); + + let codeEl; + if (lineEls.length > 1) { + codeEl = document.createElement('pre'); + codeEl.className = 'code highlight'; + + const lang = lineEls[0].getAttribute('lang'); + if (lang) { + codeEl.setAttribute('lang', lang); + } + } else { + codeEl = document.createElement('code'); + } + + if (lineEls.length > 0) { + for (let i = 0; i < lineEls.length; i += 1) { + const lineEl = lineEls[i]; + codeEl.appendChild(lineEl); + codeEl.appendChild(document.createTextNode('\n')); + } + } else { + codeEl.appendChild(documentFragment); + } + + return codeEl; + } + + static selectionToGFM(documentFragment, transformer) { + const el = transformer(documentFragment.cloneNode(true)); + if (!el) return null; + + return CopyAsGFM.nodeToGFM(el); + } + static nodeToGFM(node) { + if (node.nodeType === Node.COMMENT_NODE) { + return ''; + } + if (node.nodeType === Node.TEXT_NODE) { return node.textContent; } diff --git a/changelogs/unreleased/dm-copy-code-as-gfm.yml b/changelogs/unreleased/dm-copy-code-as-gfm.yml new file mode 100644 index 00000000000..15ae2da44a3 --- /dev/null +++ b/changelogs/unreleased/dm-copy-code-as-gfm.yml @@ -0,0 +1,4 @@ +--- +title: Copy code as GFM from diffs, blobs and GFM code blocks +merge_request: +author: diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index a447e2b8bff..9f09ca90697 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -5,8 +5,6 @@ module Banzai # HTML Filter to highlight fenced code blocks # class SyntaxHighlightFilter < HTML::Pipeline::Filter - include Rouge::Plugins::Redcarpet - def call doc.search('pre > code').each do |node| highlight_node(node) @@ -23,7 +21,7 @@ module Banzai lang = lexer.tag begin - code = format(lex(lexer, code)) + code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang) css_classes << " js-syntax-highlight #{lang}" rescue @@ -45,10 +43,6 @@ module Banzai lexer.lex(code) end - def format(tokens) - rouge_formatter.format(tokens) - end - def lexer_for(language) (Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new end @@ -57,11 +51,6 @@ module Banzai # Replace the parent `pre` element with the entire highlighted block node.parent.replace(highlighted) end - - # Override Rouge::Plugins::Redcarpet#rouge_formatter - def rouge_formatter(lexer = nil) - @rouge_formatter ||= Rouge::Formatters::HTML.new - end end end end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 9360afedfcb..d787d5db4a0 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -14,7 +14,7 @@ module Gitlab end def initialize(blob_name, blob_content, repository: nil) - @formatter = Rouge::Formatters::HTMLGitlab.new + @formatter = Rouge::Formatters::HTMLGitlab @repository = repository @blob_name = blob_name @blob_content = blob_content @@ -28,7 +28,7 @@ module Gitlab hl_lexer = self.lexer end - @formatter.format(hl_lexer.lex(text, continue: continue)).html_safe + @formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe rescue @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe end diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index 4edfd015074..ec95ddf03ea 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -6,9 +6,10 @@ module Rouge # Creates a new Rouge::Formatter::HTMLGitlab instance. # # [+linenostart+] The line number for the first line (default: 1). - def initialize(linenostart: 1) + def initialize(linenostart: 1, tag: nil) @linenostart = linenostart @line_number = linenostart + @tag = tag end def stream(tokens, &b) @@ -17,7 +18,7 @@ module Rouge yield "\n" unless is_first is_first = false - yield %() + yield %() line.each { |token, value| yield span(token, value.chomp) } yield %() diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index 4638812b2d9..f134d4be154 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -2,437 +2,589 @@ require 'spec_helper' describe 'Copy as GFM', feature: true, js: true do include GitlabMarkdownHelper + include RepoHelpers include ActionView::Helpers::JavaScriptHelper before do - @feat = MarkdownFeature.new + login_as :admin + end - # `markdown` helper expects a `@project` variable - @project = @feat.project + describe 'Copying rendered GFM' do + before do + @feat = MarkdownFeature.new - visit namespace_project_issue_path(@project.namespace, @project, @feat.issue) - end + # `markdown` helper expects a `@project` variable + @project = @feat.project - # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML. - # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM. - # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle - # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper. + visit namespace_project_issue_path(@project.namespace, @project, @feat.issue) + end - # These are all in a single `it` for performance reasons. - it 'works', :aggregate_failures do - verify( - 'nesting', + # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML. + # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM. + # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle + # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper. - '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**' - ) + # These are all in a single `it` for performance reasons. + it 'works', :aggregate_failures do + verify( + 'nesting', - verify( - 'a real world example from the gitlab-ce README', + '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**' + ) - <<-GFM.strip_heredoc - # GitLab + verify( + 'a real world example from the gitlab-ce README', - [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) - [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) - [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) - [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) + <<-GFM.strip_heredoc + # GitLab - ## Canonical source + [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) + [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) + [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) + [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) - The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). + ## Canonical source - ## Open source software to collaborate on code + The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). - To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). + ## Open source software to collaborate on code + To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). - - Manage Git repositories with fine grained access controls that keep your code secure - - Perform code reviews and enhance collaboration with merge requests + - Manage Git repositories with fine grained access controls that keep your code secure - - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications + - Perform code reviews and enhance collaboration with merge requests - - Each project can also have an issue tracker, issue board, and a wiki + - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications - - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises + - Each project can also have an issue tracker, issue board, and a wiki - - Completely free and open source (MIT Expat license) - GFM - ) + - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - verify( - 'InlineDiffFilter', + - Completely free and open source (MIT Expat license) + GFM + ) - '{-Deleted text-}', - '{+Added text+}' - ) + verify( + 'InlineDiffFilter', - verify( - 'TaskListFilter', + '{-Deleted text-}', + '{+Added text+}' + ) - '- [ ] Unchecked task', - '- [x] Checked task', - '1. [ ] Unchecked numbered task', - '1. [x] Checked numbered task' - ) + verify( + 'TaskListFilter', - verify( - 'ReferenceFilter', + '- [ ] Unchecked task', + '- [x] Checked task', + '1. [ ] Unchecked numbered task', + '1. [x] Checked numbered task' + ) - # issue reference - @feat.issue.to_reference, - # full issue reference - @feat.issue.to_reference(full: true), - # issue URL - namespace_project_issue_url(@project.namespace, @project, @feat.issue), - # issue URL with note anchor - namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'), - # issue link - "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})", - # issue link with note anchor - "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})", - ) + verify( + 'ReferenceFilter', - verify( - 'AutolinkFilter', + # issue reference + @feat.issue.to_reference, + # full issue reference + @feat.issue.to_reference(full: true), + # issue URL + namespace_project_issue_url(@project.namespace, @project, @feat.issue), + # issue URL with note anchor + namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'), + # issue link + "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})", + # issue link with note anchor + "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})", + ) - 'https://example.com' - ) + verify( + 'AutolinkFilter', - verify( - 'TableOfContentsFilter', + 'https://example.com' + ) - '[[_TOC_]]' - ) + verify( + 'TableOfContentsFilter', - verify( - 'EmojiFilter', + '[[_TOC_]]' + ) - ':thumbsup:' - ) + verify( + 'EmojiFilter', - verify( - 'ImageLinkFilter', - - '![Image](https://example.com/image.png)' - ) + ':thumbsup:' + ) - verify( - 'VideoLinkFilter', + verify( + 'ImageLinkFilter', + + '![Image](https://example.com/image.png)' + ) - '![Video](https://example.com/video.mp4)' - ) + verify( + 'VideoLinkFilter', - verify( - 'MathFilter: math as converted from GFM to HTML', + '![Video](https://example.com/video.mp4)' + ) - '$`c = \pm\sqrt{a^2 + b^2}`$', + verify( + 'MathFilter: math as converted from GFM to HTML', - # math block - <<-GFM.strip_heredoc - ```math - c = \pm\sqrt{a^2 + b^2} - ``` - GFM - ) + '$`c = \pm\sqrt{a^2 + b^2}`$', - aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do - gfm = '$`c = \pm\sqrt{a^2 + b^2}`$' + # math block + <<-GFM.strip_heredoc + ```math + c = \pm\sqrt{a^2 + b^2} + ``` + GFM + ) - html = <<-HTML.strip_heredoc - - - - - - c - = - ± - - - - a - 2 - - + - - b - 2 - - - - - c = \\pm\\sqrt{a^2 + b^2} - - - -