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 2c4a69616b59428ffe23908c96241d82ca316bcd Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Tue, 9 May 2017 18:34:14 +0200 Subject: no trailing / leading hyphens in CI_COMMIT_REF_SLUG. Fixes #32035 --- app/models/ci/build.rb | 4 +++- doc/ci/variables/README.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 971ab7cb0ee..d21f79560c4 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -170,9 +170,11 @@ module Ci # * Lowercased # * Anything not matching [a-z0-9-] is replaced with a - # * Maximum length is 63 bytes + # * First/Last Character is not a hyphen def ref_slug slugified = ref.to_s.downcase - slugified.gsub(/[^a-z0-9]/, '-')[0..62] + slugified = slugified.gsub(/[^a-z0-9]/, '-')[0..62] + slugified.gsub(/(^\-+|\-+$)/, '') end # Variables whose value does not depend on other variables diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 045d3821f66..f0c92540fbb 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -37,7 +37,7 @@ future GitLab releases.** |-------------------------------- |--------|--------|-------------| | **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_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 `-`. No leading / trailing `-`. Use in URLs, host names 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 | -- cgit v1.2.1 From 3c4f414bfccd0fda4bea4f6d553159f06bc78124 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Tue, 9 May 2017 18:34:14 +0200 Subject: no trailing / leading hyphens in CI_COMMIT_REF_SLUG. Fixes #32035 --- app/models/ci/build.rb | 4 +++- doc/ci/variables/README.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3c4a4d93349..756f976a449 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -170,9 +170,11 @@ module Ci # * Lowercased # * Anything not matching [a-z0-9-] is replaced with a - # * Maximum length is 63 bytes + # * First/Last Character is not a hyphen def ref_slug slugified = ref.to_s.downcase - slugified.gsub(/[^a-z0-9]/, '-')[0..62] + slugified = slugified.gsub(/[^a-z0-9]/, '-')[0..62] + slugified.gsub(/(^\-+|\-+$)/, '') end # Variables whose value does not depend on other variables diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 045d3821f66..f0c92540fbb 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -37,7 +37,7 @@ future GitLab releases.** |-------------------------------- |--------|--------|-------------| | **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_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 `-`. No leading / trailing `-`. Use in URLs, host names 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 | -- cgit v1.2.1 From 18759f46c53148ff63291276e5f8bb8ac642711f Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 17:28:44 +0200 Subject: added changelog entry --- changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml diff --git a/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml new file mode 100644 index 00000000000..f5ade3536d3 --- /dev/null +++ b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml @@ -0,0 +1,4 @@ +--- +title: Omit trailing / leding hyphens in CI_COMMIT_REF_SLUG variable +merge_request: 11218 +author: Stefan Hanreich -- cgit v1.2.1 From ac516151a63a4fe69363b208b4f53e74e27df94c Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 20:04:36 +0200 Subject: Updated spec for build to include new ref_slug invariants --- spec/models/ci/build_spec.rb | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index e971b4bc3f9..407b49a1ff7 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -965,19 +965,27 @@ describe Ci::Build, :models do end describe '#ref_slug' do + let(:build) { build(:ci_build, ref: "'100%") } + subject { build.ref_slug } + + it { is_expected.not_to start_with('-') } + it { is_expected.not_to end_with('-') } + { - 'master' => 'master', - '1-foo' => '1-foo', - 'fix/1-foo' => 'fix-1-foo', - 'fix-1-foo' => 'fix-1-foo', - 'a' * 63 => 'a' * 63, - 'a' * 64 => 'a' * 63, - 'FOO' => 'foo' + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + '-' + 'a' * 61 + '-' => 'a' * 61, + 'a' * 62 + ' ' => 'a' * 62, }.each do |ref, slug| it "transforms #{ref} to #{slug}" do build.ref = ref - expect(build.ref_slug).to eq(slug) + is_expected.to eq(slug) end end end -- cgit v1.2.1 From a1279eb543e5f29d93c2eedc42878cd60e5c238d Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 20:14:15 +0200 Subject: updated regex to use beginning / ending of string metacharacters --- app/models/ci/build.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 756f976a449..1dc5035605c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -174,7 +174,7 @@ module Ci def ref_slug slugified = ref.to_s.downcase slugified = slugified.gsub(/[^a-z0-9]/, '-')[0..62] - slugified.gsub(/(^\-+|\-+$)/, '') + slugified.gsub(/(\A-+|-+\z)/, '') end # Variables whose value does not depend on other variables -- cgit v1.2.1 From 3101992320687b05a7e951c0c3befeb1d4093b45 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 20:50:59 +0200 Subject: removed superfluos tests --- spec/models/ci/build_spec.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 407b49a1ff7..c29c62a2580 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -965,12 +965,6 @@ describe Ci::Build, :models do end describe '#ref_slug' do - let(:build) { build(:ci_build, ref: "'100%") } - subject { build.ref_slug } - - it { is_expected.not_to start_with('-') } - it { is_expected.not_to end_with('-') } - { 'master' => 'master', '1-foo' => '1-foo', @@ -985,7 +979,7 @@ describe Ci::Build, :models do it "transforms #{ref} to #{slug}" do build.ref = ref - is_expected.to eq(slug) + expected(build.ref_slug).to eq(slug) end end end -- cgit v1.2.1 From d4e372d5c4f9c72b36481febdd5bec55edc9ee77 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 20:54:41 +0200 Subject: fix typo --- spec/models/ci/build_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index c29c62a2580..02a2877e6fd 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -979,7 +979,7 @@ describe Ci::Build, :models do it "transforms #{ref} to #{slug}" do build.ref = ref - expected(build.ref_slug).to eq(slug) + expect(build.ref_slug).to eq(slug) end end end -- cgit v1.2.1 From 78cf7dd9d83ef65be414b526b226e1620970dc27 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 21:42:39 +0200 Subject: remove trailing comma --- spec/models/ci/build_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 02a2877e6fd..fbba2c8be60 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -974,7 +974,7 @@ describe Ci::Build, :models do 'a' * 64 => 'a' * 63, 'FOO' => 'foo', '-' + 'a' * 61 + '-' => 'a' * 61, - 'a' * 62 + ' ' => 'a' * 62, + 'a' * 62 + ' ' => 'a' * 62 }.each do |ref, slug| it "transforms #{ref} to #{slug}" do build.ref = ref -- cgit v1.2.1 From 589d7ea484f7215cff6570080caf47751f712ed9 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 21:59:05 +0200 Subject: using bang method for gsub --- app/models/ci/build.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 1dc5035605c..259d29e84d1 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -173,8 +173,8 @@ module Ci # * First/Last Character is not a hyphen def ref_slug slugified = ref.to_s.downcase - slugified = slugified.gsub(/[^a-z0-9]/, '-')[0..62] - slugified.gsub(/(\A-+|-+\z)/, '') + slugified.gsub!(/[^a-z0-9]/, '-') + slugified[0..62].gsub(/(\A-+|-+\z)/, '') end # Variables whose value does not depend on other variables -- cgit v1.2.1 From 07a65da1d96a71474f6997aed95bac6290d81a42 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Jun 2017 22:15:40 +0800 Subject: Generate KUBECONFIG in KubernetesService#predefined_variables --- app/models/project_services/kubernetes_service.rb | 14 +++++++- lib/gitlab/kubernetes.rb | 39 +++++++++++++++++++++ spec/fixtures/config/kubeconfig-without-ca.yml | 18 ++++++++++ spec/fixtures/config/kubeconfig.yml | 19 ++++++++++ spec/lib/gitlab/kubernetes_spec.rb | 24 +++++++++++++ .../project_services/kubernetes_service_spec.rb | 40 ++++++++++++++-------- 6 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 spec/fixtures/config/kubeconfig-without-ca.yml create mode 100644 spec/fixtures/config/kubeconfig.yml diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 48e7802c557..f1b321139d3 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -96,10 +96,14 @@ class KubernetesService < DeploymentService end def predefined_variables + config = YAML.dump(kubeconfig) + variables = [ { key: 'KUBE_URL', value: api_url, public: true }, { key: 'KUBE_TOKEN', value: token, public: false }, - { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true } + { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }, + { key: 'KUBECONFIG', value: config, public: false }, + { key: 'KUBECONFIG_FILE', value: config, public: false, file: true }, ] if ca_pem.present? @@ -135,6 +139,14 @@ class KubernetesService < DeploymentService private + def kubeconfig + to_kubeconfig( + url: api_url, + namespace: actual_namespace, + token: token, + ca_pem: ca_pem) + end + def namespace_placeholder default_namespace || TEMPLATE_PLACEHOLDER end diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb index c56c1a4322f..cedef9b65ba 100644 --- a/lib/gitlab/kubernetes.rb +++ b/lib/gitlab/kubernetes.rb @@ -76,5 +76,44 @@ module Gitlab url.to_s end + + def to_kubeconfig(url:, namespace:, token:, ca_pem: nil) + config = { + apiVersion: 'v1', + clusters: [ + name: 'gitlab-deploy', + cluster: { + server: url + }, + ], + contexts: [ + name: 'gitlab-deploy', + context: { + cluster: 'gitlab-deploy', + namespace: namespace, + user: 'gitlab-deploy' + }, + ], + :'current-context' => 'gitlab-deploy', + kind: 'Config', + users: [ + { + name: 'gitlab-deploy', + user: {token: token} + } + ] + } + + kubeconfig_embed_ca_pem(config, ca_pem) if ca_pem + + config.deep_stringify_keys + end + + private + + def kubeconfig_embed_ca_pem(config, ca_pem) + cluster = config.dig(:clusters, 0, :cluster) + cluster[:'certificate-authority-data'] = ca_pem + end end end diff --git a/spec/fixtures/config/kubeconfig-without-ca.yml b/spec/fixtures/config/kubeconfig-without-ca.yml new file mode 100644 index 00000000000..b2cb989d548 --- /dev/null +++ b/spec/fixtures/config/kubeconfig-without-ca.yml @@ -0,0 +1,18 @@ +--- +apiVersion: v1 +clusters: +- name: gitlab-deploy + cluster: + server: https://kube.domain.com +contexts: +- name: gitlab-deploy + context: + cluster: gitlab-deploy + namespace: NAMESPACE + user: gitlab-deploy +current-context: gitlab-deploy +kind: Config +users: +- name: gitlab-deploy + user: + token: TOKEN diff --git a/spec/fixtures/config/kubeconfig.yml b/spec/fixtures/config/kubeconfig.yml new file mode 100644 index 00000000000..4fa52818fee --- /dev/null +++ b/spec/fixtures/config/kubeconfig.yml @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +clusters: +- name: gitlab-deploy + cluster: + server: https://kube.domain.com + certificate-authority-data: PEM +contexts: +- name: gitlab-deploy + context: + cluster: gitlab-deploy + namespace: NAMESPACE + user: gitlab-deploy +current-context: gitlab-deploy +kind: Config +users: +- name: gitlab-deploy + user: + token: TOKEN diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb index e8c599a95ee..34b33772578 100644 --- a/spec/lib/gitlab/kubernetes_spec.rb +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -46,4 +46,28 @@ describe Gitlab::Kubernetes do expect(filter_by_label(items, app: 'foo')).to eq(matching_items) end end + + describe '#to_kubeconfig' do + subject do + to_kubeconfig( + url: 'https://kube.domain.com', + namespace: 'NAMESPACE', + token: 'TOKEN', + ca_pem: ca_pem) + end + + context 'when CA PEM is provided' do + let(:ca_pem) { 'PEM' } + let(:path) { expand_fixture_path('config/kubeconfig.yml') } + + it { is_expected.to eq(YAML.load_file(path)) } + end + + context 'when CA PEM is not provided' do + let(:ca_pem) { nil } + let(:path) { expand_fixture_path('config/kubeconfig-without-ca.yml') } + + it { is_expected.to eq(YAML.load_file(path)) } + end + end end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 858ad595dbf..f69e273cd7c 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -129,7 +129,7 @@ describe KubernetesService, models: true, caching: true do it "returns the default namespace" do is_expected.to eq(service.send(:default_namespace)) end - + context 'when namespace is specified' do before do service.namespace = 'my-namespace' @@ -201,6 +201,13 @@ describe KubernetesService, models: true, caching: true do end describe '#predefined_variables' do + let(:kubeconfig) do + File.read(expand_fixture_path('config/kubeconfig.yml')) + .gsub('TOKEN', 'token') + .gsub('PEM', 'CA PEM DATA') + .gsub('NAMESPACE', namespace) + end + before do subject.api_url = 'https://kube.domain.com' subject.token = 'token' @@ -208,32 +215,35 @@ describe KubernetesService, models: true, caching: true do subject.project = project end - context 'namespace is provided' do - before do - subject.namespace = 'my-project' - end - + shared_examples 'setting variables' do it 'sets the variables' do expect(subject.predefined_variables).to include( { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }, { key: 'KUBE_TOKEN', value: 'token', public: false }, - { key: 'KUBE_NAMESPACE', value: 'my-project', public: true }, + { key: 'KUBE_NAMESPACE', value: namespace, public: true }, + { key: 'KUBECONFIG', value: kubeconfig, public: false }, + { key: 'KUBECONFIG_FILE', value: kubeconfig, public: false, file: true }, { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }, { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true } ) end end - context 'no namespace provided' do - it 'sets the variables' do - expect(subject.predefined_variables).to include( - { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }, - { key: 'KUBE_TOKEN', value: 'token', public: false }, - { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }, - { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true } - ) + context 'namespace is provided' do + let(:namespace) { 'my-project' } + + before do + subject.namespace = namespace end + it_behaves_like 'setting variables' + end + + context 'no namespace provided' do + let(:namespace) { subject.actual_namespace } + + it_behaves_like 'setting variables' + it 'sets the KUBE_NAMESPACE' do kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' } -- cgit v1.2.1 From 6eaec942e6ae89818ea1ba0da5ff00daea633c41 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Jun 2017 22:26:40 +0800 Subject: Changelog entry, doc, and only pass KUBECONFIG_FILE --- app/models/project_services/kubernetes_service.rb | 3 +-- changelogs/unreleased/33360-generate-kubeconfig.yml | 4 ++++ doc/user/project/integrations/kubernetes.md | 1 + spec/models/project_services/kubernetes_service_spec.rb | 1 - 4 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/33360-generate-kubeconfig.yml diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f1b321139d3..831f4e5a3c8 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -102,8 +102,7 @@ class KubernetesService < DeploymentService { key: 'KUBE_URL', value: api_url, public: true }, { key: 'KUBE_TOKEN', value: token, public: false }, { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }, - { key: 'KUBECONFIG', value: config, public: false }, - { key: 'KUBECONFIG_FILE', value: config, public: false, file: true }, + { key: 'KUBECONFIG_FILE', value: config, public: false, file: true } ] if ca_pem.present? diff --git a/changelogs/unreleased/33360-generate-kubeconfig.yml b/changelogs/unreleased/33360-generate-kubeconfig.yml new file mode 100644 index 00000000000..354a8a7f9b4 --- /dev/null +++ b/changelogs/unreleased/33360-generate-kubeconfig.yml @@ -0,0 +1,4 @@ +--- +title: Provide KUBECONFIG_FILE from KubernetesService for runners +merge_request: 12223 +author: diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md index 73fa83d72a8..d1c3e18a276 100644 --- a/doc/user/project/integrations/kubernetes.md +++ b/doc/user/project/integrations/kubernetes.md @@ -55,6 +55,7 @@ GitLab CI build environment: - `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path to a file containing PEM data. - `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data. +- `KUBECONFIG_FILE` - Path to a file containing kubeconfig for this deployment. CA bundle would be embedded if specified. ## Web terminals diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index f69e273cd7c..d4feae231bc 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -221,7 +221,6 @@ describe KubernetesService, models: true, caching: true do { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }, { key: 'KUBE_TOKEN', value: 'token', public: false }, { key: 'KUBE_NAMESPACE', value: namespace, public: true }, - { key: 'KUBECONFIG', value: kubeconfig, public: false }, { key: 'KUBECONFIG_FILE', value: kubeconfig, public: false, file: true }, { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }, { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true } -- cgit v1.2.1 From 6fe2b79656cca21f9f1cea504e5cbb58a51a05ab Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Jun 2017 23:17:06 +0800 Subject: Fix rubocop offense --- lib/gitlab/kubernetes.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb index cedef9b65ba..88bae87211a 100644 --- a/lib/gitlab/kubernetes.rb +++ b/lib/gitlab/kubernetes.rb @@ -84,7 +84,7 @@ module Gitlab name: 'gitlab-deploy', cluster: { server: url - }, + } ], contexts: [ name: 'gitlab-deploy', @@ -92,14 +92,14 @@ module Gitlab cluster: 'gitlab-deploy', namespace: namespace, user: 'gitlab-deploy' - }, + } ], - :'current-context' => 'gitlab-deploy', + 'current-context': 'gitlab-deploy', kind: 'Config', users: [ { name: 'gitlab-deploy', - user: {token: token} + user: { token: token } } ] } -- cgit v1.2.1 From 6dda9795c2a205e5960dfaf6062d15aca1c67abe Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Sun, 18 Jun 2017 23:49:04 +0200 Subject: added additional test case --- spec/models/ci/build_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index fbba2c8be60..d0200a27365 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -974,6 +974,7 @@ describe Ci::Build, :models do 'a' * 64 => 'a' * 63, 'FOO' => 'foo', '-' + 'a' * 61 + '-' => 'a' * 61, + '-' + 'a' * 63 + '-' => 'a' * 63, 'a' * 62 + ' ' => 'a' * 62 }.each do |ref, slug| it "transforms #{ref} to #{slug}" do -- cgit v1.2.1 From f4c05e038079759ff70473036391fc023ce4b7d3 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Tue, 9 May 2017 18:34:14 +0200 Subject: no trailing / leading hyphens in CI_COMMIT_REF_SLUG. Fixes #32035 --- app/models/ci/build.rb | 4 +++- doc/ci/variables/README.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 58758f7ca8a..f190dcf306c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -187,9 +187,11 @@ module Ci # * Lowercased # * Anything not matching [a-z0-9-] is replaced with a - # * Maximum length is 63 bytes + # * First/Last Character is not a hyphen def ref_slug slugified = ref.to_s.downcase - slugified.gsub(/[^a-z0-9]/, '-')[0..62] + slugified = slugified.gsub(/[^a-z0-9]/, '-')[0..62] + slugified.gsub(/(^\-+|\-+$)/, '') end # Variables whose value does not depend on other variables diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index d1f9881e51b..eef96f3194f 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -37,7 +37,7 @@ future GitLab releases.** |-------------------------------- |--------|--------|-------------| | **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_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 `-`. No leading / trailing `-`. Use in URLs, host names 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 | -- cgit v1.2.1 From 39b2d730847a918fd5d7cca207e687c77f1d9ceb Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 17:28:44 +0200 Subject: added changelog entry --- changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml diff --git a/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml new file mode 100644 index 00000000000..f5ade3536d3 --- /dev/null +++ b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml @@ -0,0 +1,4 @@ +--- +title: Omit trailing / leding hyphens in CI_COMMIT_REF_SLUG variable +merge_request: 11218 +author: Stefan Hanreich -- cgit v1.2.1 From e02ca5fdb7f9b9a7e52d0e6edfb3be65860818a5 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 20:04:36 +0200 Subject: Updated spec for build to include new ref_slug invariants --- spec/models/ci/build_spec.rb | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 3816422fec6..a54261667bd 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1033,19 +1033,27 @@ describe Ci::Build, :models do end describe '#ref_slug' do + let(:build) { build(:ci_build, ref: "'100%") } + subject { build.ref_slug } + + it { is_expected.not_to start_with('-') } + it { is_expected.not_to end_with('-') } + { - 'master' => 'master', - '1-foo' => '1-foo', - 'fix/1-foo' => 'fix-1-foo', - 'fix-1-foo' => 'fix-1-foo', - 'a' * 63 => 'a' * 63, - 'a' * 64 => 'a' * 63, - 'FOO' => 'foo' + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + '-' + 'a' * 61 + '-' => 'a' * 61, + 'a' * 62 + ' ' => 'a' * 62, }.each do |ref, slug| it "transforms #{ref} to #{slug}" do build.ref = ref - expect(build.ref_slug).to eq(slug) + is_expected.to eq(slug) end end end -- cgit v1.2.1 From 5fc22bfabd619115c1df28d30a951084508b28ed Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 20:14:15 +0200 Subject: updated regex to use beginning / ending of string metacharacters --- app/models/ci/build.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index f190dcf306c..58bd0586ce8 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -191,7 +191,7 @@ module Ci def ref_slug slugified = ref.to_s.downcase slugified = slugified.gsub(/[^a-z0-9]/, '-')[0..62] - slugified.gsub(/(^\-+|\-+$)/, '') + slugified.gsub(/(\A-+|-+\z)/, '') end # Variables whose value does not depend on other variables -- cgit v1.2.1 From 6394d560ada1bc8b70bcd338343fe978916f84a4 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 20:50:59 +0200 Subject: removed superfluos tests --- spec/models/ci/build_spec.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a54261667bd..855e55dd489 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1033,12 +1033,6 @@ describe Ci::Build, :models do end describe '#ref_slug' do - let(:build) { build(:ci_build, ref: "'100%") } - subject { build.ref_slug } - - it { is_expected.not_to start_with('-') } - it { is_expected.not_to end_with('-') } - { 'master' => 'master', '1-foo' => '1-foo', @@ -1053,7 +1047,7 @@ describe Ci::Build, :models do it "transforms #{ref} to #{slug}" do build.ref = ref - is_expected.to eq(slug) + expected(build.ref_slug).to eq(slug) end end end -- cgit v1.2.1 From fde8f9d736b058e1688c87ed4d3ac835d1603937 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 20:54:41 +0200 Subject: fix typo --- spec/models/ci/build_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 855e55dd489..4bf1f296803 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1047,7 +1047,7 @@ describe Ci::Build, :models do it "transforms #{ref} to #{slug}" do build.ref = ref - expected(build.ref_slug).to eq(slug) + expect(build.ref_slug).to eq(slug) end end end -- cgit v1.2.1 From d60a59b067f7914ddcd7d28300f025cf125b586d Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 21:42:39 +0200 Subject: remove trailing comma --- spec/models/ci/build_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4bf1f296803..7ec06b6d6be 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1042,7 +1042,7 @@ describe Ci::Build, :models do 'a' * 64 => 'a' * 63, 'FOO' => 'foo', '-' + 'a' * 61 + '-' => 'a' * 61, - 'a' * 62 + ' ' => 'a' * 62, + 'a' * 62 + ' ' => 'a' * 62 }.each do |ref, slug| it "transforms #{ref} to #{slug}" do build.ref = ref -- cgit v1.2.1 From 935578b4ab6d49caa516a1c96b9bfd80f7358c74 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Thu, 11 May 2017 21:59:05 +0200 Subject: using bang method for gsub --- app/models/ci/build.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 58bd0586ce8..ffe8f5a28ae 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -190,8 +190,8 @@ module Ci # * First/Last Character is not a hyphen def ref_slug slugified = ref.to_s.downcase - slugified = slugified.gsub(/[^a-z0-9]/, '-')[0..62] - slugified.gsub(/(\A-+|-+\z)/, '') + slugified.gsub!(/[^a-z0-9]/, '-') + slugified[0..62].gsub(/(\A-+|-+\z)/, '') end # Variables whose value does not depend on other variables -- cgit v1.2.1 From c701ea945a9af4d1332357b9274d36fffd98c345 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Sun, 18 Jun 2017 23:49:04 +0200 Subject: added additional test case --- spec/models/ci/build_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7ec06b6d6be..7033623d413 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1042,6 +1042,7 @@ describe Ci::Build, :models do 'a' * 64 => 'a' * 63, 'FOO' => 'foo', '-' + 'a' * 61 + '-' => 'a' * 61, + '-' + 'a' * 63 + '-' => 'a' * 63, 'a' * 62 + ' ' => 'a' * 62 }.each do |ref, slug| it "transforms #{ref} to #{slug}" do -- cgit v1.2.1 From da737f13a963075ffc952756cfd6da2997d1a348 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Mon, 19 Jun 2017 14:13:46 +0200 Subject: fixed incorrect test case --- spec/models/ci/build_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7033623d413..523650eb506 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1042,7 +1042,7 @@ describe Ci::Build, :models do 'a' * 64 => 'a' * 63, 'FOO' => 'foo', '-' + 'a' * 61 + '-' => 'a' * 61, - '-' + 'a' * 63 + '-' => 'a' * 63, + '-' + 'a' * 63 => 'a' * 63, 'a' * 62 + ' ' => 'a' * 62 }.each do |ref, slug| it "transforms #{ref} to #{slug}" do -- cgit v1.2.1 From 071603a0a59ff315b1d6b1ee2996b37a37cb3c75 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Mon, 19 Jun 2017 14:16:41 +0200 Subject: fixed incorrect test case (for real), added another one --- spec/models/ci/build_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 523650eb506..dec105d13ec 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1042,7 +1042,8 @@ describe Ci::Build, :models do 'a' * 64 => 'a' * 63, 'FOO' => 'foo', '-' + 'a' * 61 + '-' => 'a' * 61, - '-' + 'a' * 63 => 'a' * 63, + '-' + 'a' * 62 + '-' => 'a' * 62, + '-' + 'a' * 63 + '-' => 'a' * 62, 'a' * 62 + ' ' => 'a' * 62 }.each do |ref, slug| it "transforms #{ref} to #{slug}" do -- cgit v1.2.1 From 91c58ed14d13dcefd7ca2834d6157a9fa630a9af Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 31 May 2017 15:59:01 +0200 Subject: FormHelper#issue_assignees_dropdown_options never has multiple assignees Only EE supports multiple issue assignees, so this CE code should not contain code to have multiple assignees. EE will override the multiple issue assignees feature by overriding this method. --- app/helpers/form_helper.rb | 16 ++++------------ .../issuable/form/_metadata_issue_assignee.html.haml | 2 +- spec/features/issues/form_spec.rb | 4 ++-- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 014fc46b130..729bc4bf329 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -16,8 +16,8 @@ module FormHelper end end - def issue_dropdown_options(issuable, has_multiple_assignees = true) - options = { + def issue_assignees_dropdown_options + { toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', title: 'Select assignee', filter: true, @@ -27,8 +27,8 @@ module FormHelper first_user: current_user&.username, null_user: true, current_user: true, - project_id: issuable.project.try(:id), - field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]", + project_id: @project.id, + field_name: 'issue[assignee_ids][]', default_label: 'Unassigned', 'max-select': 1, 'dropdown-header': 'Assignee', @@ -38,13 +38,5 @@ module FormHelper current_user_info: current_user.to_json(only: [:id, :name]) } } - - if has_multiple_assignees - options[:title] = 'Select assignee(s)' - options[:data][:'dropdown-header'] = 'Assignee(s)' - options[:data].delete(:'max-select') - end - - options end end diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml index 77175c839a6..567cde764e2 100644 --- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml +++ b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml @@ -7,5 +7,5 @@ - if issuable.assignees.length === 0 = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' } - = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_dropdown_options(issuable,false)) + = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_assignees_dropdown_options) = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}" diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index b369ef1ff79..58f6bd277e4 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -31,8 +31,8 @@ describe 'New/edit issue', :feature, :js do # the original method, resulting in infinite recurison when called. # This is likely a bug with helper modules included into dynamically generated view classes. # To work around this, we have to hold on to and call to the original implementation manually. - original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options) - allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args| + original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options) + allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args| options = original_issue_dropdown_options.bind(original.receiver).call(*args) options[:data][:per_page] = 2 -- cgit v1.2.1 From 4c2ca4c38d728af65e3608bb288ebcaaba426ad8 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 7 Jun 2017 16:20:20 +0200 Subject: [noop] Remove unused code To make the code back in line with EE. [ci skip] --- app/assets/javascripts/users_select.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index ec45253e50b..828079cc03a 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) { return $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { - var isAuthorFilter; - isAuthorFilter = $('.js-author-search'); return _this.users(term, options, function(users) { // GitLabDropdownFilter returns this.instance // GitLabDropdownRemote returns this.options.instance -- cgit v1.2.1 From fd50d9ab8ec977183240b28d749cf2eb25014f41 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 7 Jun 2017 14:48:55 +0200 Subject: Use FormHelper#issue_assignees_dropdown_options for Issue sidebar Avoid code duplication and limit the number of CE -> EE merge conflict by reusing `FormHelper#issue_assignees_dropdown_options` to set some assignee dropdown attributes. --- app/views/projects/boards/components/sidebar/_assignee.html.haml | 5 +++-- app/views/shared/issuable/_sidebar_assignees.html.haml | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml index e8db868f49b..c314573bdea 100644 --- a/app/views/projects/boards/components/sidebar/_assignee.html.haml +++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml @@ -19,10 +19,11 @@ ":data-name" => "assignee.name", ":data-username" => "assignee.username" } .dropdown - %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } }, + - dropdown_options = issue_assignees_dropdown_options + %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] }, ":data-issuable-id" => "issue.id", ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } - Select assignee + = dropdown_options[:title] = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author = dropdown_title("Assign to") diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index bcfa1dc826e..3513f2de371 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -34,19 +34,20 @@ - issuable.assignees.each do |assignee| = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username } - - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } } - + - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } } - title = 'Select assignee' - if issuable.is_a?(Issue) - unless issuable.assignees.any? = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil + - dropdown_options = issue_assignees_dropdown_options + - title = dropdown_options[:title] - options[:toggle_class] += ' js-multiselect js-save-user-data' - data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" } - data[:multi_select] = true - data['dropdown-title'] = title - - data['dropdown-header'] = 'Assignee' - - data['max-select'] = 1 + - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header'] + - data['max-select'] = dropdown_options[:data][:'max-select'] - options[:data].merge!(data) = dropdown_tag(title, options: options) -- cgit v1.2.1 From a8d4bf9724ce6dce69240d4953c7d38b7f05a7dd Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Thu, 15 Jun 2017 16:01:12 +0200 Subject: Use helper method to set filtered search input attributes The list of attributes for the filtered search input was getting long, so use a helper method to fill that hash. Also, for multiple issue assignees, a helper is more convenient because it would allow EE to override the behavior if MIA is supported. --- app/helpers/search_helper.rb | 12 ++++++++++++ app/views/shared/issuable/_search_bar.html.haml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 9c46035057f..5ea2c72f819 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -126,6 +126,18 @@ module SearchHelper search_path(options) end + def search_filter_input_options(type) + { + id: "filtered-search-#{type}", + placeholder: 'Search or filter results...', + data: { + 'project-id' => @project.id, + 'username-params' => @users.to_json(only: [:id, :username]), + 'base-endpoint' => namespace_project_path(@project.namespace, @project) + } + } + end + # Sanitize a HTML field for search display. Most tags are stripped out and the # maximum length is set to 200 characters. def search_md_sanitize(object, field) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index d3d290692a2..ae890567225 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -23,7 +23,7 @@ .scroll-container %ul.tokens-container.list-unstyled %li.input-token - %input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } + %input.form-control.filtered-search{ search_filter_input_options(type) } = icon('filter') #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown %ul{ data: { dropdown: true } } -- cgit v1.2.1 From 132cd0092d6aea87359a9a0627ad2c53c4a91837 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 14 Jun 2017 22:08:24 +0200 Subject: Backport issuable for assignee slash commands from EE Avoid conflicts when merge CE to EE by backporting code from EE. Instead of checking in `SlashCommands::InterpretService` what the issuable the type of the issuable is, ask the issuable if it is capable to do those thing and implement it in the issuable itself. The issuable will check if it's possible and if the licensed feature is available. This should also make it easier to ever add multiple assignees to MergeRequests. --- app/models/concerns/issuable.rb | 12 ++++++++ app/services/quick_actions/interpret_service.rb | 38 +++++++++++++++++++------ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 8e367576c9d..0a476efdaa9 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -102,6 +102,18 @@ module Issuable def locking_enabled? title_changed? || description_changed? end + + def allows_multiple_assignees? + false + end + + def has_multiple_assignees? + supports_multiple_assignees? && assignees.count > 1 + end + + def supports_multiple_assignees? + respond_to?(:assignee_ids) + end end module ClassMethods diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 6816b137361..8adfd939c2e 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -92,9 +92,12 @@ module QuickActions desc 'Assign' explanation do |users| - "Assigns #{users.first.to_reference}." if users.any? + users = issuable.allows_multiple_assignees? ? users : users.take(1) + "Assigns #{users.map(&:to_reference).to_sentence}." + end + params do + issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user' end - params '@user' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -104,25 +107,44 @@ module QuickActions command :assign do |users| next if users.empty? - if issuable.is_a?(Issue) + if issuable.allows_multiple_assignees? + @updates[:assignee_ids] = issuable.assignees.pluck(:id) + users.map(&:id) + elsif issuable.supports_multiple_assignees? @updates[:assignee_ids] = [users.last.id] else @updates[:assignee_id] = users.last.id end end - desc 'Remove assignee' + desc do + if issuable.allows_multiple_assignees? + 'Remove all or specific assignee(s)' + else + 'Remove assignee' + end + end explanation do - "Removes assignee #{issuable.assignees.first.to_reference}." + "Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}." + end + params do + issuable.allows_multiple_assignees? ? '@user1 @user2' : '' end condition do issuable.persisted? && issuable.assignees.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :unassign do - if issuable.is_a?(Issue) - @updates[:assignee_ids] = [] + command :unassign do |unassign_param = nil| + # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed + users = extract_users(unassign_param) if issuable.allows_multiple_assignees? + + if issuable.supports_multiple_assignees? + @updates[:assignee_ids] = + if users&.any? + issuable.assignees.pluck(:id) - users.map(&:id) + else + [] + end else @updates[:assignee_id] = nil end -- cgit v1.2.1 From fcd46c1af4ceeec7813a91111dfce5e492695119 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Tue, 20 Jun 2017 15:03:25 +0200 Subject: Backport /reassign quick command The /reassign quick command works even when no multiple assignees are allowed of there isn't any assignee yet. So for consistency, it's also be backported to CE. But it functions the same as the /assign quick action. --- app/services/quick_actions/interpret_service.rb | 18 ++++++++++++++++++ spec/services/quick_actions/interpret_service_spec.rb | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 8adfd939c2e..4ceabaf021e 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -150,6 +150,24 @@ module QuickActions end end + desc do + "Change assignee#{'(s)' if issuable.allows_multiple_assignees?}" + end + explanation do |users| + users = issuable.allows_multiple_assignees? ? users : users.take(1) + "Change #{'assignee'.pluralize(users.size)} to #{users.map(&:to_reference).to_sentence}." + end + params do + issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user' + end + condition do + issuable.persisted? && + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :reassign do |unassign_param| + @updates[:assignee_ids] = extract_users(unassign_param).map(&:id) + end + desc 'Set milestone' explanation do |milestone| "Sets the milestone to #{milestone.to_reference}." if milestone diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index c9e63efbc14..b1997e64557 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -362,7 +362,7 @@ describe QuickActions::InterpretService, services: true do it 'fetches assignee and populates assignee_id if content contains /assign' do _, updates = service.execute(content, issue) - expect(updates).to eq(assignee_ids: [developer.id]) + expect(updates[:assignee_ids]).to match_array([developer.id]) end end @@ -431,6 +431,22 @@ describe QuickActions::InterpretService, services: true do end end + context 'reassign command' do + let(:content) { '/reassign' } + + context 'Issue' do + it 'reassigns user if content contains /reassign @user' do + user = create(:user) + + issue.update(assignee_ids: [developer.id]) + + _, updates = service.execute("/reassign @#{user.username}", issue) + + expect(updates).to eq(assignee_ids: [user.id]) + end + end + end + it_behaves_like 'milestone command' do let(:content) { "/milestone %#{milestone.title}" } let(:issuable) { issue } -- cgit v1.2.1 From 451e25532ff43de8151b71ced8246f709c08bf92 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Tue, 20 Jun 2017 21:32:49 +0200 Subject: Make MergeRequest respond to assignee_ids & assignee_ids= To make it simpler to assign users to an Issuable, make MergeRequest support the attribute `assignee_ids`. --- app/models/concerns/issuable.rb | 6 +---- app/models/merge_request.rb | 10 +++++++- app/services/quick_actions/interpret_service.rb | 29 +++++++++------------- spec/models/merge_request_spec.rb | 16 ++++++++++++ .../quick_actions/interpret_service_spec.rb | 18 +++++++------- 5 files changed, 47 insertions(+), 32 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0a476efdaa9..1bebd55a089 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -108,11 +108,7 @@ module Issuable end def has_multiple_assignees? - supports_multiple_assignees? && assignees.count > 1 - end - - def supports_multiple_assignees? - respond_to?(:assignee_ids) + assignees.count > 1 end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ea22ab53587..77da8413904 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -197,11 +197,19 @@ class MergeRequest < ActiveRecord::Base } end - # This method is needed for compatibility with issues to not mess view and other code + # These method are needed for compatibility with issues to not mess view and other code def assignees Array(assignee) end + def assignee_ids + Array(assignee_id) + end + + def assignee_ids=(ids) + write_attribute(:assignee_id, ids.last) + end + def assignee_or_author?(user) author_id == user.id || assignee_id == user.id end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 4ceabaf021e..df13976fb3b 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -107,13 +107,12 @@ module QuickActions command :assign do |users| next if users.empty? - if issuable.allows_multiple_assignees? - @updates[:assignee_ids] = issuable.assignees.pluck(:id) + users.map(&:id) - elsif issuable.supports_multiple_assignees? - @updates[:assignee_ids] = [users.last.id] - else - @updates[:assignee_id] = users.last.id - end + @updates[:assignee_ids] = + if issuable.allows_multiple_assignees? + issuable.assignees.pluck(:id) + users.map(&:id) + else + [users.last.id] + end end desc do @@ -138,16 +137,12 @@ module QuickActions # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed users = extract_users(unassign_param) if issuable.allows_multiple_assignees? - if issuable.supports_multiple_assignees? - @updates[:assignee_ids] = - if users&.any? - issuable.assignees.pluck(:id) - users.map(&:id) - else - [] - end - else - @updates[:assignee_id] = nil - end + @updates[:assignee_ids] = + if users&.any? + issuable.assignees.pluck(:id) - users.map(&:id) + else + [] + end end desc do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a56bc524a98..945189b922b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -105,6 +105,22 @@ describe MergeRequest, models: true do end end + describe '#assignee_ids' do + it 'returns an array of the assigned user id' do + subject.assignee_id = 123 + + expect(subject.assignee_ids).to eq([123]) + end + end + + describe '#assignee_ids=' do + it 'sets assignee_id to the last id in the array' do + subject.assignee_ids = [123, 456] + + expect(subject.assignee_id).to eq(456) + end + end + describe '#assignee_or_author?' do let(:user) { create(:user) } diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index b1997e64557..35373675894 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -359,7 +359,7 @@ describe QuickActions::InterpretService, services: true do let(:content) { "/assign @#{developer.username}" } context 'Issue' do - it 'fetches assignee and populates assignee_id if content contains /assign' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do _, updates = service.execute(content, issue) expect(updates[:assignee_ids]).to match_array([developer.id]) @@ -367,10 +367,10 @@ describe QuickActions::InterpretService, services: true do end context 'Merge Request' do - it 'fetches assignee and populates assignee_id if content contains /assign' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do _, updates = service.execute(content, merge_request) - expect(updates).to eq(assignee_id: developer.id) + expect(updates).to eq(assignee_ids: [developer.id]) end end end @@ -383,7 +383,7 @@ describe QuickActions::InterpretService, services: true do end context 'Issue' do - it 'fetches assignee and populates assignee_id if content contains /assign' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do _, updates = service.execute(content, issue) expect(updates[:assignee_ids]).to match_array([developer.id]) @@ -391,10 +391,10 @@ describe QuickActions::InterpretService, services: true do end context 'Merge Request' do - it 'fetches assignee and populates assignee_id if content contains /assign' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do _, updates = service.execute(content, merge_request) - expect(updates).to eq(assignee_id: developer.id) + expect(updates).to eq(assignee_ids: [developer.id]) end end end @@ -422,11 +422,11 @@ describe QuickActions::InterpretService, services: true do end context 'Merge Request' do - it 'populates assignee_id: nil if content contains /unassign' do - merge_request.update(assignee_id: developer.id) + it 'populates assignee_ids: [] if content contains /unassign' do + merge_request.update(assignee_ids: [developer.id]) _, updates = service.execute(content, merge_request) - expect(updates).to eq(assignee_id: nil) + expect(updates).to eq(assignee_ids: []) end end end -- cgit v1.2.1 From 2194856fcf45c9556067a7c2fb6db677f9388c61 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Tue, 20 Jun 2017 22:02:41 +0200 Subject: Ensure /reassign does not assign multiple users Set the assignee to last user in the array if multiple assignees aren't allowed. Also, use `parse_params` where possible. --- app/services/quick_actions/interpret_service.rb | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index df13976fb3b..e4dfe87e614 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -133,10 +133,11 @@ module QuickActions issuable.assignees.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :unassign do |unassign_param = nil| + parse_params do |unassign_param| # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed - users = extract_users(unassign_param) if issuable.allows_multiple_assignees? - + extract_users(unassign_param) if issuable.allows_multiple_assignees? + end + command :unassign do |users = nil| @updates[:assignee_ids] = if users&.any? issuable.assignees.pluck(:id) - users.map(&:id) @@ -159,8 +160,16 @@ module QuickActions issuable.persisted? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :reassign do |unassign_param| - @updates[:assignee_ids] = extract_users(unassign_param).map(&:id) + parse_params do |assignee_param| + extract_users(assignee_param) + end + command :reassign do |users| + @updates[:assignee_ids] = + if issuable.allows_multiple_assignees? + users.map(&:id) + else + [users.last.id] + end end desc 'Set milestone' -- cgit v1.2.1 From d9ad56f3c5a7fc5e682ec96731b0578719934122 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 21 Jun 2017 17:30:12 +0000 Subject: Add environment_scope column to ci_variables table This is merely to make CE and EE more compatible. See the EE merge request at: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2112 --- app/models/ci/variable.rb | 2 +- ...0170612150426_add_environment_scope_to_ci_variables.rb | 15 +++++++++++++++ db/schema.rb | 1 + spec/models/ci/variable_spec.rb | 14 ++++++++------ 4 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20170612150426_add_environment_scope_to_ci_variables.rb diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index f235260208f..67444118ec5 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -6,7 +6,7 @@ module Ci validates :key, presence: true, - uniqueness: { scope: :project_id }, + uniqueness: { scope: [:project_id, :environment_scope] }, length: { maximum: 255 }, format: { with: /\A[a-zA-Z0-9_]+\z/, message: "can contain only letters, digits and '_'." } diff --git a/db/migrate/20170612150426_add_environment_scope_to_ci_variables.rb b/db/migrate/20170612150426_add_environment_scope_to_ci_variables.rb new file mode 100644 index 00000000000..17fe062d8d5 --- /dev/null +++ b/db/migrate/20170612150426_add_environment_scope_to_ci_variables.rb @@ -0,0 +1,15 @@ +class AddEnvironmentScopeToCiVariables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:ci_variables, :environment_scope, :string, default: '*') + end + + def down + remove_column(:ci_variables, :environment_scope) + end +end diff --git a/db/schema.rb b/db/schema.rb index 028556bdccf..006122bc7c7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -374,6 +374,7 @@ ActiveRecord::Schema.define(version: 20170621102400) do t.string "encrypted_value_iv" t.integer "project_id", null: false t.boolean "protected", default: false, null: false + t.string "environment_scope", default: "*", null: false end add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 83494af24ba..ad3aa660b96 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -5,12 +5,14 @@ describe Ci::Variable, models: true do let(:secret_value) { 'secret' } - it { is_expected.to validate_presence_of(:key) } - it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) } - it { is_expected.to validate_length_of(:key).is_at_most(255) } - it { is_expected.to allow_value('foo').for(:key) } - it { is_expected.not_to allow_value('foo bar').for(:key) } - it { is_expected.not_to allow_value('foo/bar').for(:key) } + describe 'validations' do + it { is_expected.to validate_presence_of(:key) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope) } + it { is_expected.to validate_length_of(:key).is_at_most(255) } + it { is_expected.to allow_value('foo').for(:key) } + it { is_expected.not_to allow_value('foo bar').for(:key) } + it { is_expected.not_to allow_value('foo/bar').for(:key) } + end describe '.unprotected' do subject { described_class.unprotected } -- cgit v1.2.1 From 5725b3769fdfbffc86bcea72d26197391f538759 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 22 Jun 2017 15:54:23 +0000 Subject: Remove duplicated records and add unique constraint --- ...150426_add_environment_scope_to_ci_variables.rb | 15 -------- .../20170622135451_remove_duplicated_variable.rb | 45 ++++++++++++++++++++++ ...135628_add_environment_scope_to_ci_variables.rb | 15 ++++++++ ...135728_add_unique_constraint_to_ci_variables.rb | 23 +++++++++++ db/schema.rb | 4 +- spec/migrations/remove_duplicated_variable_spec.rb | 25 ++++++++++++ 6 files changed, 110 insertions(+), 17 deletions(-) delete mode 100644 db/migrate/20170612150426_add_environment_scope_to_ci_variables.rb create mode 100644 db/migrate/20170622135451_remove_duplicated_variable.rb create mode 100644 db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb create mode 100644 db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb create mode 100644 spec/migrations/remove_duplicated_variable_spec.rb diff --git a/db/migrate/20170612150426_add_environment_scope_to_ci_variables.rb b/db/migrate/20170612150426_add_environment_scope_to_ci_variables.rb deleted file mode 100644 index 17fe062d8d5..00000000000 --- a/db/migrate/20170612150426_add_environment_scope_to_ci_variables.rb +++ /dev/null @@ -1,15 +0,0 @@ -class AddEnvironmentScopeToCiVariables < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - - disable_ddl_transaction! - - def up - add_column_with_default(:ci_variables, :environment_scope, :string, default: '*') - end - - def down - remove_column(:ci_variables, :environment_scope) - end -end diff --git a/db/migrate/20170622135451_remove_duplicated_variable.rb b/db/migrate/20170622135451_remove_duplicated_variable.rb new file mode 100644 index 00000000000..bd3aa3f5323 --- /dev/null +++ b/db/migrate/20170622135451_remove_duplicated_variable.rb @@ -0,0 +1,45 @@ +class RemoveDuplicatedVariable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + if Gitlab::Database.postgresql? + execute <<~SQL + DELETE FROM ci_variables var USING (#{duplicated_ids}) dup + #{join_conditions} + SQL + else + execute <<~SQL + DELETE var FROM ci_variables var INNER JOIN (#{duplicated_ids}) dup + #{join_conditions} + SQL + end + end + + def down + # noop + end + + def duplicated_ids + <<~SQL + SELECT MAX(id) AS id, #{key}, project_id + FROM ci_variables GROUP BY #{key}, project_id + SQL + end + + def join_conditions + <<~SQL + WHERE var.key = dup.key + AND var.project_id = dup.project_id + AND var.id <> dup.id + SQL + end + + def key + # key needs to be quoted in MySQL + quote_column_name('key') + end +end diff --git a/db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb b/db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb new file mode 100644 index 00000000000..17fe062d8d5 --- /dev/null +++ b/db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb @@ -0,0 +1,15 @@ +class AddEnvironmentScopeToCiVariables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:ci_variables, :environment_scope, :string, default: '*') + end + + def down + remove_column(:ci_variables, :environment_scope) + end +end diff --git a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb new file mode 100644 index 00000000000..f953cd66414 --- /dev/null +++ b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb @@ -0,0 +1,23 @@ +class AddUniqueConstraintToCiVariables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless index_exists?(:ci_variables, columns) + add_concurrent_index(:ci_variables, columns, unique: true) + end + end + + def down + if index_exists?(:ci_variables, columns) + remove_concurrent_index(:ci_variables, columns) + end + end + + def columns + @columns ||= [:project_id, :key, :environment_scope] + end +end diff --git a/db/schema.rb b/db/schema.rb index 006122bc7c7..fa66d515a0d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170621102400) do +ActiveRecord::Schema.define(version: 20170622135728) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -377,7 +377,7 @@ ActiveRecord::Schema.define(version: 20170621102400) do t.string "environment_scope", default: "*", null: false end - add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree + add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree create_table "container_repositories", force: :cascade do |t| t.integer "project_id", null: false diff --git a/spec/migrations/remove_duplicated_variable_spec.rb b/spec/migrations/remove_duplicated_variable_spec.rb new file mode 100644 index 00000000000..9a521a7d980 --- /dev/null +++ b/spec/migrations/remove_duplicated_variable_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20170622135451_remove_duplicated_variable.rb') + +describe RemoveDuplicatedVariable, :migration do + let(:variables) { table(:ci_variables) } + let(:projects) { table(:projects) } + + before do + projects.create!(id: 1) + variables.create!(id: 1, key: 'key1', project_id: 1) + variables.create!(id: 2, key: 'key2', project_id: 1) + variables.create!(id: 3, key: 'keyX', project_id: 1) + variables.create!(id: 4, key: 'keyX', project_id: 1) + variables.create!(id: 5, key: 'keyY', project_id: 1) + variables.create!(id: 6, key: 'keyX', project_id: 1) + variables.create!(id: 7, key: 'key7', project_id: 1) + variables.create!(id: 8, key: 'keyY', project_id: 1) + end + + it 'correctly remove duplicated records with smaller id' do + migrate! + + expect(variables.pluck(:id)).to contain_exactly(1, 2, 6, 7, 8) + end +end -- cgit v1.2.1 From 411bd9c5bf2eb3e1ab7a960183570a5e18371853 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 23 Jun 2017 00:14:55 +0800 Subject: Add changelog entry --- changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml diff --git a/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml b/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml new file mode 100644 index 00000000000..3414f1ed8ca --- /dev/null +++ b/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml @@ -0,0 +1,6 @@ +--- +title: Remove duplicated variables with the same key for projects. Add environment_scope + column to variables and add unique constraint to make sure that no variables could + be created with the same key within a project +merge_request: 12363 +author: -- cgit v1.2.1 From 5eb4a47204931ebf5e5f40a3653f1ba9557af2ce Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 23 Jun 2017 00:20:44 +0800 Subject: We cannot delete the index for MySQL, because fk foreign key, I mean. --- db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb index f953cd66414..69729a17054 100644 --- a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb +++ b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb @@ -12,7 +12,7 @@ class AddUniqueConstraintToCiVariables < ActiveRecord::Migration end def down - if index_exists?(:ci_variables, columns) + if index_exists?(:ci_variables, columns) && Gitlab::Database.postgresql? remove_concurrent_index(:ci_variables, columns) end end -- cgit v1.2.1 From b5175550d10d0c3ccbcfca65e20fac83a0893094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 13:41:37 +0800 Subject: Add Simplified Chinese translations of Commits Page [skip ci] --- locale/zh_CN/part.po | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 locale/zh_CN/part.po diff --git a/locale/zh_CN/part.po b/locale/zh_CN/part.po new file mode 100644 index 00000000000..9c6b820f235 --- /dev/null +++ b/locale/zh_CN/part.po @@ -0,0 +1,52 @@ +# Huang Tao , 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-23 01:04-0400\n" +"Last-Translator: Huang Tao \n" +"Language-Team: Chinese (China)\n" +"Language: zh-CN\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=1; plural=0\n" + +msgid "%d additional commit have been omitted to prevent performance issues." +msgid_plural "" +"%d additional commits have been omitted to prevent performance issues." +msgstr[0] "为提高页面加载速度及性能,已省略了 %d 次提交。" + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "%d 次提交" + +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "搜索分支" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "切换分支" + +msgid "Browse Directory" +msgstr "浏览目录" + +msgid "Browse File" +msgstr "浏览文件" + +msgid "Browse Files" +msgstr "浏览文件" + +msgid "Commits feed" +msgstr "提交动态" + +msgid "Filter by commit message" +msgstr "按提交消息过滤" + +msgid "UploadLink|click to upload" +msgstr "点击上传" + +msgid "View open merge request" +msgstr "查看开启的合并请求" + -- cgit v1.2.1 From 2a757692e4a6a068be1dc22731a40eed13629c23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 13:43:18 +0800 Subject: Add Traditional Chinese in HongKong translations of Commits Page [skip ci] --- locale/zh_HK/part.po | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 locale/zh_HK/part.po diff --git a/locale/zh_HK/part.po b/locale/zh_HK/part.po new file mode 100644 index 00000000000..60c922ff54f --- /dev/null +++ b/locale/zh_HK/part.po @@ -0,0 +1,52 @@ +# Huang Tao , 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-23 01:23-0400\n" +"Last-Translator: Huang Tao \n" +"Language-Team: Chinese (Hong Kong SAR China)\n" +"Language: zh-HK\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=1; plural=0\n" + +msgid "%d additional commit have been omitted to prevent performance issues." +msgid_plural "" +"%d additional commits have been omitted to prevent performance issues." +msgstr[0] "為提高頁面加載速度及性能,已省略了 %d 次提交。" + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] " %d 次提交" + +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "搜索分支" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "切換分支" + +msgid "Browse Directory" +msgstr "瀏覽目錄" + +msgid "Browse File" +msgstr "瀏覽文件" + +msgid "Browse Files" +msgstr "瀏覽文件" + +msgid "Commits feed" +msgstr "提交動態" + +msgid "Filter by commit message" +msgstr "按提交消息過濾" + +msgid "UploadLink|click to upload" +msgstr "點擊上傳" + +msgid "View open merge request" +msgstr "查看開啟的合並請求" + -- cgit v1.2.1 From c31db46ed1c88e4680c10a04bd504c8cb64de8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 13:43:57 +0800 Subject: Add Traditional Chinese in Taiwan translations of Commits Page [skip ci] --- locale/zh_TW/part.po | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 locale/zh_TW/part.po diff --git a/locale/zh_TW/part.po b/locale/zh_TW/part.po new file mode 100644 index 00000000000..3e4ddd07cf6 --- /dev/null +++ b/locale/zh_TW/part.po @@ -0,0 +1,52 @@ +# Huang Tao , 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-23 01:23-0400\n" +"Last-Translator: Huang Tao \n" +"Language-Team: Chinese (Taiwan)\n" +"Language: zh-TW\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=1; plural=0\n" + +msgid "%d additional commit have been omitted to prevent performance issues." +msgid_plural "" +"%d additional commits have been omitted to prevent performance issues." +msgstr[0] "為提高頁面加載速度及性能,已省略了 %d 次送交 (commit)。" + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "%d 次送交 (commit)" + +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "搜索分支 (branches)" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "切換分支 (branch)" + +msgid "Browse Directory" +msgstr "瀏覽目錄" + +msgid "Browse File" +msgstr "瀏覽檔案" + +msgid "Browse Files" +msgstr "瀏覽檔案" + +msgid "Commits feed" +msgstr "送交動態" + +msgid "Filter by commit message" +msgstr "按送交消息過濾" + +msgid "UploadLink|click to upload" +msgstr "點擊上傳" + +msgid "View open merge request" +msgstr "查看開啟的合並請求 (merge request)" + -- cgit v1.2.1 From 6e63878d65865087b13fc89d605da3918dc0b5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 13:47:11 +0800 Subject: Add Bulgarian translations of Commits Page [skip ci] --- locale/bg/part.po | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 locale/bg/part.po diff --git a/locale/bg/part.po b/locale/bg/part.po new file mode 100644 index 00000000000..66ef49a54b3 --- /dev/null +++ b/locale/bg/part.po @@ -0,0 +1,54 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: Bulgarian\n" +"Language: bg\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "%d additional commit have been omitted to prevent performance issues." +msgid_plural "" +"%d additional commits have been omitted to prevent performance issues." +msgstr[0] "" +msgstr[1] "" + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "" +msgstr[1] "" + +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "" + +msgid "Browse Directory" +msgstr "" + +msgid "Browse File" +msgstr "" + +msgid "Browse Files" +msgstr "" + +msgid "Commits feed" +msgstr "" + +msgid "Filter by commit message" +msgstr "" + +msgid "UploadLink|click to upload" +msgstr "" + +msgid "View open merge request" +msgstr "" + -- cgit v1.2.1 From 25d59953a0cb3fbe573baba27d1d30ba5bff61b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 13:49:17 +0800 Subject: Add Esperanto translations of Commits Page [skip ci] --- locale/eo/part.po | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 locale/eo/part.po diff --git a/locale/eo/part.po b/locale/eo/part.po new file mode 100644 index 00000000000..24060a0fd20 --- /dev/null +++ b/locale/eo/part.po @@ -0,0 +1,54 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: Esperanto\n" +"Language: eo\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "%d additional commit have been omitted to prevent performance issues." +msgid_plural "" +"%d additional commits have been omitted to prevent performance issues." +msgstr[0] "" +msgstr[1] "" + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "" +msgstr[1] "" + +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "" + +msgid "Browse Directory" +msgstr "" + +msgid "Browse File" +msgstr "" + +msgid "Browse Files" +msgstr "" + +msgid "Commits feed" +msgstr "" + +msgid "Filter by commit message" +msgstr "" + +msgid "UploadLink|click to upload" +msgstr "" + +msgid "View open merge request" +msgstr "" + -- cgit v1.2.1 From 7963bc363df8cfb6aa4846fe684cdd6e10683b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 14:16:31 +0800 Subject: add changelog [skip ci] --- .../34169-add-simplified-chinese-translations-of-commits-page.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/34169-add-simplified-chinese-translations-of-commits-page.yml diff --git a/changelogs/unreleased/34169-add-simplified-chinese-translations-of-commits-page.yml b/changelogs/unreleased/34169-add-simplified-chinese-translations-of-commits-page.yml new file mode 100644 index 00000000000..1a631c3f0a4 --- /dev/null +++ b/changelogs/unreleased/34169-add-simplified-chinese-translations-of-commits-page.yml @@ -0,0 +1,4 @@ +--- +title: Add Simplified Chinese translations of Commits Page +merge_request: 12405 +author: Huang Tao -- cgit v1.2.1 From f28a1d94a7bef0625e3a0aee3d00bcab74c97013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 14:19:28 +0800 Subject: add changelog [skip ci] --- ...add-traditional-chinese-in-taiwan-translations-of-commits-page.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/34172-add-traditional-chinese-in-taiwan-translations-of-commits-page.yml diff --git a/changelogs/unreleased/34172-add-traditional-chinese-in-taiwan-translations-of-commits-page.yml b/changelogs/unreleased/34172-add-traditional-chinese-in-taiwan-translations-of-commits-page.yml new file mode 100644 index 00000000000..224b9e1852f --- /dev/null +++ b/changelogs/unreleased/34172-add-traditional-chinese-in-taiwan-translations-of-commits-page.yml @@ -0,0 +1,4 @@ +--- +title: Add Traditional Chinese in Taiwan translations of Commits Page +merge_request: 12407 +author: Huang Tao -- cgit v1.2.1 From 2f4b6adec7e915da939d349c3ce3b653d8b35849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 14:18:07 +0800 Subject: add changelog [skip ci] --- ...d-traditional-chinese-in-hongkong-translations-of-commits-page.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/34171-add-traditional-chinese-in-hongkong-translations-of-commits-page.yml diff --git a/changelogs/unreleased/34171-add-traditional-chinese-in-hongkong-translations-of-commits-page.yml b/changelogs/unreleased/34171-add-traditional-chinese-in-hongkong-translations-of-commits-page.yml new file mode 100644 index 00000000000..3cf7c0b547f --- /dev/null +++ b/changelogs/unreleased/34171-add-traditional-chinese-in-hongkong-translations-of-commits-page.yml @@ -0,0 +1,4 @@ +--- +title: Add Traditional Chinese in HongKong translations of Commits Page +merge_request: 12406 +author: Huang Tao -- cgit v1.2.1 From a3695cd1b5931cbd52fe0aa3ec040238e8709953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 14:22:57 +0800 Subject: add changelog [skip ci] --- .../unreleased/34175-add-esperanto-translations-of-commits-page.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/34175-add-esperanto-translations-of-commits-page.yml diff --git a/changelogs/unreleased/34175-add-esperanto-translations-of-commits-page.yml b/changelogs/unreleased/34175-add-esperanto-translations-of-commits-page.yml new file mode 100644 index 00000000000..b43a38f3794 --- /dev/null +++ b/changelogs/unreleased/34175-add-esperanto-translations-of-commits-page.yml @@ -0,0 +1,4 @@ +--- +title: Add Esperanto translations of Commits Page +merge_request: 12410 +author: Huang Tao -- cgit v1.2.1 From d9a9f8327fc43edd70b8379d21c787023a89998e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 23 Jun 2017 14:23:43 +0800 Subject: add changelog [skip ci] --- .../unreleased/34176-add-bulgarian-translations-of-commits-page.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/34176-add-bulgarian-translations-of-commits-page.yml diff --git a/changelogs/unreleased/34176-add-bulgarian-translations-of-commits-page.yml b/changelogs/unreleased/34176-add-bulgarian-translations-of-commits-page.yml new file mode 100644 index 00000000000..9177ae3acd1 --- /dev/null +++ b/changelogs/unreleased/34176-add-bulgarian-translations-of-commits-page.yml @@ -0,0 +1,4 @@ +--- +title: Add Bulgarian translations of Commits Page +merge_request: 12411 +author: Huang Tao -- cgit v1.2.1 From d34e87818c217a1a7368852e4cba384914afedd0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 23 Jun 2017 08:23:27 +0000 Subject: Remove old project_id index and make sure mysql work --- ...622135728_add_unique_constraint_to_ci_variables.rb | 18 ++++++++++++++---- ...0623080805_remove_ci_variables_project_id_index.rb | 19 +++++++++++++++++++ db/schema.rb | 2 +- 3 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20170623080805_remove_ci_variables_project_id_index.rb diff --git a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb index 69729a17054..f7fca511e9b 100644 --- a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb +++ b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb @@ -6,18 +6,28 @@ class AddUniqueConstraintToCiVariables < ActiveRecord::Migration disable_ddl_transaction! def up - unless index_exists?(:ci_variables, columns) - add_concurrent_index(:ci_variables, columns, unique: true) + unless this_index_exists? + add_concurrent_index(:ci_variables, columns, name: index_name, unique: true) end end def down - if index_exists?(:ci_variables, columns) && Gitlab::Database.postgresql? - remove_concurrent_index(:ci_variables, columns) + if this_index_exists? && Gitlab::Database.postgresql? + remove_concurrent_index(:ci_variables, columns, name: index_name) end end + private + + def this_index_exists? + index_exists?(:ci_variables, name: index_name) + end + def columns @columns ||= [:project_id, :key, :environment_scope] end + + def index_name + 'index_ci_variables_on_project_id_and_key_and_environment_scope' + end end diff --git a/db/migrate/20170623080805_remove_ci_variables_project_id_index.rb b/db/migrate/20170623080805_remove_ci_variables_project_id_index.rb new file mode 100644 index 00000000000..ddcc0292b9d --- /dev/null +++ b/db/migrate/20170623080805_remove_ci_variables_project_id_index.rb @@ -0,0 +1,19 @@ +class RemoveCiVariablesProjectIdIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + if index_exists?(:ci_variables, :project_id) + remove_concurrent_index(:ci_variables, :project_id) + end + end + + def down + unless index_exists?(:ci_variables, :project_id) + add_concurrent_index(:ci_variables, :project_id) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fa66d515a0d..94b03219b21 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170622135728) do +ActiveRecord::Schema.define(version: 20170623080805) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" -- cgit v1.2.1 From 6ff162cfd911ccfeeabc8fd1516840b10a8f9700 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 23 Jun 2017 19:09:23 +0800 Subject: Add back project_id index for MySQL if reverting --- .../20170622135728_add_unique_constraint_to_ci_variables.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb index f7fca511e9b..8b2cc40ee59 100644 --- a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb +++ b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb @@ -12,7 +12,12 @@ class AddUniqueConstraintToCiVariables < ActiveRecord::Migration end def down - if this_index_exists? && Gitlab::Database.postgresql? + if this_index_exists? + if Gitlab::Database.mysql? && !index_exists?(:ci_variables, :project_id) + # Need to add this index for MySQL project_id foreign key constraint + add_concurrent_index(:ci_variables, :project_id) + end + remove_concurrent_index(:ci_variables, columns, name: index_name) end end @@ -20,7 +25,7 @@ class AddUniqueConstraintToCiVariables < ActiveRecord::Migration private def this_index_exists? - index_exists?(:ci_variables, name: index_name) + index_exists?(:ci_variables, columns, name: index_name) end def columns -- cgit v1.2.1 From c8bb359f8e4ba720ebe4b59b0ea0ad4fe8afa8c7 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Sat, 24 Jun 2017 22:40:53 +0200 Subject: chaining the methods in ref_slug --- app/models/ci/build.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ffe8f5a28ae..b6bfbd8e0d2 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -189,9 +189,10 @@ module Ci # * Maximum length is 63 bytes # * First/Last Character is not a hyphen def ref_slug - slugified = ref.to_s.downcase - slugified.gsub!(/[^a-z0-9]/, '-') - slugified[0..62].gsub(/(\A-+|-+\z)/, '') + ref.to_s + .downcase + .gsub(/[^a-z0-9]/, '-')[0..62] + .gsub(/(\A-+|-+\z)/, '') end # Variables whose value does not depend on other variables -- cgit v1.2.1 From a241f18b319a34e6102c7864cd10192fb79e19aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Mon, 26 Jun 2017 09:34:20 +0800 Subject: Synchronous zanata translation --- locale/bg/part.po | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/locale/bg/part.po b/locale/bg/part.po index 66ef49a54b3..a795d193da0 100644 --- a/locale/bg/part.po +++ b/locale/bg/part.po @@ -1,4 +1,4 @@ -# +# Lyubomir Vasilev , 2017. #zanata msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" @@ -7,8 +7,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: \n" -"Last-Translator: \n" +"PO-Revision-Date: 2017-06-23 04:07-0400\n" +"Last-Translator: Lyubomir Vasilev \n" "Language-Team: Bulgarian\n" "Language: bg\n" "X-Generator: Zanata 3.9.6\n" @@ -17,38 +17,38 @@ msgstr "" msgid "%d additional commit have been omitted to prevent performance issues." msgid_plural "" "%d additional commits have been omitted to prevent performance issues." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%d подаване беше пропуснато, за да не се натоварва системата." +msgstr[1] "%d подавания бяха пропуснати, за да не се натоварва системата." msgid "%d commit" msgid_plural "%d commits" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%d подаване" +msgstr[1] "%d подавания" msgid "BranchSwitcherPlaceholder|Search branches" -msgstr "" +msgstr "Търсете в клоновете" msgid "BranchSwitcherTitle|Switch branch" -msgstr "" +msgstr "Превключване на клона" msgid "Browse Directory" -msgstr "" +msgstr "Преглед на папката" msgid "Browse File" -msgstr "" +msgstr "Преглед на файла" msgid "Browse Files" -msgstr "" +msgstr "Преглед на файловете" msgid "Commits feed" -msgstr "" +msgstr "Поток от подавания" msgid "Filter by commit message" -msgstr "" +msgstr "Филтриране по съобщение" msgid "UploadLink|click to upload" -msgstr "" +msgstr "щракнете за качване" msgid "View open merge request" -msgstr "" +msgstr "Преглед на отворената заявка за сливане" -- cgit v1.2.1 From bc576ed1128cfcb82bd8523db28cabda0002f111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Mon, 26 Jun 2017 09:35:27 +0800 Subject: Synchronous zanata translation --- locale/eo/part.po | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/locale/eo/part.po b/locale/eo/part.po index 24060a0fd20..8e10c55b290 100644 --- a/locale/eo/part.po +++ b/locale/eo/part.po @@ -1,4 +1,4 @@ -# +# Lyubomir Vasilev , 2017. #zanata msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" @@ -7,8 +7,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: \n" -"Last-Translator: \n" +"PO-Revision-Date: 2017-06-23 04:07-0400\n" +"Last-Translator: Lyubomir Vasilev \n" "Language-Team: Esperanto\n" "Language: eo\n" "X-Generator: Zanata 3.9.6\n" @@ -17,38 +17,38 @@ msgstr "" msgid "%d additional commit have been omitted to prevent performance issues." msgid_plural "" "%d additional commits have been omitted to prevent performance issues." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%d enmetado estis transsaltita, por ne troŝarĝi la sistemon." +msgstr[1] "%d enmetadoj estis transsaltitaj, por ne troŝarĝi la sistemon." msgid "%d commit" msgid_plural "%d commits" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%d enmetado" +msgstr[1] "%d enmetadoj" msgid "BranchSwitcherPlaceholder|Search branches" -msgstr "" +msgstr "Serĉu branĉon" msgid "BranchSwitcherTitle|Switch branch" -msgstr "" +msgstr "Iri al branĉo" msgid "Browse Directory" -msgstr "" +msgstr "Foliumi dosierujon" msgid "Browse File" -msgstr "" +msgstr "Foliumi dosieron" msgid "Browse Files" -msgstr "" +msgstr "Foliumi dosierojn" msgid "Commits feed" -msgstr "" +msgstr "Fluo de enmetadoj" msgid "Filter by commit message" -msgstr "" +msgstr "Filtri per mesaĝo" msgid "UploadLink|click to upload" -msgstr "" +msgstr "alklaku por alŝuti" msgid "View open merge request" -msgstr "" +msgstr "Vidi la malfermitan peton pri kunfando" -- cgit v1.2.1 From 20f679d620380b5b5e662b790c76caf256867b01 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 26 Jun 2017 07:20:30 +0000 Subject: Allow unauthenticated access to the `/api/v4/users` API. - The issue filtering frontend code needs access to this API for non-logged-in users + public projects. It uses the API to fetch information for a user by username. - We don't authenticate this API anymore, but instead - if the `current_user` is not present: - Verify that the `username` parameter has been passed. This disallows an unauthenticated user from grabbing a list of all users on the instance. The `UsersFinder` class performs an exact match on the `username`, so we are guaranteed to get 0 or 1 users. - Verify that the resulting user (if any) is accessible to be viewed publicly by calling `can?(current_user, :read_user, user)` --- app/finders/users_finder.rb | 7 +++++-- lib/api/helpers.rb | 6 ++++++ lib/api/users.rb | 23 +++++++++++++++++------ spec/requests/api/users_spec.rb | 35 +++++++++++++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index dbd50d1db7c..0534317df8f 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -27,8 +27,11 @@ class UsersFinder users = by_search(users) users = by_blocked(users) users = by_active(users) - users = by_external_identity(users) - users = by_external(users) + + if current_user + users = by_external_identity(users) + users = by_external(users) + end users end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2c73a6fdc4e..1322afaa64f 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -407,5 +407,11 @@ module API exception.status == 500 end + + # Does the current route match the route identified by + # `description`? + def route_matches_description?(description) + options.dig(:route_options, :description) == description + end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index c10e3364382..34619c90d8b 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -4,7 +4,7 @@ module API before do allow_access_with_scope :read_user if request.get? - authenticate! + authenticate! unless route_matches_description?("Get the list of users") end resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do @@ -51,15 +51,26 @@ module API use :pagination end get do - unless can?(current_user, :read_users_list) - render_api_error!("Not authorized.", 403) - end - authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) users = UsersFinder.new(current_user, params).execute - entity = current_user.admin? ? Entities::UserWithAdmin : Entities::UserBasic + authorized = + if current_user + can?(current_user, :read_users_list) + else + # When `current_user` is not present, require that the `username` + # parameter is passed, to prevent an unauthenticated user from accessing + # a list of all the users on the GitLab instance. `UsersFinder` performs + # an exact match on the `username` parameter, so we are guaranteed to + # get either 0 or 1 `users` here. + params[:username].present? && + users.all? { |user| can?(current_user, :read_user, user) } + end + + render_api_error!("Not authorized.", 403) unless authorized + + entity = current_user.try(:admin?) ? Entities::UserWithAdmin : Entities::UserBasic present paginate(users), with: entity end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 18000d91795..01541901330 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -13,9 +13,40 @@ describe API::Users do describe 'GET /users' do context "when unauthenticated" do - it "returns authentication error" do + it "returns authorization error when the `username` parameter is not passed" do get api("/users") - expect(response).to have_http_status(401) + + expect(response).to have_http_status(403) + end + + it "returns the user when a valid `username` parameter is passed" do + user = create(:user) + + get api("/users"), username: user.username + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response[0]['id']).to eq(user.id) + expect(json_response[0]['username']).to eq(user.username) + end + + it "returns authorization error when the `username` parameter refers to an inaccessible user" do + user = create(:user) + + expect(Ability).to receive(:allowed?).with(nil, :read_user, user).and_return(false) + + get api("/users"), username: user.username + + expect(response).to have_http_status(403) + end + + it "returns an empty response when an invalid `username` parameter is passed" do + get api("/users"), username: 'invalid' + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(0) end end -- cgit v1.2.1 From c39e4ccfb7cb76b9bdb613399aba2c2467b77751 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 26 Jun 2017 08:43:12 +0000 Subject: Add CHANGELOG entry for CE MR 12445 --- .../34141-allow-unauthenticated-access-to-the-users-api.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml diff --git a/changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml b/changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml new file mode 100644 index 00000000000..a3ade8db214 --- /dev/null +++ b/changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml @@ -0,0 +1,4 @@ +--- +title: Allow unauthenticated access to the /api/v4/users API +merge_request: 12445 +author: -- cgit v1.2.1 From 175b7834c398546550a75b545fa42828302d656c Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Thu, 22 Jun 2017 16:19:43 +0900 Subject: Add tests for MilestonesHelper#milestones_filter_dropdown_path --- spec/helpers/milestones_helper_spec.rb | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 3cb809d42b5..24d4f1b4938 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -1,6 +1,42 @@ require 'spec_helper' describe MilestonesHelper do + describe '#milestones_filter_dropdown_path' do + let(:project) { create(:empty_project) } + let(:project2) { create(:empty_project) } + let(:group) { create(:group) } + + context 'when @project present' do + it 'returns project milestones JSON URL' do + assign(:project, project) + + expect(helper.milestones_filter_dropdown_path).to eq(namespace_project_milestones_path(project.namespace, project, :json)) + end + end + + context 'when @target_project present' do + it 'returns targeted project milestones JSON URL' do + assign(:target_project, project2) + + expect(helper.milestones_filter_dropdown_path).to eq(namespace_project_milestones_path(project2.namespace, project2, :json)) + end + end + + context 'when @group present' do + it 'returns group milestones JSON URL' do + assign(:group, group) + + expect(helper.milestones_filter_dropdown_path).to eq(group_milestones_path(group, :json)) + end + end + + context 'when neither of @project/@target_project/@group present' do + it 'returns dashboard milestones JSON URL' do + expect(helper.milestones_filter_dropdown_path).to eq(dashboard_milestones_path(:json)) + end + end + end + describe "#milestone_date_range" do def result_for(*args) milestone_date_range(build(:milestone, *args)) -- cgit v1.2.1 From 5d4ec03687852564b44ce1befccb292e11f1504b Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Fri, 23 Jun 2017 09:29:08 +0900 Subject: Add tests for Groups::MilestonesController#index --- spec/controllers/groups/milestones_controller_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index f3263bc177d..c6e5fb61cf9 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -23,6 +23,21 @@ describe Groups::MilestonesController do project.team << [user, :master] end + describe "#index" do + it 'shows group milestones page' do + get :index, group_id: group.to_param + + expect(response).to have_http_status(200) + end + + it 'shows group milestones JSON' do + get :index, group_id: group.to_param, format: :json + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'application/json' + end + end + it_behaves_like 'milestone tabs' describe "#create" do -- cgit v1.2.1 From bd874729a2110e1270d09338bb326110c880e001 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Fri, 23 Jun 2017 08:31:25 +0900 Subject: Add JSON support to group milestones --- app/controllers/groups/milestones_controller.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index e52fa766044..6b1d418fc9a 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController @milestone_states = GlobalMilestone.states_count(@projects) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end + format.json do + render json: milestones.map { |m| m.for_display.slice(:title, :name) } + end end end -- cgit v1.2.1 From b8430dddbd413c87f77ba7f02d9b0a0cd4ec42ad Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Thu, 22 Jun 2017 16:25:56 +0900 Subject: Change milestone endpoint for groups --- app/helpers/milestones_helper.rb | 2 ++ changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index a230db22fa2..f346e20e807 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -74,6 +74,8 @@ module MilestonesHelper project = @target_project || @project if project namespace_project_milestones_path(project.namespace, project, :json) + elsif @group + group_milestones_path(@group, :json) else dashboard_milestones_path(:json) end diff --git a/changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml b/changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml new file mode 100644 index 00000000000..8f8b5a96c2b --- /dev/null +++ b/changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml @@ -0,0 +1,4 @@ +--- +title: Change milestone endpoint for groups +merge_request: 12374 +author: Takuya Noguchi -- cgit v1.2.1 From 10e732d2bb1afff22a0549c74636641859cc3bde Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 27 Jun 2017 17:46:45 +0800 Subject: Rename instead of delete, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12363#note_33449374 --- .../20170622135451_remove_duplicated_variable.rb | 45 ---------------------- ...0170622135451_rename_duplicated_variable_key.rb | 35 +++++++++++++++++ spec/migrations/remove_duplicated_variable_spec.rb | 15 ++++++-- 3 files changed, 47 insertions(+), 48 deletions(-) delete mode 100644 db/migrate/20170622135451_remove_duplicated_variable.rb create mode 100644 db/migrate/20170622135451_rename_duplicated_variable_key.rb diff --git a/db/migrate/20170622135451_remove_duplicated_variable.rb b/db/migrate/20170622135451_remove_duplicated_variable.rb deleted file mode 100644 index bd3aa3f5323..00000000000 --- a/db/migrate/20170622135451_remove_duplicated_variable.rb +++ /dev/null @@ -1,45 +0,0 @@ -class RemoveDuplicatedVariable < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - - disable_ddl_transaction! - - def up - if Gitlab::Database.postgresql? - execute <<~SQL - DELETE FROM ci_variables var USING (#{duplicated_ids}) dup - #{join_conditions} - SQL - else - execute <<~SQL - DELETE var FROM ci_variables var INNER JOIN (#{duplicated_ids}) dup - #{join_conditions} - SQL - end - end - - def down - # noop - end - - def duplicated_ids - <<~SQL - SELECT MAX(id) AS id, #{key}, project_id - FROM ci_variables GROUP BY #{key}, project_id - SQL - end - - def join_conditions - <<~SQL - WHERE var.key = dup.key - AND var.project_id = dup.project_id - AND var.id <> dup.id - SQL - end - - def key - # key needs to be quoted in MySQL - quote_column_name('key') - end -end diff --git a/db/migrate/20170622135451_rename_duplicated_variable_key.rb b/db/migrate/20170622135451_rename_duplicated_variable_key.rb new file mode 100644 index 00000000000..b88e7d7ba81 --- /dev/null +++ b/db/migrate/20170622135451_rename_duplicated_variable_key.rb @@ -0,0 +1,35 @@ +class RenameDuplicatedVariableKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + execute(<<~SQL) + UPDATE ci_variables SET #{key} = CONCAT(#{key}, #{underscore}, id) + WHERE id IN ( + SELECT * FROM ( -- MySQL requires an extra layer + SELECT dup.id FROM ci_variables dup + INNER JOIN (SELECT max(id) AS id, #{key}, project_id + FROM ci_variables tmp + GROUP BY #{key}, project_id) var + USING (#{key}, project_id) where dup.id <> var.id + ) dummy + ) + SQL + end + + def down + # noop + end + + def key + # key needs to be quoted in MySQL + quote_column_name('key') + end + + def underscore + quote('_') + end +end diff --git a/spec/migrations/remove_duplicated_variable_spec.rb b/spec/migrations/remove_duplicated_variable_spec.rb index 9a521a7d980..11096564dfa 100644 --- a/spec/migrations/remove_duplicated_variable_spec.rb +++ b/spec/migrations/remove_duplicated_variable_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -require Rails.root.join('db', 'migrate', '20170622135451_remove_duplicated_variable.rb') +require Rails.root.join('db', 'migrate', '20170622135451_rename_duplicated_variable_key.rb') -describe RemoveDuplicatedVariable, :migration do +describe RenameDuplicatedVariableKey, :migration do let(:variables) { table(:ci_variables) } let(:projects) { table(:projects) } @@ -20,6 +20,15 @@ describe RemoveDuplicatedVariable, :migration do it 'correctly remove duplicated records with smaller id' do migrate! - expect(variables.pluck(:id)).to contain_exactly(1, 2, 6, 7, 8) + expect(variables.pluck(:id, :key)).to contain_exactly( + [1, 'key1'], + [2, 'key2'], + [3, 'keyX_3'], + [4, 'keyX_4'], + [5, 'keyY_5'], + [6, 'keyX'], + [7, 'key7'], + [8, 'keyY'] + ) end end -- cgit v1.2.1 From 7f3f053a7d2c720eb946c07a4b4bd09a54a72bbe Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 27 Jun 2017 18:29:23 +0800 Subject: Rename the migration test to match the migration path --- spec/migrations/remove_duplicated_variable_spec.rb | 34 ---------------------- .../rename_duplicated_variable_key_spec.rb | 34 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 34 deletions(-) delete mode 100644 spec/migrations/remove_duplicated_variable_spec.rb create mode 100644 spec/migrations/rename_duplicated_variable_key_spec.rb diff --git a/spec/migrations/remove_duplicated_variable_spec.rb b/spec/migrations/remove_duplicated_variable_spec.rb deleted file mode 100644 index 11096564dfa..00000000000 --- a/spec/migrations/remove_duplicated_variable_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' -require Rails.root.join('db', 'migrate', '20170622135451_rename_duplicated_variable_key.rb') - -describe RenameDuplicatedVariableKey, :migration do - let(:variables) { table(:ci_variables) } - let(:projects) { table(:projects) } - - before do - projects.create!(id: 1) - variables.create!(id: 1, key: 'key1', project_id: 1) - variables.create!(id: 2, key: 'key2', project_id: 1) - variables.create!(id: 3, key: 'keyX', project_id: 1) - variables.create!(id: 4, key: 'keyX', project_id: 1) - variables.create!(id: 5, key: 'keyY', project_id: 1) - variables.create!(id: 6, key: 'keyX', project_id: 1) - variables.create!(id: 7, key: 'key7', project_id: 1) - variables.create!(id: 8, key: 'keyY', project_id: 1) - end - - it 'correctly remove duplicated records with smaller id' do - migrate! - - expect(variables.pluck(:id, :key)).to contain_exactly( - [1, 'key1'], - [2, 'key2'], - [3, 'keyX_3'], - [4, 'keyX_4'], - [5, 'keyY_5'], - [6, 'keyX'], - [7, 'key7'], - [8, 'keyY'] - ) - end -end diff --git a/spec/migrations/rename_duplicated_variable_key_spec.rb b/spec/migrations/rename_duplicated_variable_key_spec.rb new file mode 100644 index 00000000000..11096564dfa --- /dev/null +++ b/spec/migrations/rename_duplicated_variable_key_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20170622135451_rename_duplicated_variable_key.rb') + +describe RenameDuplicatedVariableKey, :migration do + let(:variables) { table(:ci_variables) } + let(:projects) { table(:projects) } + + before do + projects.create!(id: 1) + variables.create!(id: 1, key: 'key1', project_id: 1) + variables.create!(id: 2, key: 'key2', project_id: 1) + variables.create!(id: 3, key: 'keyX', project_id: 1) + variables.create!(id: 4, key: 'keyX', project_id: 1) + variables.create!(id: 5, key: 'keyY', project_id: 1) + variables.create!(id: 6, key: 'keyX', project_id: 1) + variables.create!(id: 7, key: 'key7', project_id: 1) + variables.create!(id: 8, key: 'keyY', project_id: 1) + end + + it 'correctly remove duplicated records with smaller id' do + migrate! + + expect(variables.pluck(:id, :key)).to contain_exactly( + [1, 'key1'], + [2, 'key2'], + [3, 'keyX_3'], + [4, 'keyX_4'], + [5, 'keyY_5'], + [6, 'keyX'], + [7, 'key7'], + [8, 'keyY'] + ) + end +end -- cgit v1.2.1 From f6314bdba5feff48a68e0323744ac635220ff635 Mon Sep 17 00:00:00 2001 From: Stefan Hanreich Date: Tue, 27 Jun 2017 13:39:31 +0200 Subject: updated changelog entry --- changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml index f5ade3536d3..bbcf2946ea7 100644 --- a/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml +++ b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml @@ -1,4 +1,4 @@ --- -title: Omit trailing / leding hyphens in CI_COMMIT_REF_SLUG variable +title: Omit trailing / leading hyphens in CI_COMMIT_REF_SLUG variable to make it usable as a hostname merge_request: 11218 author: Stefan Hanreich -- cgit v1.2.1 From 42a9eea79186c02f1fdccec886d1202adb33fcf6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 27 Jun 2017 21:14:38 +0800 Subject: Better indent the SQL --- .../20170622135451_rename_duplicated_variable_key.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/db/migrate/20170622135451_rename_duplicated_variable_key.rb b/db/migrate/20170622135451_rename_duplicated_variable_key.rb index b88e7d7ba81..1005a212131 100644 --- a/db/migrate/20170622135451_rename_duplicated_variable_key.rb +++ b/db/migrate/20170622135451_rename_duplicated_variable_key.rb @@ -7,15 +7,18 @@ class RenameDuplicatedVariableKey < ActiveRecord::Migration def up execute(<<~SQL) - UPDATE ci_variables SET #{key} = CONCAT(#{key}, #{underscore}, id) + UPDATE ci_variables + SET #{key} = CONCAT(#{key}, #{underscore}, id) WHERE id IN ( - SELECT * FROM ( -- MySQL requires an extra layer - SELECT dup.id FROM ci_variables dup - INNER JOIN (SELECT max(id) AS id, #{key}, project_id - FROM ci_variables tmp - GROUP BY #{key}, project_id) var + SELECT * + FROM ( -- MySQL requires an extra layer + SELECT dup.id + FROM ci_variables dup + INNER JOIN (SELECT max(id) AS id, #{key}, project_id + FROM ci_variables tmp + GROUP BY #{key}, project_id) var USING (#{key}, project_id) where dup.id <> var.id - ) dummy + ) dummy ) SQL end -- cgit v1.2.1 From b4d325c80c63ee9ee2676a57a42fac472b5b20d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Jun 2017 16:49:51 +0200 Subject: Allow the feature flags to be enabled/disabled with more granularity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows to enable/disable a feature flag for a given user, or a given Flipper group (must be declared statically in the `flipper.rb` initializer beforehand). Signed-off-by: Rémy Coutable --- app/models/concerns/flippable.rb | 7 + app/models/user.rb | 1 + ...-enable-feature-flags-with-more-granularity.yml | 4 + doc/api/features.md | 2 + lib/api/features.rb | 38 ++++- lib/feature.rb | 22 ++- spec/models/user_spec.rb | 14 ++ spec/requests/api/features_spec.rb | 178 ++++++++++++++++++--- 8 files changed, 233 insertions(+), 33 deletions(-) create mode 100644 app/models/concerns/flippable.rb create mode 100644 changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml diff --git a/app/models/concerns/flippable.rb b/app/models/concerns/flippable.rb new file mode 100644 index 00000000000..341501e8250 --- /dev/null +++ b/app/models/concerns/flippable.rb @@ -0,0 +1,7 @@ +module Flippable + def flipper_id + return nil if new_record? + + "#{self.class.name}:#{id}" + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 6dd1b1415d6..bcce260ab08 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,6 +11,7 @@ class User < ActiveRecord::Base include CaseSensitivity include TokenAuthenticatable include IgnorableColumn + include Flippable DEFAULT_NOTIFICATION_LEVEL = :participating diff --git a/changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml b/changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml new file mode 100644 index 00000000000..8ead1b404f3 --- /dev/null +++ b/changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml @@ -0,0 +1,4 @@ +--- +title: Allow the feature flags to be enabled/disabled with more granularity +merge_request: +author: diff --git a/doc/api/features.md b/doc/api/features.md index 89b8d3ac948..0ca2e637614 100644 --- a/doc/api/features.md +++ b/doc/api/features.md @@ -58,6 +58,8 @@ POST /features/:name | --------- | ---- | -------- | ----------- | | `name` | string | yes | Name of the feature to create or update | | `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time | +| `flipper_group` | string | no | A Flipper group name | +| `user` | string | no | A GitLab username | ```bash curl --data "value=30" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/new_library diff --git a/lib/api/features.rb b/lib/api/features.rb index cff0ba2ddff..4e10e36fce9 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -2,6 +2,29 @@ module API class Features < Grape::API before { authenticated_as_admin! } + helpers do + def gate_value(params) + case params[:value] + when 'true' + true + when '0', 'false' + false + else + params[:value].to_i + end + end + + def gate_target(params) + if params[:flipper_group] + Feature.group(params[:flipper_group]) + elsif params[:user] + User.find_by_username(params[:user]) + else + gate_value(params) + end + end + end + resource :features do desc 'Get a list of all features' do success Entities::Feature @@ -17,16 +40,21 @@ module API end params do requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' + optional :flipper_group, type: String, desc: 'A Flipper group name' + optional :user, type: String, desc: 'A GitLab username' end post ':name' do feature = Feature.get(params[:name]) + target = gate_target(params) + value = gate_value(params) - if %w(0 false).include?(params[:value]) - feature.disable - elsif params[:value] == 'true' - feature.enable + case value + when true + feature.enable(target) + when false + feature.disable(target) else - feature.enable_percentage_of_time(params[:value].to_i) + feature.enable_percentage_of_time(value) end present feature, with: Entities::Feature, current_user: current_user diff --git a/lib/feature.rb b/lib/feature.rb index d3d972564af..363f66ba60e 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -12,6 +12,8 @@ class Feature end class << self + delegate :group, to: :flipper + def all flipper.features.to_a end @@ -27,16 +29,24 @@ class Feature all.map(&:name).include?(feature.name) end - def enabled?(key) - get(key).enabled? + def enabled?(key, thing = nil) + get(key).enabled?(thing) + end + + def enable(key, thing = true) + get(key).enable(thing) + end + + def disable(key, thing = false) + get(key).disable(thing) end - def enable(key) - get(key).enable + def enable_group(key, group) + get(key).enable_group(group) end - def disable(key) - get(key).disable + def disable_group(key, group) + get(key).disable_group(group) end def flipper diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8e895ec6634..05ba887c51f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -430,6 +430,20 @@ describe User, models: true do end end + describe '#flipper_id' do + context 'when user is not persisted' do + let(:user) { build(:user) } + + it { expect(user.flipper_id).to be_nil } + end + + context 'when user is persisted' do + let(:user) { create(:user) } + + it { expect(user.flipper_id).to eq "User:#{user.id}" } + end + end + describe '#generate_password' do it "does not generate password by default" do user = create(:user, password: 'abcdefghe') diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index f169e6661d1..0ee0749c7a1 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -4,6 +4,13 @@ describe API::Features do let(:user) { create(:user) } let(:admin) { create(:admin) } + before do + Flipper.unregister_groups + Flipper.register(:perf_team) do |actor| + actor.respond_to?(:admin) && actor.admin? + end + end + describe 'GET /features' do let(:expected_features) do [ @@ -16,6 +23,14 @@ describe API::Features do 'name' => 'feature_2', 'state' => 'off', 'gates' => [{ 'key' => 'boolean', 'value' => false }] + }, + { + 'name' => 'feature_3', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'groups', 'value' => ['perf_team'] } + ] } ] end @@ -23,6 +38,7 @@ describe API::Features do before do Feature.get('feature_1').enable Feature.get('feature_2').disable + Feature.get('feature_3').enable Feature.group(:perf_team) end it 'returns a 401 for anonymous users' do @@ -47,30 +63,70 @@ describe API::Features do describe 'POST /feature' do let(:feature_name) { 'my_feature' } - it 'returns a 401 for anonymous users' do - post api("/features/#{feature_name}") - expect(response).to have_http_status(401) - end + context 'when the feature does not exist' do + it 'returns a 401 for anonymous users' do + post api("/features/#{feature_name}") - it 'returns a 403 for users' do - post api("/features/#{feature_name}", user) + expect(response).to have_http_status(401) + end - expect(response).to have_http_status(403) - end + it 'returns a 403 for users' do + post api("/features/#{feature_name}", user) - it 'creates an enabled feature if passed true' do - post api("/features/#{feature_name}", admin), value: 'true' + expect(response).to have_http_status(403) + end - expect(response).to have_http_status(201) - expect(Feature.get(feature_name)).to be_enabled - end + context 'when passed value=true' do + it 'creates an enabled feature' do + post api("/features/#{feature_name}", admin), value: 'true' - it 'creates a feature with the given percentage if passed an integer' do - post api("/features/#{feature_name}", admin), value: '50' + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'on', + 'gates' => [{ 'key' => 'boolean', 'value' => true }]) + end + + it 'creates an enabled feature for the given Flipper group when passed flipper_group=perf_team' do + post api("/features/#{feature_name}", admin), value: 'true', flipper_group: 'perf_team' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'groups', 'value' => ['perf_team'] } + ]) + end + + it 'creates an enabled feature for the given user when passed user=username' do + post api("/features/#{feature_name}", admin), value: 'true', user: user.username - expect(response).to have_http_status(201) - expect(Feature.get(feature_name).percentage_of_time_value).to be(50) + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["User:#{user.id}"] } + ]) + end + end + + it 'creates a feature with the given percentage if passed an integer' do + post api("/features/#{feature_name}", admin), value: '50' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'percentage_of_time', 'value' => 50 } + ]) + end end context 'when the feature exists' do @@ -80,11 +136,83 @@ describe API::Features do feature.disable # This also persists the feature on the DB end - it 'enables the feature if passed true' do - post api("/features/#{feature_name}", admin), value: 'true' + context 'when passed value=true' do + it 'enables the feature' do + post api("/features/#{feature_name}", admin), value: 'true' - expect(response).to have_http_status(201) - expect(feature).to be_enabled + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'on', + 'gates' => [{ 'key' => 'boolean', 'value' => true }]) + end + + it 'enables the feature for the given Flipper group when passed flipper_group=perf_team' do + post api("/features/#{feature_name}", admin), value: 'true', flipper_group: 'perf_team' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'groups', 'value' => ['perf_team'] } + ]) + end + + it 'enables the feature for the given user when passed user=username' do + post api("/features/#{feature_name}", admin), value: 'true', user: user.username + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["User:#{user.id}"] } + ]) + end + end + + context 'when feature is enabled and value=false is passed' do + it 'disables the feature' do + feature.enable + expect(feature).to be_enabled + + post api("/features/#{feature_name}", admin), value: 'false' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'off', + 'gates' => [{ 'key' => 'boolean', 'value' => false }]) + end + + it 'disables the feature for the given Flipper group when passed flipper_group=perf_team' do + feature.enable(Feature.group(:perf_team)) + expect(Feature.get(feature_name).enabled?(admin)).to be_truthy + + post api("/features/#{feature_name}", admin), value: 'false', flipper_group: 'perf_team' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'off', + 'gates' => [{ 'key' => 'boolean', 'value' => false }]) + end + + it 'disables the feature for the given user when passed user=username' do + feature.enable(user) + expect(Feature.get(feature_name).enabled?(user)).to be_truthy + + post api("/features/#{feature_name}", admin), value: 'false', user: user.username + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'off', + 'gates' => [{ 'key' => 'boolean', 'value' => false }]) + end end context 'with a pre-existing percentage value' do @@ -96,7 +224,13 @@ describe API::Features do post api("/features/#{feature_name}", admin), value: '30' expect(response).to have_http_status(201) - expect(Feature.get(feature_name).percentage_of_time_value).to be(30) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'percentage_of_time', 'value' => 30 } + ]) end end end -- cgit v1.2.1 From 5fa9d6a17dac86e9976946ded7857e1392403136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 27 Jun 2017 18:58:56 +0200 Subject: Rename FLippable to FeatureGate and make `flipper_group` and `user` mutually exclusive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/models/concerns/feature_gate.rb | 7 +++++++ app/models/concerns/flippable.rb | 7 ------- app/models/user.rb | 2 +- doc/api/features.md | 2 ++ lib/api/features.rb | 1 + spec/models/concerns/feature_gate_spec.rb | 19 +++++++++++++++++++ spec/models/user_spec.rb | 14 -------------- 7 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 app/models/concerns/feature_gate.rb delete mode 100644 app/models/concerns/flippable.rb create mode 100644 spec/models/concerns/feature_gate_spec.rb diff --git a/app/models/concerns/feature_gate.rb b/app/models/concerns/feature_gate.rb new file mode 100644 index 00000000000..5db64fe82c4 --- /dev/null +++ b/app/models/concerns/feature_gate.rb @@ -0,0 +1,7 @@ +module FeatureGate + def flipper_id + return nil if new_record? + + "#{self.class.name}:#{id}" + end +end diff --git a/app/models/concerns/flippable.rb b/app/models/concerns/flippable.rb deleted file mode 100644 index 341501e8250..00000000000 --- a/app/models/concerns/flippable.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Flippable - def flipper_id - return nil if new_record? - - "#{self.class.name}:#{id}" - end -end diff --git a/app/models/user.rb b/app/models/user.rb index bcce260ab08..e08096284ef 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,7 +11,7 @@ class User < ActiveRecord::Base include CaseSensitivity include TokenAuthenticatable include IgnorableColumn - include Flippable + include FeatureGate DEFAULT_NOTIFICATION_LEVEL = :participating diff --git a/doc/api/features.md b/doc/api/features.md index 0ca2e637614..a3bf5d018a7 100644 --- a/doc/api/features.md +++ b/doc/api/features.md @@ -61,6 +61,8 @@ POST /features/:name | `flipper_group` | string | no | A Flipper group name | | `user` | string | no | A GitLab username | +Note that `flipper_group` and `user` are mutually exclusive. + ```bash curl --data "value=30" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/new_library ``` diff --git a/lib/api/features.rb b/lib/api/features.rb index 4e10e36fce9..e426bc050eb 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -42,6 +42,7 @@ module API requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' optional :flipper_group, type: String, desc: 'A Flipper group name' optional :user, type: String, desc: 'A GitLab username' + mutually_exclusive :flipper_group, :user end post ':name' do feature = Feature.get(params[:name]) diff --git a/spec/models/concerns/feature_gate_spec.rb b/spec/models/concerns/feature_gate_spec.rb new file mode 100644 index 00000000000..3f601243245 --- /dev/null +++ b/spec/models/concerns/feature_gate_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe FeatureGate do + describe 'User' do + describe '#flipper_id' do + context 'when user is not persisted' do + let(:user) { build(:user) } + + it { expect(user.flipper_id).to be_nil } + end + + context 'when user is persisted' do + let(:user) { create(:user) } + + it { expect(user.flipper_id).to eq "User:#{user.id}" } + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 05ba887c51f..8e895ec6634 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -430,20 +430,6 @@ describe User, models: true do end end - describe '#flipper_id' do - context 'when user is not persisted' do - let(:user) { build(:user) } - - it { expect(user.flipper_id).to be_nil } - end - - context 'when user is persisted' do - let(:user) { create(:user) } - - it { expect(user.flipper_id).to eq "User:#{user.id}" } - end - end - describe '#generate_password' do it "does not generate password by default" do user = create(:user, password: 'abcdefghe') -- cgit v1.2.1 From 502bd6c033aaf05eba23122681852b63a90ad4a6 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Tue, 27 Jun 2017 12:35:13 -0500 Subject: Fixed sidebar not collapsing on merge request in mobile screens --- app/assets/javascripts/merge_request_tabs.js | 1 - .../unreleased/fix-sidebar-showing-mobile-merge-requests.yml | 4 ++++ spec/features/issues/issue_sidebar_spec.rb | 7 +++++-- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 786b6014dc6..c25d9a95d14 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -144,7 +144,6 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; this.resetViewContainer(); this.mountPipelinesView(); } else { - this.expandView(); this.resetViewContainer(); this.destroyPipelinesView(); } diff --git a/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml new file mode 100644 index 00000000000..856990a6126 --- /dev/null +++ b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml @@ -0,0 +1,4 @@ +--- +title: Fixed sidebar not collapsing on merge requests in mobile screens +merge_request: +author: diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 163bc4bb32f..cd06b5af675 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -6,6 +6,7 @@ feature 'Issue Sidebar', feature: true do let(:group) { create(:group, :nested) } let(:project) { create(:project, :public, namespace: group) } let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, source_project: project) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project, title: 'bug') } @@ -158,11 +159,13 @@ feature 'Issue Sidebar', feature: true do before do project.team << [user, :developer] resize_screen_xs - visit_issue(project, issue) end context 'mobile sidebar' do - it 'collapses the sidebar for small screens' do + it 'collapses the sidebar for small screens on an issue/merge_request' do + visit_issue(project, issue) + expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') end end -- cgit v1.2.1 From 6f1922500bc9e2c6d53c46dfcbd420687dfe6e6b Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 20 Jun 2017 07:40:24 +0000 Subject: Initial attempt at refactoring API scope declarations. - Declaring an endpoint's scopes in a `before` block has proved to be unreliable. For example, if we're accessing the `API::Users` endpoint - code in a `before` block in `API::API` wouldn't be able to see the scopes set in `API::Users` since the `API::API` `before` block runs first. - This commit moves these declarations to the class level, since they don't need to change once set. --- app/services/access_token_validation_service.rb | 5 +++- lib/api/api.rb | 3 ++- lib/api/api_guard.rb | 33 ++++++++++++++++--------- lib/api/helpers.rb | 6 +++-- lib/api/users.rb | 4 ++- lib/api/v3/users.rb | 4 ++- spec/requests/api/users_spec.rb | 22 +++++++++++++++++ spec/support/api_helpers.rb | 6 +++-- 8 files changed, 63 insertions(+), 20 deletions(-) diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index b2a543daa00..f171f8194bd 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -31,8 +31,11 @@ class AccessTokenValidationService if scopes.blank? true else + #scopes = scopes.reject { |scope| scope[:if].presence && !scope[:if].call(request) } # Check whether the token is allowed access to any of the required scopes. - Set.new(scopes).intersection(Set.new(token.scopes)).present? + + scope_names = scopes.map { |scope| scope[:name].to_s } + Set.new(scope_names).intersection(Set.new(token.scopes)).present? end end end diff --git a/lib/api/api.rb b/lib/api/api.rb index d767af36e8e..efcf0976a81 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -2,6 +2,8 @@ module API class API < Grape::API include APIGuard + allow_access_with_scope :api + version %w(v3 v4), using: :path version 'v3', using: :path do @@ -44,7 +46,6 @@ module API mount ::API::V3::Variables end - before { allow_access_with_scope :api } before { header['X-Frame-Options'] = 'SAMEORIGIN' } before { Gitlab::I18n.locale = current_user&.preferred_language } diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 9fcf04efa38..9a9e32a0242 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -23,6 +23,27 @@ module API install_error_responders(base) end + class_methods do + # Set the authorization scope(s) allowed for the current request. + # + # A call to this method adds to any previous scopes in place, either from the same class, or + # higher up in the inheritance chain. For example, if we call `allow_access_with_scope :api` from + # `API::API`, and `allow_access_with_scope :read_user` from `API::Users` (which inherits from `API::API`), + # `API::Users` will allow access with either the `api` or `read_user` scope. `API::API` will allow + # access only with the `api` scope. + def allow_access_with_scope(scopes, options = {}) + @scopes ||= [] + + params = Array.wrap(scopes).map { |scope| { name: scope, if: options[:if] } } + + @scopes.concat(params) + end + + def scopes + @scopes + end + end + # Helper Methods for Grape Endpoint module HelperMethods # Invokes the doorkeeper guard. @@ -74,18 +95,6 @@ module API @current_user end - # Set the authorization scope(s) allowed for the current request. - # - # Note: A call to this method adds to any previous scopes in place. This is done because - # `Grape` callbacks run from the outside-in: the top-level callback (API::API) runs first, then - # the next-level callback (API::API::Users, for example) runs. All these scopes are valid for the - # given endpoint (GET `/api/users` is accessible by the `api` and `read_user` scopes), and so they - # need to be stored. - def allow_access_with_scope(*scopes) - @scopes ||= [] - @scopes.concat(scopes.map(&:to_s)) - end - private def find_user_by_authentication_token(token_string) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2c73a6fdc4e..3cf04e6df3c 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -340,10 +340,12 @@ module API end def initial_current_user + endpoint_class = options[:for] + return @initial_current_user if defined?(@initial_current_user) Gitlab::Auth::UniqueIpsLimiter.limit_user! do - @initial_current_user ||= find_user_by_private_token(scopes: @scopes) - @initial_current_user ||= doorkeeper_guard(scopes: @scopes) + @initial_current_user ||= find_user_by_private_token(scopes: endpoint_class.scopes) + @initial_current_user ||= doorkeeper_guard(scopes: endpoint_class.scopes) @initial_current_user ||= find_user_from_warden unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? diff --git a/lib/api/users.rb b/lib/api/users.rb index f9555842daf..2cac8c089f2 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1,9 +1,11 @@ module API class Users < Grape::API include PaginationParams + include APIGuard + + allow_access_with_scope :read_user, if: -> (request) { request.get? } before do - allow_access_with_scope :read_user if request.get? authenticate! end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index 37020019e07..cf106f2552d 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -2,9 +2,11 @@ module API module V3 class Users < Grape::API include PaginationParams + include APIGuard + + allow_access_with_scope :read_user, if: -> (request) { request.get? } before do - allow_access_with_scope :read_user if request.get? authenticate! end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index c0174b304c8..c8e22799ba4 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -50,6 +50,28 @@ describe API::Users do end['username']).to eq(username) end + context "scopes" do + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user']) } + + it 'returns a "200" response' do + get api("/users", user, personal_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token does not have any required scope' do + let(:token) { create(:personal_access_token, scopes: ['read_registry']) } + + it 'returns a "401" response' do + get api("/users", user, personal_access_token: token) + + expect(response).to have_http_status(401) + end + end + end + it "returns an array of blocked users" do ldap_blocked_user create(:user, state: 'blocked') diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 35d1e1cfc7d..163979a2a28 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -17,14 +17,16 @@ module ApiHelpers # => "/api/v2/issues?foo=bar&private_token=..." # # Returns the relative path to the requested API resource - def api(path, user = nil, version: API::API.version) + def api(path, user = nil, version: API::API.version, personal_access_token: nil) "/api/#{version}#{path}" + # Normalize query string (path.index('?') ? '' : '?') + + if personal_access_token.present? + "&private_token=#{personal_access_token.token}" # Append private_token if given a User object - if user.respond_to?(:private_token) + elsif user.respond_to?(:private_token) "&private_token=#{user.private_token}" else '' -- cgit v1.2.1 From 80c1ebaa83f346e45346baac584f21878652c350 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 20 Jun 2017 08:27:45 +0000 Subject: Allow API scope declarations to be applied conditionally. - Scope declarations of the form: allow_access_with_scope :read_user, if: -> (request) { request.get? } will only apply for `GET` requests - Add a negative test to a `POST` endpoint in the `users` API to test this. Also test for this case in the `AccessTokenValidationService` unit tests. --- app/services/access_token_validation_service.rb | 15 +++++---- lib/api/api_guard.rb | 4 +-- lib/api/helpers.rb | 2 +- spec/requests/api/helpers_spec.rb | 3 +- spec/requests/api/users_spec.rb | 10 ++++++ .../access_token_validation_service_spec.rb | 36 ++++++++++++++++++---- 6 files changed, 54 insertions(+), 16 deletions(-) diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index f171f8194bd..6d39ad245d2 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -5,10 +5,11 @@ class AccessTokenValidationService REVOKED = :revoked INSUFFICIENT_SCOPE = :insufficient_scope - attr_reader :token + attr_reader :token, :request - def initialize(token) + def initialize(token, request) @token = token + @request = request end def validate(scopes: []) @@ -31,11 +32,13 @@ class AccessTokenValidationService if scopes.blank? true else - #scopes = scopes.reject { |scope| scope[:if].presence && !scope[:if].call(request) } - # Check whether the token is allowed access to any of the required scopes. + # Remove any scopes whose `if` condition does not return `true` + scopes = scopes.reject { |scope| scope[:if].presence && !scope[:if].call(request) } - scope_names = scopes.map { |scope| scope[:name].to_s } - Set.new(scope_names).intersection(Set.new(token.scopes)).present? + # Check whether the token is allowed access to any of the required scopes. + passed_scope_names = scopes.map { |scope| scope[:name].to_sym } + token_scope_names = token.scopes.map(&:to_sym) + Set.new(passed_scope_names).intersection(Set.new(token_scope_names)).present? end end end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 9a9e32a0242..ceeecbbc00b 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -68,7 +68,7 @@ module API access_token = find_access_token return nil unless access_token - case AccessTokenValidationService.new(access_token).validate(scopes: scopes) + case AccessTokenValidationService.new(access_token, request).validate(scopes: scopes) when AccessTokenValidationService::INSUFFICIENT_SCOPE raise InsufficientScopeError.new(scopes) @@ -105,7 +105,7 @@ module API access_token = PersonalAccessToken.active.find_by_token(token_string) return unless access_token - if AccessTokenValidationService.new(access_token).include_any_scope?(scopes) + if AccessTokenValidationService.new(access_token, request).include_any_scope?(scopes) User.find(access_token.user_id) end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3cf04e6df3c..c69e7afea8c 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -340,7 +340,7 @@ module API end def initial_current_user - endpoint_class = options[:for] + endpoint_class = options[:for].presence || ::API::API return @initial_current_user if defined?(@initial_current_user) Gitlab::Auth::UniqueIpsLimiter.limit_user! do diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 191c60aba31..87d6f46533e 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -14,6 +14,8 @@ describe API::Helpers do let(:request) { Rack::Request.new(env) } let(:header) { } + before { allow_any_instance_of(self.class).to receive(:options).and_return({}) } + def set_env(user_or_token, identifier) clear_env clear_param @@ -167,7 +169,6 @@ describe API::Helpers do it "returns nil for a token without the appropriate scope" do personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_access_with_scope('write_user') expect(current_user).to be_nil end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index c8e22799ba4..982c1a50e3b 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -321,6 +321,16 @@ describe API::Users do .to eq([Gitlab::PathRegex.namespace_format_message]) end + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: admin) } + + it 'returns a "401" response' do + post api("/users", admin, personal_access_token: token), attributes_for(:user, projects_limit: 3) + + expect(response).to have_http_status(401) + end + end + it "is not available for non admin users" do post api("/users", user), attributes_for(:user) expect(response).to have_http_status(403) diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index 87f093ee8ce..0023678dc3b 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -2,40 +2,64 @@ require 'spec_helper' describe AccessTokenValidationService, services: true do describe ".include_any_scope?" do + let(:request) { double("request") } + it "returns true if the required scope is present in the token's scopes" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token).include_any_scope?([:api])).to be(true) + expect(described_class.new(token, request).include_any_scope?([{ name: :api }])).to be(true) end it "returns true if more than one of the required scopes is present in the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true) + expect(described_class.new(token, request).include_any_scope?([{ name: :api }, { name: :other_scope }])).to be(true) end it "returns true if the list of required scopes is an exact match for the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + expect(described_class.new(token, request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true) end it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + expect(described_class.new(token, request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true) end it 'returns true if the list of required scopes is blank' do token = double("token", scopes: []) - expect(described_class.new(token).include_any_scope?([])).to be(true) + expect(described_class.new(token, request).include_any_scope?([])).to be(true) end it "returns false if there are no scopes in common between the required scopes and the token scopes" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false) + expect(described_class.new(token, request).include_any_scope?([{ name: :other_scope }])).to be(false) + end + + context "conditions" do + context "if" do + it "ignores any scopes whose `if` condition returns false" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token, request).include_any_scope?([{ name: :api, if: ->(_) { false } }])).to be(false) + end + + it "does not ignore scopes whose `if` condition is not set" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token, request).include_any_scope?([{ name: :api, if: ->(_) { false } }, { name: :read_user }])).to be(true) + end + + it "does not ignore scopes whose `if` condition returns true" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token, request).include_any_scope?([{ name: :api, if: ->(_) { true } }, { name: :read_user, if: ->(_) { false } }])).to be(true) + end + end end end end -- cgit v1.2.1 From 157c05f49da1d6992d6b491e4fba8d90a7d821c8 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 20 Jun 2017 09:35:59 +0000 Subject: Test `/users` endpoints for the `read_user` scope. - Test `GET` endpoints to check that the scope is allowed. - Test `POST` endpoints to check that the scope is disallowed. - Test both `v3` and `v4` endpoints. --- spec/requests/api/users_spec.rb | 54 +++++++++------------- spec/requests/api/v3/users_spec.rb | 21 +++++++++ .../api/scopes/read_user_shared_examples.rb | 33 +++++++++++++ spec/support/api_helpers.rb | 4 +- 4 files changed, 78 insertions(+), 34 deletions(-) create mode 100644 spec/support/api/scopes/read_user_shared_examples.rb diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 982c1a50e3b..f0edc06cd0b 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -50,28 +50,6 @@ describe API::Users do end['username']).to eq(username) end - context "scopes" do - context 'when the requesting token has the "read_user" scope' do - let(:token) { create(:personal_access_token, scopes: ['read_user']) } - - it 'returns a "200" response' do - get api("/users", user, personal_access_token: token) - - expect(response).to have_http_status(200) - end - end - - context 'when the requesting token does not have any required scope' do - let(:token) { create(:personal_access_token, scopes: ['read_registry']) } - - it 'returns a "401" response' do - get api("/users", user, personal_access_token: token) - - expect(response).to have_http_status(401) - end - end - end - it "returns an array of blocked users" do ldap_blocked_user create(:user, state: 'blocked') @@ -104,6 +82,13 @@ describe API::Users do expect(json_response.first.keys).not_to include 'is_admin' end + + context "scopes" do + let(:path) { "/users" } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end end context "when admin" do @@ -186,6 +171,13 @@ describe API::Users do expect(response).to have_http_status(404) end + + context "scopes" do + let(:path) { "/users/#{user.id}" } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end end describe "POST /users" do @@ -321,16 +313,6 @@ describe API::Users do .to eq([Gitlab::PathRegex.namespace_format_message]) end - context 'when the requesting token has the "read_user" scope' do - let(:token) { create(:personal_access_token, scopes: ['read_user'], user: admin) } - - it 'returns a "401" response' do - post api("/users", admin, personal_access_token: token), attributes_for(:user, projects_limit: 3) - - expect(response).to have_http_status(401) - end - end - it "is not available for non admin users" do post api("/users", user), attributes_for(:user) expect(response).to have_http_status(403) @@ -377,6 +359,14 @@ describe API::Users do expect(json_response['identities'].first['provider']).to eq('github') end end + + context "scopes" do + let(:user) { admin } + let(:path) { '/users' } + let(:api_call) { method(:api) } + + include_examples 'does not allow the "read_user" scope' + end end describe "GET /users/sign_up" do diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index 6d7401f9764..b2c5003c97a 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -67,6 +67,19 @@ describe API::V3::Users do expect(json_response.first['title']).to eq(key.title) end end + + context "scopes" do + let(:user) { admin } + let(:path) { "/users/#{user.id}/keys" } + let(:api_call) { method(:v3_api) } + + before do + user.keys << key + user.save + end + + include_examples 'allows the "read_user" scope' + end end describe 'GET /user/:id/emails' do @@ -312,5 +325,13 @@ describe API::V3::Users do expect(json_response['is_admin']).to be_nil end + + context "scopes" do + let(:user) { admin } + let(:path) { '/users' } + let(:api_call) { method(:v3_api) } + + include_examples 'does not allow the "read_user" scope' + end end end diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb new file mode 100644 index 00000000000..bb5f493f3fd --- /dev/null +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -0,0 +1,33 @@ +shared_examples_for 'allows the "read_user" scope' do + describe 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + + it 'returns a "200" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(200) + end + end + + describe 'when the requesting token does not have any required scope' do + let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) } + + it 'returns a "401" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(401) + end + end +end + +shared_examples_for 'does not allow the "read_user" scope' do + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + + it 'returns a "401" response' do + post api_call.call(path, user, personal_access_token: token), attributes_for(:user, projects_limit: 3) + + expect(response).to have_http_status(401) + end + end +end diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 163979a2a28..1becd302d77 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -34,8 +34,8 @@ module ApiHelpers end # Temporary helper method for simplifying V3 exclusive API specs - def v3_api(path, user = nil) - api(path, user, version: 'v3') + def v3_api(path, user = nil, personal_access_token: nil) + api(path, user, version: 'v3', personal_access_token: personal_access_token) end def ci_api(path, user = nil) -- cgit v1.2.1 From d774825f981a73263c9a6c276c672b0c3e9bf104 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 20 Jun 2017 12:00:57 +0000 Subject: When verifying scopes, manually include scopes from `API::API`. - They are not included automatically since `API::Users` does not inherit from `API::API`, as I initially assumed. - Scopes declared in `API::API` are considered global (to the API), and need to be included in all cases. --- lib/api/api_guard.rb | 10 ++++------ lib/api/helpers.rb | 23 +++++++++++++++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index ceeecbbc00b..29ca760ec25 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -24,13 +24,11 @@ module API end class_methods do - # Set the authorization scope(s) allowed for the current request. + # Set the authorization scope(s) allowed for an API endpoint. # - # A call to this method adds to any previous scopes in place, either from the same class, or - # higher up in the inheritance chain. For example, if we call `allow_access_with_scope :api` from - # `API::API`, and `allow_access_with_scope :read_user` from `API::Users` (which inherits from `API::API`), - # `API::Users` will allow access with either the `api` or `read_user` scope. `API::API` will allow - # access only with the `api` scope. + # A call to this method maps the given scope(s) to the current API + # endpoint class. If this method is called multiple times on the same class, + # the scopes are all aggregated. def allow_access_with_scope(scopes, options = {}) @scopes ||= [] diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index c69e7afea8c..5c0b82587ab 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -340,12 +340,10 @@ module API end def initial_current_user - endpoint_class = options[:for].presence || ::API::API - return @initial_current_user if defined?(@initial_current_user) Gitlab::Auth::UniqueIpsLimiter.limit_user! do - @initial_current_user ||= find_user_by_private_token(scopes: endpoint_class.scopes) - @initial_current_user ||= doorkeeper_guard(scopes: endpoint_class.scopes) + @initial_current_user ||= find_user_by_private_token(scopes: scopes_registered_for_endpoint) + @initial_current_user ||= doorkeeper_guard(scopes: scopes_registered_for_endpoint) @initial_current_user ||= find_user_from_warden unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? @@ -409,5 +407,22 @@ module API exception.status == 500 end + + # An array of scopes that were registered (using `allow_access_with_scope`) + # for the current endpoint class. It also returns scopes registered on + # `API::API`, since these are meant to apply to all API routes. + def scopes_registered_for_endpoint + @scopes_registered_for_endpoint ||= + begin + endpoint_classes = [options[:for].presence, ::API::API].compact + endpoint_classes.reduce([]) do |memo, endpoint| + if endpoint.respond_to?(:scopes) + memo.concat(endpoint.scopes) + else + memo + end + end + end + end end end -- cgit v1.2.1 From 0ff1d161920a083e07b5f1629aa395642609b251 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 20 Jun 2017 12:02:25 +0000 Subject: Test OAuth token scope verification in the `API::Users` endpoint --- spec/requests/api/helpers_spec.rb | 4 +- .../api/scopes/read_user_shared_examples.rb | 67 ++++++++++++++++++---- spec/support/api_helpers.rb | 14 ++++- 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 87d6f46533e..25ec44fa036 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -14,7 +14,9 @@ describe API::Helpers do let(:request) { Rack::Request.new(env) } let(:header) { } - before { allow_any_instance_of(self.class).to receive(:options).and_return({}) } + before do + allow_any_instance_of(self.class).to receive(:options).and_return({}) + end def set_env(user_or_token, identifier) clear_env diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb index bb5f493f3fd..cae6099a0c2 100644 --- a/spec/support/api/scopes/read_user_shared_examples.rb +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -1,21 +1,68 @@ shared_examples_for 'allows the "read_user" scope' do - describe 'when the requesting token has the "read_user" scope' do - let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + context 'for personal access tokens' do + context 'when the requesting token has the "api" scope' do + let(:token) { create(:personal_access_token, scopes: ['api'], user: user) } + + it 'returns a "200" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } - it 'returns a "200" response' do - get api_call.call(path, user, personal_access_token: token) + it 'returns a "200" response' do + get api_call.call(path, user, personal_access_token: token) - expect(response).to have_http_status(200) + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token does not have any required scope' do + let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) } + + it 'returns a "401" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(401) + end end end - describe 'when the requesting token does not have any required scope' do - let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) } + context 'for doorkeeper (OAuth) tokens' do + let!(:user) {create(:user)} + let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } - it 'returns a "401" response' do - get api_call.call(path, user, personal_access_token: token) + context 'when the requesting token has the "api" scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } - expect(response).to have_http_status(401) + it 'returns a "200" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token has the "read_user" scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "read_user" } + + it 'returns a "200" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token does not have any required scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "invalid" } + + it 'returns a "403" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_http_status(403) + end end end end diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 1becd302d77..ac0aaa524b7 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -17,7 +17,7 @@ module ApiHelpers # => "/api/v2/issues?foo=bar&private_token=..." # # Returns the relative path to the requested API resource - def api(path, user = nil, version: API::API.version, personal_access_token: nil) + def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil) "/api/#{version}#{path}" + # Normalize query string @@ -25,6 +25,8 @@ module ApiHelpers if personal_access_token.present? "&private_token=#{personal_access_token.token}" + elsif oauth_access_token.present? + "&access_token=#{oauth_access_token.token}" # Append private_token if given a User object elsif user.respond_to?(:private_token) "&private_token=#{user.private_token}" @@ -34,8 +36,14 @@ module ApiHelpers end # Temporary helper method for simplifying V3 exclusive API specs - def v3_api(path, user = nil, personal_access_token: nil) - api(path, user, version: 'v3', personal_access_token: personal_access_token) + def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil) + api( + path, + user, + version: 'v3', + personal_access_token: personal_access_token, + oauth_access_token: oauth_access_token + ) end def ci_api(path, user = nil) -- cgit v1.2.1 From 8b399b185cf72f396be8d6b7caae37f2a3aa4279 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 21 Jun 2017 07:13:36 +0000 Subject: Add CHANGELOG entry for CE MR 12300 --- changelogs/unreleased/33580-fix-api-scoping.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/33580-fix-api-scoping.yml diff --git a/changelogs/unreleased/33580-fix-api-scoping.yml b/changelogs/unreleased/33580-fix-api-scoping.yml new file mode 100644 index 00000000000..f4ebb13c082 --- /dev/null +++ b/changelogs/unreleased/33580-fix-api-scoping.yml @@ -0,0 +1,4 @@ +--- +title: Fix API Scoping +merge_request: 12300 +author: -- cgit v1.2.1 From 1b8223dd51345f6075172a92dab610f9dee89d84 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 21 Jun 2017 09:22:39 +0000 Subject: Fix remaining spec failures for !12300. 1. Get the spec for `lib/gitlab/auth.rb` passing. - Make the `request` argument to `AccessTokenValidationService` optional - `auth.rb` doesn't need to pass in a request. - Pass in scopes in the format `[{ name: 'api' }]` rather than `['api']`, which is what `AccessTokenValidationService` now expects. 2. Get the spec for `API::V3::Users` passing 2. Get the spec for `AccessTokenValidationService` passing --- app/services/access_token_validation_service.rb | 2 +- lib/api/api_guard.rb | 4 ++-- lib/gitlab/auth.rb | 4 ++-- spec/requests/api/v3/users_spec.rb | 2 +- spec/services/access_token_validation_service_spec.rb | 18 +++++++++--------- spec/support/api/scopes/read_user_shared_examples.rb | 1 - 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index 6d39ad245d2..450e90d947d 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -7,7 +7,7 @@ class AccessTokenValidationService attr_reader :token, :request - def initialize(token, request) + def initialize(token, request: nil) @token = token @request = request end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 29ca760ec25..d4599aaeed0 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -66,7 +66,7 @@ module API access_token = find_access_token return nil unless access_token - case AccessTokenValidationService.new(access_token, request).validate(scopes: scopes) + case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) when AccessTokenValidationService::INSUFFICIENT_SCOPE raise InsufficientScopeError.new(scopes) @@ -103,7 +103,7 @@ module API access_token = PersonalAccessToken.active.find_by_token(token_string) return unless access_token - if AccessTokenValidationService.new(access_token, request).include_any_scope?(scopes) + if AccessTokenValidationService.new(access_token, request: request).include_any_scope?(scopes) User.find(access_token.user_id) end end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 3933c3b04dd..37ac8ecc2f0 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -130,13 +130,13 @@ module Gitlab token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password) - if token && valid_scoped_token?(token, AVAILABLE_SCOPES.map(&:to_s)) + if token && valid_scoped_token?(token, AVAILABLE_SCOPES.map { |scope| { name: scope.to_s }}) Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes)) end end def valid_oauth_token?(token) - token && token.accessible? && valid_scoped_token?(token, ["api"]) + token && token.accessible? && valid_scoped_token?(token, [{ name: "api" }]) end def valid_scoped_token?(token, scopes) diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index b2c5003c97a..de7499a4e43 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -300,7 +300,7 @@ describe API::V3::Users do end it 'returns a 404 error if not found' do - get v3_api('/users/42/events', user) + get v3_api('/users/420/events', user) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index 0023678dc3b..eff4269a4d5 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -7,37 +7,37 @@ describe AccessTokenValidationService, services: true do it "returns true if the required scope is present in the token's scopes" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token, request).include_any_scope?([{ name: :api }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api }])).to be(true) end it "returns true if more than one of the required scopes is present in the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - expect(described_class.new(token, request).include_any_scope?([{ name: :api }, { name: :other_scope }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api }, { name: :other_scope }])).to be(true) end it "returns true if the list of required scopes is an exact match for the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - expect(described_class.new(token, request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true) end it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token, request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true) end it 'returns true if the list of required scopes is blank' do token = double("token", scopes: []) - expect(described_class.new(token, request).include_any_scope?([])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?([])).to be(true) end it "returns false if there are no scopes in common between the required scopes and the token scopes" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token, request).include_any_scope?([{ name: :other_scope }])).to be(false) + expect(described_class.new(token, request: request).include_any_scope?([{ name: :other_scope }])).to be(false) end context "conditions" do @@ -45,19 +45,19 @@ describe AccessTokenValidationService, services: true do it "ignores any scopes whose `if` condition returns false" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token, request).include_any_scope?([{ name: :api, if: ->(_) { false } }])).to be(false) + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { false } }])).to be(false) end it "does not ignore scopes whose `if` condition is not set" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token, request).include_any_scope?([{ name: :api, if: ->(_) { false } }, { name: :read_user }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { false } }, { name: :read_user }])).to be(true) end it "does not ignore scopes whose `if` condition returns true" do token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token, request).include_any_scope?([{ name: :api, if: ->(_) { true } }, { name: :read_user, if: ->(_) { false } }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { true } }, { name: :read_user, if: ->(_) { false } }])).to be(true) end end end diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb index cae6099a0c2..3bd589d64b9 100644 --- a/spec/support/api/scopes/read_user_shared_examples.rb +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -32,7 +32,6 @@ shared_examples_for 'allows the "read_user" scope' do end context 'for doorkeeper (OAuth) tokens' do - let!(:user) {create(:user)} let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } context 'when the requesting token has the "api" scope' do -- cgit v1.2.1 From 4dbfa14e160e0d9bca11941adcf04b3d272aa1a2 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 23 Jun 2017 11:18:44 +0000 Subject: Implement review comments from @dbalexandre for !12300. --- lib/api/api_guard.rb | 12 +++++------ lib/api/helpers.rb | 4 ++-- .../access_token_validation_service_spec.rb | 24 ++++++++++------------ 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index d4599aaeed0..8411ad8ec34 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -30,15 +30,13 @@ module API # endpoint class. If this method is called multiple times on the same class, # the scopes are all aggregated. def allow_access_with_scope(scopes, options = {}) - @scopes ||= [] - - params = Array.wrap(scopes).map { |scope| { name: scope, if: options[:if] } } - - @scopes.concat(params) + Array(scopes).each do |scope| + allowed_scopes << { name: scope, if: options[:if] } + end end - def scopes - @scopes + def allowed_scopes + @scopes ||= [] end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 5c0b82587ab..a2a661b205c 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -416,8 +416,8 @@ module API begin endpoint_classes = [options[:for].presence, ::API::API].compact endpoint_classes.reduce([]) do |memo, endpoint| - if endpoint.respond_to?(:scopes) - memo.concat(endpoint.scopes) + if endpoint.respond_to?(:allowed_scopes) + memo.concat(endpoint.allowed_scopes) else memo end diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index eff4269a4d5..c8189aa14d8 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -41,24 +41,22 @@ describe AccessTokenValidationService, services: true do end context "conditions" do - context "if" do - it "ignores any scopes whose `if` condition returns false" do - token = double("token", scopes: [:api, :read_user]) + it "ignores any scopes whose `if` condition returns false" do + token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { false } }])).to be(false) - end + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { false } }])).to be(false) + end - it "does not ignore scopes whose `if` condition is not set" do - token = double("token", scopes: [:api, :read_user]) + it "does not ignore scopes whose `if` condition is not set" do + token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { false } }, { name: :read_user }])).to be(true) - end + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { false } }, { name: :read_user }])).to be(true) + end - it "does not ignore scopes whose `if` condition returns true" do - token = double("token", scopes: [:api, :read_user]) + it "does not ignore scopes whose `if` condition returns true" do + token = double("token", scopes: [:api, :read_user]) - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { true } }, { name: :read_user, if: ->(_) { false } }])).to be(true) - end + expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { true } }, { name: :read_user, if: ->(_) { false } }])).to be(true) end end end -- cgit v1.2.1 From c1fcd730cc9dbee5b41ce2a6a12f8d84416b1a4a Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 26 Jun 2017 04:14:10 +0000 Subject: Implement review comments from @DouweM for !12300. - Use a struct for scopes, so we can call `scope.if` instead of `scope[:if]` - Refactor the "remove scopes whose :if condition returns false" logic to use a `select` rather than a `reject`. --- app/services/access_token_validation_service.rb | 4 +-- lib/api/api_guard.rb | 2 +- lib/gitlab/auth.rb | 5 ++-- .../access_token_validation_service_spec.rb | 31 +++++++++++++++------- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index 450e90d947d..ee2e93a0d63 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -33,10 +33,10 @@ class AccessTokenValidationService true else # Remove any scopes whose `if` condition does not return `true` - scopes = scopes.reject { |scope| scope[:if].presence && !scope[:if].call(request) } + scopes = scopes.select { |scope| scope.if.nil? || scope.if.call(request) } # Check whether the token is allowed access to any of the required scopes. - passed_scope_names = scopes.map { |scope| scope[:name].to_sym } + passed_scope_names = scopes.map { |scope| scope.name.to_sym } token_scope_names = token.scopes.map(&:to_sym) Set.new(passed_scope_names).intersection(Set.new(token_scope_names)).present? end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 8411ad8ec34..56f6da57555 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -31,7 +31,7 @@ module API # the scopes are all aggregated. def allow_access_with_scope(scopes, options = {}) Array(scopes).each do |scope| - allowed_scopes << { name: scope, if: options[:if] } + allowed_scopes << OpenStruct.new(name: scope.to_sym, if: options[:if]) end end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 37ac8ecc2f0..ec73255b20a 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -130,16 +130,17 @@ module Gitlab token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password) - if token && valid_scoped_token?(token, AVAILABLE_SCOPES.map { |scope| { name: scope.to_s }}) + if token && valid_scoped_token?(token, AVAILABLE_SCOPES) Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes)) end end def valid_oauth_token?(token) - token && token.accessible? && valid_scoped_token?(token, [{ name: "api" }]) + token && token.accessible? && valid_scoped_token?(token, ['api']) end def valid_scoped_token?(token, scopes) + scopes = scopes.map { |scope| OpenStruct.new(name: scope) } AccessTokenValidationService.new(token).include_any_scope?(scopes) end diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index c8189aa14d8..279f4ed93ac 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -1,62 +1,75 @@ require 'spec_helper' describe AccessTokenValidationService, services: true do + def scope(data) + OpenStruct.new(data) + end + describe ".include_any_scope?" do let(:request) { double("request") } it "returns true if the required scope is present in the token's scopes" do token = double("token", scopes: [:api, :read_user]) + scopes = [scope({ name: :api })] - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if more than one of the required scopes is present in the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) + scopes = [scope({ name: :api }), scope({ name: :other_scope })] - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api }, { name: :other_scope }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes is an exact match for the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) + scopes = [scope({ name: :api }), scope({ name: :read_user }), scope({ name: :other_scope })] - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do token = double("token", scopes: [:api, :read_user]) + scopes = [scope({ name: :api }), scope({ name: :read_user }), scope({ name: :other_scope })] - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api }, { name: :read_user }, { name: :other_scope }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it 'returns true if the list of required scopes is blank' do token = double("token", scopes: []) + scopes = [] - expect(described_class.new(token, request: request).include_any_scope?([])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns false if there are no scopes in common between the required scopes and the token scopes" do token = double("token", scopes: [:api, :read_user]) + scopes = [scope({ name: :other_scope })] - expect(described_class.new(token, request: request).include_any_scope?([{ name: :other_scope }])).to be(false) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(false) end context "conditions" do it "ignores any scopes whose `if` condition returns false" do token = double("token", scopes: [:api, :read_user]) + scopes = [scope({ name: :api, if: ->(_) { false } })] - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { false } }])).to be(false) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(false) end it "does not ignore scopes whose `if` condition is not set" do token = double("token", scopes: [:api, :read_user]) + scopes = [scope({ name: :api, if: ->(_) { false } }), scope({ name: :read_user })] - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { false } }, { name: :read_user }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "does not ignore scopes whose `if` condition returns true" do token = double("token", scopes: [:api, :read_user]) + scopes = [scope({ name: :api, if: ->(_) { true } }), scope({ name: :read_user, if: ->(_) { false } })] - expect(described_class.new(token, request: request).include_any_scope?([{ name: :api, if: ->(_) { true } }, { name: :read_user, if: ->(_) { false } }])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end end end -- cgit v1.2.1 From 6e0cf082e5a42520c844e779d09cf61b08ebaa11 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 28 Jun 2017 16:26:58 +0800 Subject: Inline what it was before for the regexp and message --- app/models/project.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 916393db776..0601f7fb977 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -187,8 +187,8 @@ class Project < ActiveRecord::Base 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 }, + format: { without: /\.{2}/.freeze, + message: 'cannot include directory traversal.' }, length: { maximum: 255 }, allow_blank: true validates :name, -- cgit v1.2.1 From 322b495889da023464ac998b0a23f75d691287f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Wed, 28 Jun 2017 18:06:21 +0800 Subject: supplement traditional chinese in taiwan translation Fix #33443 --- ...ional_chinese_in_taiwan_translation_of_i18n.yml | 4 + locale/zh_TW/gitlab.po | 965 +++++++++++++++++++-- 2 files changed, 883 insertions(+), 86 deletions(-) create mode 100644 changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml diff --git a/changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml b/changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml new file mode 100644 index 00000000000..d6b1b2524c6 --- /dev/null +++ b/changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml @@ -0,0 +1,4 @@ +--- +title: Supplement Traditional Chinese in Taiwan translation of Project Page & Repository Page +merge_request: 12514 +author: Huang Tao diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index 5130572d7ed..799b50c086c 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -1,128 +1,460 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the gitlab package. -# FIRST AUTHOR , YEAR. -# +# Huang Tao , 2017. #zanata +# Lin Jen-Shin , 2017. +# Hazel Yang , 2017. +# TzeKei Lee , 2017. +# Jerry Ho , 2017. msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2017-05-04 19:24-0500\n" -"Last-Translator: HuangTao , 2017\n" -"Language-Team: Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/751" -"77/zh_TW/)\n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: zh_TW\n" -"Plural-Forms: nplurals=1; plural=0;\n" +"PO-Revision-Date: 2017-06-27 11:32-0400\n" +"Last-Translator: Huang Tao \n" +"Language-Team: Chinese (Taiwan) (https://translate.zanata.org/project/view/GitLab)\n" +"Language: zh-TW\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=1; plural=0\n" + +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "%{commit_author_link} 在 %{commit_timeago} 送交" + +msgid "About auto deploy" +msgstr "關於自動部署" + +msgid "Active" +msgstr "啟用" + +msgid "Activity" +msgstr "活動" + +msgid "Add Changelog" +msgstr "新增更新日誌" + +msgid "Add Contribution guide" +msgstr "新增協作指南" + +msgid "Add License" +msgstr "新增授權條款" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "請先新增 SSH 金鑰到您的個人帳號,才能使用 SSH 來上傳 (push) 或下載 (pull) 。" + +msgid "Add new directory" +msgstr "新增目錄" + +msgid "Archived project! Repository is read-only" +msgstr "此專案已封存!檔案庫 (repository) 為唯讀狀態" msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "確定要刪除此流水線 (pipeline) 排程嗎?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "拖放檔案到此處或者 %{upload_link}" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "分支 (branch) " + +msgid "" +"Branch %{branch_name} was created. To set up auto deploy, " +"choose a GitLab CI Yaml template and commit your changes. " +"%{link_to_autodeploy_doc}" msgstr "" +"已建立分支 (branch) %{branch_name} 。如要設定自動部署, 請選擇合適的 GitLab CI " +"Yaml 模板,然後記得要送交 (commit) 您的編輯內容。%{link_to_autodeploy_doc}\n" + +msgid "Branches" +msgstr "分支 (branch) " + +msgid "Browse files" +msgstr "瀏覽檔案" msgid "ByAuthor|by" -msgstr "作者:" +msgstr "作者:" + +msgid "CI configuration" +msgstr "CI 組態" msgid "Cancel" -msgstr "" +msgstr "取消" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "挑選到分支 (branch) " + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "還原分支 (branch) " + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "優選" + +msgid "ChangeTypeAction|Revert" +msgstr "還原" + +msgid "Changelog" +msgstr "更新日誌" + +msgid "Charts" +msgstr "統計圖" + +msgid "Cherry-pick this commit" +msgstr "挑選此更動記錄 (commit) " + +msgid "Cherry-pick this merge request" +msgstr "挑選此合併請求 (merge request) " + +msgid "CiStatusLabel|canceled" +msgstr "已取消" + +msgid "CiStatusLabel|created" +msgstr "已建立" + +msgid "CiStatusLabel|failed" +msgstr "失敗" + +msgid "CiStatusLabel|manual action" +msgstr "手動操作" + +msgid "CiStatusLabel|passed" +msgstr "已通過" + +msgid "CiStatusLabel|passed with warnings" +msgstr "通過,但有警告訊息" + +msgid "CiStatusLabel|pending" +msgstr "等待中" + +msgid "CiStatusLabel|skipped" +msgstr "已跳過" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "等待手動操作" + +msgid "CiStatusText|blocked" +msgstr "已阻擋" + +msgid "CiStatusText|canceled" +msgstr "已取消" + +msgid "CiStatusText|created" +msgstr "已建立" + +msgid "CiStatusText|failed" +msgstr "失敗" + +msgid "CiStatusText|manual" +msgstr "手動操作" + +msgid "CiStatusText|passed" +msgstr "已通過" + +msgid "CiStatusText|pending" +msgstr "等待中" + +msgid "CiStatusText|skipped" +msgstr "已跳過" + +msgid "CiStatus|running" +msgstr "執行中" msgid "Commit" msgid_plural "Commits" -msgstr[0] "送交" +msgstr[0] "更動記錄 (commit) " + +msgid "Commit message" +msgstr "更動說明 (commit) " + +msgid "CommitBoxTitle|Commit" +msgstr "送交" + +msgid "CommitMessage|Add %{file_name}" +msgstr "建立 %{file_name}" + +msgid "Commits" +msgstr "更動記錄 (commit) " + +msgid "Commits|History" +msgstr "過去更動 (commit) " + +msgid "Committed by" +msgstr "送交者為 " + +msgid "Compare" +msgstr "比較" + +msgid "Contribution guide" +msgstr "協作指南" + +msgid "Contributors" +msgstr "協作者" + +msgid "Copy URL to clipboard" +msgstr "複製網址到剪貼簿" + +msgid "Copy commit SHA to clipboard" +msgstr "複製更動記錄 (commit) 的 SHA 值到剪貼簿" + +msgid "Create New Directory" +msgstr "建立新目錄" + +msgid "Create directory" +msgstr "建立目錄" + +msgid "Create empty bare repository" +msgstr "建立一個新的 bare repository" + +msgid "Create merge request" +msgstr "發出合併請求 (merge request) " + +msgid "Create new..." +msgstr "建立..." + +msgid "CreateNewFork|Fork" +msgstr "分支 (fork) " + +msgid "CreateTag|Tag" +msgstr "建立標籤" msgid "Cron Timezone" +msgstr "Cron 時區" + +msgid "Cron syntax" +msgstr "Cron 語法" + +msgid "Custom notification events" +msgstr "自訂事件通知" + +msgid "" +"Custom notification levels are the same as participating levels. With custom " +"notification levels you will also receive notifications for select events. " +"To find out more, check out %{notification_link}." msgstr "" +"自訂通知層級相當於參與度設定。使用自訂通知層級,您可以只收到特定的事件通知。請參照 %{notification_link} 以獲得更多訊息。" -msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." -msgstr "週期分析概述了你的專案從想法到產品實現,各階段所需的時間。" +msgid "Cycle Analytics" +msgstr "週期分析" + +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." +msgstr "週期分析讓您可以有效的釐清專案從發想到產品推出所花的時間長短。" msgid "CycleAnalyticsStage|Code" msgstr "程式開發" msgid "CycleAnalyticsStage|Issue" -msgstr "議題" +msgstr "議題 (issue) " msgid "CycleAnalyticsStage|Plan" msgstr "計劃" msgid "CycleAnalyticsStage|Production" -msgstr "上線" +msgstr "營運" msgid "CycleAnalyticsStage|Review" msgstr "複閱" msgid "CycleAnalyticsStage|Staging" -msgstr "預備" +msgstr "試營運" msgid "CycleAnalyticsStage|Test" msgstr "測試" +msgid "Define a custom pattern with cron syntax" +msgstr "使用 Cron 語法自訂排程" + msgid "Delete" -msgstr "" +msgstr "刪除" msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" msgid "Description" -msgstr "" +msgstr "描述" + +msgid "Directory name" +msgstr "目錄名稱" + +msgid "Don't show again" +msgstr "不再顯示" + +msgid "Download" +msgstr "下載" + +msgid "Download tar" +msgstr "下載 tar" + +msgid "Download tar.bz2" +msgstr "下載 tar.bz2" + +msgid "Download tar.gz" +msgstr "下載 tar.gz" + +msgid "Download zip" +msgstr "下載 zip" + +msgid "DownloadArtifacts|Download" +msgstr "下載" + +msgid "DownloadCommit|Email Patches" +msgstr "電子郵件修補檔案 (patch)" + +msgid "DownloadCommit|Plain Diff" +msgstr "差異檔 (diff)" + +msgid "DownloadSource|Download" +msgstr "下載原始碼" msgid "Edit" -msgstr "" +msgstr "編輯" msgid "Edit Pipeline Schedule %{id}" -msgstr "" +msgstr "編輯 %{id} 流水線 (pipeline) 排程" + +msgid "Every day (at 4:00am)" +msgstr "每日執行(淩晨四點)" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "每月執行(每月一日淩晨四點)" + +msgid "Every week (Sundays at 4:00am)" +msgstr "每週執行(週日淩晨 四點)" msgid "Failed to change the owner" -msgstr "" +msgstr "無法變更所有權" msgid "Failed to remove the pipeline schedule" -msgstr "" +msgstr "無法刪除流水線 (pipeline) 排程" -msgid "Filter" -msgstr "" +msgid "Files" +msgstr "檔案" + +msgid "Find by path" +msgstr "以路徑搜尋" + +msgid "Find file" +msgstr "搜尋檔案" msgid "FirstPushedBy|First" -msgstr "首次推送" +msgstr "首次推送 (push) " msgid "FirstPushedBy|pushed by" -msgstr "推送者:" +msgstr "推送者 (push) :" + +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "分支 (fork) " + +msgid "ForkedFromProjectPath|Forked from" +msgstr "分支 (fork) 自" msgid "From issue creation until deploy to production" -msgstr "從議題建立至線上部署" +msgstr "從議題 (issue) 建立直到部署至營運環境" msgid "From merge request merge until deploy to production" -msgstr "從請求被合併後至線上部署" +msgstr "從請求被合併後 (merge request merged) 直到部署至營運環境" + +msgid "Go to your fork" +msgstr "前往您的分支 (fork) " + +msgid "GoToYourFork|Fork" +msgstr "前往您的分支 (fork) " + +msgid "Home" +msgstr "首頁" + +msgid "Housekeeping successfully started" +msgstr "已開始維護" + +msgid "Import repository" +msgstr "匯入檔案庫 (repository)" msgid "Interval Pattern" -msgstr "" +msgstr "循環週期" msgid "Introducing Cycle Analytics" msgstr "週期分析簡介" +msgid "LFSStatus|Disabled" +msgstr "停用" + +msgid "LFSStatus|Enabled" +msgstr "啟用" + msgid "Last %d day" msgid_plural "Last %d days" -msgstr[0] "最後 %d 天" +msgstr[0] "最近 %d 天" msgid "Last Pipeline" -msgstr "" +msgstr "最新流水線 (pipeline) " + +msgid "Last Update" +msgstr "最後更新" + +msgid "Last commit" +msgstr "最後更動記錄 (commit) " + +msgid "Learn more in the" +msgstr "了解更多" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "流水線 (pipeline) 排程說明文件" + +msgid "Leave group" +msgstr "退出群組" + +msgid "Leave project" +msgstr "退出專案" msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" -msgstr[0] "最多顯示 %d 個事件" +msgstr[0] "限制最多顯示 %d 個事件" msgid "Median" msgstr "中位數" +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "新增 SSH 金鑰" + msgid "New Issue" msgid_plural "New Issues" -msgstr[0] "新議題" +msgstr[0] "建立議題 (issue) " msgid "New Pipeline Schedule" -msgstr "" +msgstr "建立流水線 (pipeline) 排程" + +msgid "New branch" +msgstr "新分支 (branch) " + +msgid "New directory" +msgstr "新增目錄" + +msgid "New file" +msgstr "新增檔案" + +msgid "New issue" +msgstr "新增議題 (issue) " + +msgid "New merge request" +msgstr "新增合併請求 (merge request) " + +msgid "New schedule" +msgstr "新增排程" + +msgid "New snippet" +msgstr "新文字片段" + +msgid "New tag" +msgstr "新增標籤" + +msgid "No repository" +msgstr "找不到檔案庫 (repository)" msgid "No schedules" -msgstr "" +msgstr "沒有排程" msgid "Not available" msgstr "無法使用" @@ -130,135 +462,502 @@ msgstr "無法使用" msgid "Not enough data" msgstr "資料不足" +msgid "Notification events" +msgstr "事件通知" + +msgid "NotificationEvent|Close issue" +msgstr "關閉議題 (issue) " + +msgid "NotificationEvent|Close merge request" +msgstr "關閉合併請求 (merge request) " + +msgid "NotificationEvent|Failed pipeline" +msgstr "流水線 (pipeline) 失敗" + +msgid "NotificationEvent|Merge merge request" +msgstr "合併請求 (merge request) 被合併" + +msgid "NotificationEvent|New issue" +msgstr "新增議題 (issue) " + +msgid "NotificationEvent|New merge request" +msgstr "新增合併請求 (merge request) " + +msgid "NotificationEvent|New note" +msgstr "新增評論" + +msgid "NotificationEvent|Reassign issue" +msgstr "重新指派議題 (issue) " + +msgid "NotificationEvent|Reassign merge request" +msgstr "重新指派合併請求 (merge request) " + +msgid "NotificationEvent|Reopen issue" +msgstr "重啟議題 (issue)" + +msgid "NotificationEvent|Successful pipeline" +msgstr "流水線 (pipeline) 成功完成" + +msgid "NotificationLevel|Custom" +msgstr "自訂" + +msgid "NotificationLevel|Disabled" +msgstr "停用" + +msgid "NotificationLevel|Global" +msgstr "全域" + +msgid "NotificationLevel|On mention" +msgstr "提及" + +msgid "NotificationLevel|Participate" +msgstr "參與" + +msgid "NotificationLevel|Watch" +msgstr "關注" + +msgid "OfSearchInADropdown|Filter" +msgstr "篩選" + msgid "OpenedNDaysAgo|Opened" msgstr "開始於" +msgid "Options" +msgstr "選項" + msgid "Owner" -msgstr "" +msgstr "所有權" + +msgid "Pipeline" +msgstr "流水線 (pipeline) " msgid "Pipeline Health" -msgstr "流水線健康指標" +msgstr "流水線 (pipeline) 健康指數" msgid "Pipeline Schedule" -msgstr "" +msgstr "流水線 (pipeline) 排程" msgid "Pipeline Schedules" -msgstr "" +msgstr "流水線 (pipeline) 排程" msgid "PipelineSchedules|Activated" -msgstr "" +msgstr "是否啟用" msgid "PipelineSchedules|Active" -msgstr "" +msgstr "已啟用" msgid "PipelineSchedules|All" -msgstr "" +msgstr "所有" msgid "PipelineSchedules|Inactive" -msgstr "" +msgstr "未啟用" msgid "PipelineSchedules|Next Run" -msgstr "" +msgstr "下次執行時間" msgid "PipelineSchedules|None" -msgstr "" +msgstr "無" msgid "PipelineSchedules|Provide a short description for this pipeline" -msgstr "" +msgstr "請簡單說明此流水線 (pipeline) " msgid "PipelineSchedules|Take ownership" -msgstr "" +msgstr "取得所有權" msgid "PipelineSchedules|Target" -msgstr "" +msgstr "目標" + +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "自訂" + +msgid "Pipeline|with stage" +msgstr "於階段" + +msgid "Pipeline|with stages" +msgstr "於階段" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "專案 '%{project_name}' 已加入刪除佇列。" + +msgid "Project '%{project_name}' was successfully created." +msgstr "專案 '%{project_name}' 建立完成。" + +msgid "Project '%{project_name}' was successfully updated." +msgstr "專案 '%{project_name}' 更新完成。" + +msgid "Project '%{project_name}' will be deleted." +msgstr "專案 '%{project_name}' 將被刪除。" + +msgid "Project access must be granted explicitly to each user." +msgstr "專案權限必須一一指派給每個使用者。" + +msgid "Project export could not be deleted." +msgstr "匯出的專案無法被刪除。" + +msgid "Project export has been deleted." +msgstr "匯出的專案已被刪除。" + +msgid "" +"Project export link has expired. Please generate a new export from your " +"project settings." +msgstr "專案的匯出連結已失效。請到專案設定中產生新的連結。" + +msgid "Project export started. A download link will be sent by email." +msgstr "專案導出已開始。完成後下載連結會送到您的信箱。" + +msgid "Project home" +msgstr "專案首頁" + +msgid "ProjectFeature|Disabled" +msgstr "停用" + +msgid "ProjectFeature|Everyone with access" +msgstr "任何人都可存取" + +msgid "ProjectFeature|Only team members" +msgstr "只有團隊成員可以存取" + +msgid "ProjectFileTree|Name" +msgstr "名稱" + +msgid "ProjectLastActivity|Never" +msgstr "從未" msgid "ProjectLifecycle|Stage" -msgstr "專案生命週期" +msgstr "階段" + +msgid "ProjectNetworkGraph|Graph" +msgstr "分支圖" msgid "Read more" -msgstr "了解更多" +msgstr "瞭解更多" + +msgid "Readme" +msgstr "說明檔" + +msgid "RefSwitcher|Branches" +msgstr "分支 (branch) " + +msgid "RefSwitcher|Tags" +msgstr "標籤" msgid "Related Commits" -msgstr "相關的送交" +msgstr "相關的更動記錄 (commit) " msgid "Related Deployed Jobs" msgstr "相關的部署作業" msgid "Related Issues" -msgstr "相關的議題" +msgstr "相關的議題 (issue) " msgid "Related Jobs" msgstr "相關的作業" msgid "Related Merge Requests" -msgstr "相關的合併請求" +msgstr "相關的合併請求 (merge request) " msgid "Related Merged Requests" msgstr "相關已合併的請求" +msgid "Remind later" +msgstr "稍後提醒" + +msgid "Remove project" +msgstr "刪除專案" + +msgid "Request Access" +msgstr "申請權限" + +msgid "Revert this commit" +msgstr "還原此更動記錄 (commit)" + +msgid "Revert this merge request" +msgstr "還原此合併請求 (merge request) " + msgid "Save pipeline schedule" -msgstr "" +msgstr "保存流水線 (pipeline) 排程" msgid "Schedule a new pipeline" -msgstr "" +msgstr "建立流水線 (pipeline) 排程" + +msgid "Scheduling Pipelines" +msgstr "流水線 (pipeline) 計劃" + +msgid "Search branches and tags" +msgstr "搜尋分支 (branch) 和標籤" + +msgid "Select Archive Format" +msgstr "選擇下載格式" msgid "Select a timezone" -msgstr "" +msgstr "選擇時區" msgid "Select target branch" -msgstr "" +msgstr "選擇目標分支 (branch) " + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "請先設定密碼,才能使用 %{protocol} 來上傳 (push) 或下載 (pull) 。" + +msgid "Set up CI" +msgstr "設定 CI" + +msgid "Set up Koding" +msgstr "設定 Koding" + +msgid "Set up auto deploy" +msgstr "設定自動部署" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "設定密碼" msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "顯示 %d 個事件" +msgid "Source code" +msgstr "原始碼" + +msgid "StarProject|Star" +msgstr "收藏" + +msgid "Start a %{new_merge_request} with these changes" +msgstr "以這些改動建立一個新的 %{new_merge_request} " + +msgid "Switch branch/tag" +msgstr "切換分支 (branch) 或標籤" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "標籤" + +msgid "Tags" +msgstr "標籤" + msgid "Target Branch" -msgstr "" +msgstr "目標分支 (branch) " -msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." -msgstr "程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。" +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." +msgstr "" +"程式開發階段顯示從第一次更動記錄 (commit) 到建立合併請求 (merge request) 的時間。建立第一個合併請求後,資料將自動填入。" msgid "The collection of events added to the data gathered for that stage." -msgstr "與該階段相關的事件。" +msgstr "該階段中的相關事件集合。" + +msgid "The fork relationship has been removed." +msgstr "分支與主幹間的關聯 (fork relationship) 已被刪除。" -msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." -msgstr "議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。" +msgid "" +"The issue stage shows the time it takes from creating an issue to assigning " +"the issue to a milestone, or add the issue to a list on your Issue Board. " +"Begin creating issues to see data for this stage." +msgstr "" +"議題 (issue) 階段顯示從議題建立到設定里程碑所花的時間,或是議題被分類到議題看板 (issue board) " +"中所花的時間。建立第一個議題後,資料將自動填入。" msgid "The phase of the development lifecycle." -msgstr "專案開發生命週期的各個階段。" +msgstr "專案開發週期的各個階段。" -msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." -msgstr "計劃階段所顯示的是議題被排程後至第一個送交被推送的時間。一旦完成(或執行)首次的推送,資料將自動填入。" +msgid "" +"The pipelines schedule runs pipelines in the future, repeatedly, for " +"specific branches or tags. Those scheduled pipelines will inherit limited " +"project access based on their associated user." +msgstr "" +"在指定了特定分支 (branch) 或標籤後,此處的流水線 (pipeline) 排程會不斷地重複執行。\n" +"流水線排程的存取權限與專案本身相同。" -msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." -msgstr "上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。" +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "計劃階段顯示從更動記錄 (commit) 被排程至第一個推送的時間。第一次推送之後,資料將自動填入。" + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." +msgstr "營運階段顯示從建立一個議題 (issue) 到部署程式至營運的總時間。當完成從想法到產品實現的循環後,資料將自動填入。" -msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." -msgstr "複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。" +msgid "The project can be accessed by any logged in user." +msgstr "該專案允許已登入的用戶存取。" + +msgid "The project can be accessed without any authentication." +msgstr "該專案允許任何人存取。" + +msgid "The repository for this project does not exist." +msgstr "此專案的檔案庫 (repository)不存在。" + +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." +msgstr "" +"複閱階段顯示從合併請求 (merge request) 建立後至被合併的時間。當建立第一個合併請求 (merge request) 後,資料將自動填入。" -msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." -msgstr "預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。" +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." +msgstr "試營運段顯示從合併請求 (merge request) 被合併後至部署營運的時間。當第一次部署營運後,資料將自動填入" -msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." -msgstr "測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。" +msgid "" +"The testing stage shows the time GitLab CI takes to run every pipeline for " +"the related merge request. The data will automatically be added after your " +"first pipeline finishes running." +msgstr "" +"測試階段顯示相關合併請求 (merge request) 的流水線 (pipeline) 所花的時間。當第一個流水線 (pipeline) " +"執行完畢後,資料將自動填入。" msgid "The time taken by each data entry gathered by that stage." -msgstr "每筆該階段相關資料所花的時間。" +msgstr "該階段中每一個資料項目所花的時間。" -msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。" +msgid "" +"This means you can not push code until you create an empty repository or " +"import existing one." +msgstr "在建立一個空的檔案庫 (repository) 或匯入現有檔案庫之後,才可以推送档案。" + msgid "Time before an issue gets scheduled" -msgstr "議題等待排程的時間" +msgstr "議題 (issue) 被列入日程表的時間" msgid "Time before an issue starts implementation" -msgstr "議題等待開始實作的時間" +msgstr "議題 (issue) 等待開始實作的時間" msgid "Time between merge request creation and merge/close" -msgstr "合併請求被合併或是關閉的時間" +msgstr "合併請求 (merge request) 從建立到被合併或是關閉的時間" msgid "Time until first merge request" -msgstr "第一個合併請求被建立前的時間" +msgstr "第一個合併請求 (merge request) 被建立前的時間" + +msgid "Timeago|%s days ago" +msgstr " %s 天前" + +msgid "Timeago|%s days remaining" +msgstr "剩下 %s 天" + +msgid "Timeago|%s hours remaining" +msgstr "剩下 %s 小時" + +msgid "Timeago|%s minutes ago" +msgstr " %s 分鐘前" + +msgid "Timeago|%s minutes remaining" +msgstr "剩下 %s 分鐘" + +msgid "Timeago|%s months ago" +msgstr " %s 個月前" + +msgid "Timeago|%s months remaining" +msgstr "剩下 %s 月" + +msgid "Timeago|%s seconds remaining" +msgstr "剩下 %s 秒" + +msgid "Timeago|%s weeks ago" +msgstr " %s 星期前" + +msgid "Timeago|%s weeks remaining" +msgstr "剩下 %s 星期" + +msgid "Timeago|%s years ago" +msgstr " %s 年前" + +msgid "Timeago|%s years remaining" +msgstr "剩下 %s 年" + +msgid "Timeago|1 day remaining" +msgstr "剩下 1 天" + +msgid "Timeago|1 hour remaining" +msgstr "剩下 1 小時" + +msgid "Timeago|1 minute remaining" +msgstr "剩下 1 分鐘" + +msgid "Timeago|1 month remaining" +msgstr "剩下 1 個月" + +msgid "Timeago|1 week remaining" +msgstr "剩下 1 星期" + +msgid "Timeago|1 year remaining" +msgstr "剩下 1 年" + +msgid "Timeago|Past due" +msgstr "逾期" + +msgid "Timeago|a day ago" +msgstr " 1 天前" + +msgid "Timeago|a month ago" +msgstr " 1 個月前" + +msgid "Timeago|a week ago" +msgstr " 1 星期前" + +msgid "Timeago|a while" +msgstr "剛剛" + +msgid "Timeago|a year ago" +msgstr " 1 年前" + +msgid "Timeago|about %s hours ago" +msgstr "約 %s 小時前" + +msgid "Timeago|about a minute ago" +msgstr "約 1 分鐘前" + +msgid "Timeago|about an hour ago" +msgstr "約 1 小時前" + +msgid "Timeago|in %s days" +msgstr " %s 天後" + +msgid "Timeago|in %s hours" +msgstr " %s 小時後" + +msgid "Timeago|in %s minutes" +msgstr " %s 分鐘後" + +msgid "Timeago|in %s months" +msgstr " %s 個月後" + +msgid "Timeago|in %s seconds" +msgstr " %s 秒後" + +msgid "Timeago|in %s weeks" +msgstr " %s 星期後" + +msgid "Timeago|in %s years" +msgstr " %s 年後" + +msgid "Timeago|in 1 day" +msgstr " 1 天後" + +msgid "Timeago|in 1 hour" +msgstr " 1 小時後" + +msgid "Timeago|in 1 minute" +msgstr " 1 分鐘後" + +msgid "Timeago|in 1 month" +msgstr " 1 月後" + +msgid "Timeago|in 1 week" +msgstr " 1 星期後" + +msgid "Timeago|in 1 year" +msgstr " 1 年後" + +msgid "Timeago|less than a minute ago" +msgstr "不到 1 分鐘前" msgid "Time|hr" msgid_plural "Time|hrs" @@ -275,7 +974,28 @@ msgid "Total Time" msgstr "總時間" msgid "Total test time for all commits/merges" -msgstr "所有送交和合併的總測試時間" +msgstr "合併 (merge) 與更動記錄 (commit) 的總測試時間" + +msgid "Unstar" +msgstr "取消收藏" + +msgid "Upload New File" +msgstr "上傳新檔案" + +msgid "Upload file" +msgstr "上傳檔案" + +msgid "Use your global notification setting" +msgstr "使用全域通知設定" + +msgid "VisibilityLevel|Internal" +msgstr "內部" + +msgid "VisibilityLevel|Private" +msgstr "私人" + +msgid "VisibilityLevel|Public" +msgstr "公開" msgid "Want to see the data? Please ask an administrator for access." msgstr "權限不足。如需查看相關資料,請向管理員申請權限。" @@ -283,12 +1003,85 @@ msgstr "權限不足。如需查看相關資料,請向管理員申請權限。 msgid "We don't have enough data to show this stage." msgstr "因該階段的資料不足而無法顯示相關資訊" -msgid "You have reached your project limit" +msgid "Withdraw Access Request" +msgstr "取消權限申请" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" msgstr "" +"即將要刪除 %{project_name_with_namespace}。\n" +"被刪除的專案完全無法救回來喔!\n" +"真的「100%確定」要這麼做嗎?" + +msgid "" +"You are going to remove the fork relationship to source project " +"%{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "" +"將要刪除本分支專案與主幹的所有關聯 (fork relationship) 。 %{forked_from_project} " +"真的「100%確定」要這麼做嗎?" + +msgid "" +"You are going to transfer %{project_name_with_namespace} to another owner. " +"Are you ABSOLUTELY sure?" +msgstr "將要把 %{project_name_with_namespace} 的所有權轉移給另一個人。真的「100%確定」要這麼做嗎?" + +msgid "You can only add files when you are on a branch" +msgstr "只能在分支 (branch) 上建立檔案" + +msgid "You have reached your project limit" +msgstr "您已達到專案數量限制" + +msgid "You must sign in to star a project" +msgstr "必須登錄才能收藏專案" msgid "You need permission." -msgstr "您需要相關的權限。" +msgstr "需要權限才能這麼做。" + +msgid "You will not get any notifications via email" +msgstr "不會收到任何通知郵件" + +msgid "You will only receive notifications for the events you choose" +msgstr "只接收您選擇的事件通知" + +msgid "" +"You will only receive notifications for threads you have participated in" +msgstr "只接收參與主題的通知" + +msgid "You will receive notifications for any activity" +msgstr "接收所有活動的通知" + +msgid "" +"You will receive notifications only for comments in which you were " +"@mentioned" +msgstr "只接收評論中提及(@)您的通知" + +msgid "" +"You won't be able to pull or push project code via %{protocol} until you " +"%{set_password_link} on your account" +msgstr "" +"在帳號上 %{set_password_link} 之前,將無法使用 %{protocol} 來上傳 (push) 或下載 (pull) 專案更新。" + +msgid "" +"You won't be able to pull or push project code via SSH until you " +"%{add_ssh_key_link} to your profile" +msgstr "在個人帳號中 %{add_ssh_key_link} 之前,將無法使用 SSH 來上傳 (push) 或下載 (pull) 專案更新。" + +msgid "Your name" +msgstr "您的名字" msgid "day" msgid_plural "days" msgstr[0] "天" + +msgid "new merge request" +msgstr "建立合併請求" + +msgid "notification emails" +msgstr "通知信" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "父級" + -- cgit v1.2.1 From 468e8b55585d54b1f92f647e8a1932f22610889e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 28 Jun 2017 18:17:53 +0800 Subject: Fix doc, test, and form --- .../projects/pipelines_settings/_show.html.haml | 8 ++ .../projects/pipelines_settings/show.html.haml | 87 ---------------------- doc/user/project/pipelines/settings.md | 7 +- .../gitlab/import_export/safe_model_attributes.yml | 1 + 4 files changed, 13 insertions(+), 90 deletions(-) delete mode 100644 app/views/projects/pipelines_settings/show.html.haml diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 580129ca809..2c2f0341e2a 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -45,6 +45,14 @@ Per job in minutes. If a job passes this threshold, it will be marked as failed = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank' + %hr + .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. + %hr .form-group .checkbox diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml deleted file mode 100644 index 25a991cdbfc..00000000000 --- a/app/views/projects/pipelines_settings/show.html.haml +++ /dev/null @@ -1,87 +0,0 @@ -- page_title "CI/CD Pipelines" - -.row.prepend-top-default - .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 - = page_title - .col-lg-9 - %h5.prepend-top-0 - Pipelines - = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project), remote: true, authenticity_token: true do |f| - %fieldset.builds-feature - - unless @repository.gitlab_ci_yml - .form-group - %p Pipelines need to be configured before you can begin using Continuous Integration. - = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' - .form-group - %p Get recent application code using the following command: - .radio - = f.label :build_allow_git_fetch_false do - = f.radio_button :build_allow_git_fetch, 'false' - %strong git clone - %br - %span.descr Slower but makes sure you have a clean dir before every build - .radio - = f.label :build_allow_git_fetch_true do - = f.radio_button :build_allow_git_fetch, 'true' - %strong git fetch - %br - %span.descr Faster - - .form-group - = 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 - %span.input-group-addon / - = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' - %span.input-group-addon / - %p.help-block - We will use this regular expression to find test coverage output in build trace. - Leave blank if you want to disable this feature - .bs-callout.bs-callout-info - %p Below are examples of regex for existing tools: - %ul - %li - Simplecov (Ruby) - - %code \(\d+.\d+\%\) covered - %li - pytest-cov (Python) - - %code \d+\%\s*$ - %li - phpunit --coverage-text --colors=never (PHP) - - %code ^\s*Lines:\s*\d+.\d+\% - %li - gcovr (C/C++) - - %code ^TOTAL.*\s+(\d+\%)$ - %li - tap --coverage-report=text-summary (Node.js) - - %code ^Statements\s*:\s*([^%]+) - - .form-group - .checkbox - = f.label :public_builds do - = f.check_box :public_builds - %strong Public pipelines - .help-block Allow everyone to access pipelines for Public and Internal projects - - .form-group.append-bottom-default - = f.label :runners_token, "Runners token", class: 'label-light' - = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' - %p.help-block The secure token used to checkout project. - - = f.submit 'Save changes', class: "btn btn-save" - -%hr - -.row.prepend-top-default - = render partial: 'badge', collection: @badges diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 435aacd8bb6..702b3453a0e 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -29,10 +29,10 @@ if the job surpasses the threshold, it is marked as failed. ## Custom CI Config File -> - [Introduced][ce-15041] in GitLab 8.13. +> - [Introduced][ce-12509] in GitLab 9.4. -By default we look for the `.gitlab-ci.yml` file in the projects root -directory. If you require a different location **within** the repository +By default we look for the `.gitlab-ci.yml` file in the project's 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. @@ -131,3 +131,4 @@ into your `README.md`: [var]: ../../../ci/yaml/README.md#git-strategy [coverage report]: #test-coverage-parsing [ce-9362]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362 +[ce-12509]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12509 diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index fadd3ad1330..f782cf533e8 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -383,6 +383,7 @@ Project: - printing_merge_request_link_enabled - build_allow_git_fetch - last_repository_updated_at +- ci_config_file Author: - name ProjectFeature: -- cgit v1.2.1 From 02ff4381979a5148cc17f7b6ea023fd4a1a5bffe Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 28 Jun 2017 18:18:06 +0800 Subject: Try to report where the file should be --- app/services/ci/create_pipeline_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 942145c4a8c..4f35255fb53 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -33,7 +33,7 @@ module Ci unless pipeline.config_processor unless pipeline.ci_yaml_file - return error('Missing .gitlab-ci.yml file') + return error("Missing #{pipeline.ci_yaml_file_path} file") end return error(pipeline.yaml_errors, save: save_on_errors) end -- cgit v1.2.1 From 0d5e6536e7c18d839b1c1da0807aa90ba5be3e06 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 28 Jun 2017 18:29:14 +0800 Subject: Fix the test and implement missing update --- app/models/ci/pipeline.rb | 2 ++ spec/models/ci/pipeline_spec.rb | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 23b641c334d..3a514718ca8 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -327,6 +327,8 @@ module Ci @ci_yaml_file = begin project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path) rescue + self.yaml_errors = + "Failed to load CI/CD config file at #{ci_yaml_file_path}" nil end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 8fb6759d3ab..fef40874d95 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -744,31 +744,45 @@ describe Ci::Pipeline, models: true do 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 + allow(project).to receive(:ci_config_file) { 'custom' } + pipeline.ci_yaml_file - expect(pipeline.yaml_errors).to eq('Failed to load CI config file') + + expect(pipeline.yaml_errors) + .to eq('Failed to load CI/CD config file at custom/.gitlab-ci.yml') end end -- cgit v1.2.1 From 94b673747e94e32dea576b0eaa915f6fb731628d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 28 Jun 2017 12:32:51 +0200 Subject: Add MR iid in CHANGELOG entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../34078-allow-to-enable-feature-flags-with-more-granularity.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml b/changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml index 8ead1b404f3..69d5d34b072 100644 --- a/changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml +++ b/changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml @@ -1,4 +1,4 @@ --- title: Allow the feature flags to be enabled/disabled with more granularity -merge_request: +merge_request: 12357 author: -- cgit v1.2.1 From 291309f431b6997b079c0b0ac738f210d316a77b Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 27 Jun 2017 20:16:49 -0500 Subject: Fix scroll flicker See https://gitlab.com/gitlab-org/gitlab-ce/issues/34407 - Revert https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12399 - Update https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12299 throttle/debounce to happen immediately and cleanup --- app/assets/javascripts/right_sidebar.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 322162afdb8..b5cd01044a3 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -10,6 +10,8 @@ import Cookies from 'js-cookie'; this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); this.$navGitlab = $('.navbar-gitlab'); + this.$layoutNav = $('.layout-nav'); + this.$subScroll = $('.sub-nav-scroll'); this.$rightSidebar = $('.js-right-sidebar'); this.removeListeners(); @@ -27,14 +29,14 @@ import Cookies from 'js-cookie'; Sidebar.prototype.addEventListeners = function() { const $document = $(document); const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); - const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200); + const slowerThrottledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 200); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $(window).on('resize', () => throttledSetSidebarHeight()); - $document.on('scroll', () => debouncedSetSidebarHeight()); + $document.on('scroll', () => slowerThrottledSetSidebarHeight()); $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); @@ -213,7 +215,7 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = this.$navGitlab.outerHeight(); + const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0); const diff = $navHeight - $(window).scrollTop(); if (diff > 0) { this.$rightSidebar.outerHeight($(window).height() - diff); -- cgit v1.2.1 From 289fae78e971e117e69fb87602f5f6284419b863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 28 Jun 2017 19:29:56 +0200 Subject: Rename flipper_group to feature_group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- doc/api/features.md | 4 ++-- lib/api/features.rb | 8 ++++---- spec/requests/api/features_spec.rb | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/api/features.md b/doc/api/features.md index a3bf5d018a7..558869255cc 100644 --- a/doc/api/features.md +++ b/doc/api/features.md @@ -58,10 +58,10 @@ POST /features/:name | --------- | ---- | -------- | ----------- | | `name` | string | yes | Name of the feature to create or update | | `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time | -| `flipper_group` | string | no | A Flipper group name | +| `feature_group` | string | no | A Feature group name | | `user` | string | no | A GitLab username | -Note that `flipper_group` and `user` are mutually exclusive. +Note that `feature_group` and `user` are mutually exclusive. ```bash curl --data "value=30" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/new_library diff --git a/lib/api/features.rb b/lib/api/features.rb index e426bc050eb..21745916463 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -15,8 +15,8 @@ module API end def gate_target(params) - if params[:flipper_group] - Feature.group(params[:flipper_group]) + if params[:feature_group] + Feature.group(params[:feature_group]) elsif params[:user] User.find_by_username(params[:user]) else @@ -40,9 +40,9 @@ module API end params do requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' - optional :flipper_group, type: String, desc: 'A Flipper group name' + optional :feature_group, type: String, desc: 'A Feature group name' optional :user, type: String, desc: 'A GitLab username' - mutually_exclusive :flipper_group, :user + mutually_exclusive :feature_group, :user end post ':name' do feature = Feature.get(params[:name]) diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 0ee0749c7a1..1d8aaeea8f2 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -88,8 +88,8 @@ describe API::Features do 'gates' => [{ 'key' => 'boolean', 'value' => true }]) end - it 'creates an enabled feature for the given Flipper group when passed flipper_group=perf_team' do - post api("/features/#{feature_name}", admin), value: 'true', flipper_group: 'perf_team' + it 'creates an enabled feature for the given Flipper group when passed feature_group=perf_team' do + post api("/features/#{feature_name}", admin), value: 'true', feature_group: 'perf_team' expect(response).to have_http_status(201) expect(json_response).to eq( @@ -147,8 +147,8 @@ describe API::Features do 'gates' => [{ 'key' => 'boolean', 'value' => true }]) end - it 'enables the feature for the given Flipper group when passed flipper_group=perf_team' do - post api("/features/#{feature_name}", admin), value: 'true', flipper_group: 'perf_team' + it 'enables the feature for the given Flipper group when passed feature_group=perf_team' do + post api("/features/#{feature_name}", admin), value: 'true', feature_group: 'perf_team' expect(response).to have_http_status(201) expect(json_response).to eq( @@ -188,11 +188,11 @@ describe API::Features do 'gates' => [{ 'key' => 'boolean', 'value' => false }]) end - it 'disables the feature for the given Flipper group when passed flipper_group=perf_team' do + it 'disables the feature for the given Flipper group when passed feature_group=perf_team' do feature.enable(Feature.group(:perf_team)) expect(Feature.get(feature_name).enabled?(admin)).to be_truthy - post api("/features/#{feature_name}", admin), value: 'false', flipper_group: 'perf_team' + post api("/features/#{feature_name}", admin), value: 'false', feature_group: 'perf_team' expect(response).to have_http_status(201) expect(json_response).to eq( -- cgit v1.2.1 From b37923235bd67542d8f7a02a08c710b6ef3b339d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 11 Apr 2017 16:48:22 -0500 Subject: ensure eslint recognizes es2015 dynamic import() syntax --- .eslintrc | 1 + package.json | 1 + yarn.lock | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 73cd7ecf66d..c72a5e0335b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,6 +11,7 @@ "gon": false, "localStorage": false }, + "parser": "babel-eslint", "plugins": [ "filenames", "import", diff --git a/package.json b/package.json index 045f07ee2f9..5a997e813f8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "babel-core": "^6.22.1", + "babel-eslint": "^7.2.1", "babel-loader": "^6.2.10", "babel-plugin-transform-define": "^1.2.0", "babel-preset-latest": "^6.24.0", diff --git a/yarn.lock b/yarn.lock index b902d5235d0..b04eebe60af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -265,6 +265,15 @@ babel-core@^6.22.1, babel-core@^6.23.0: slash "^1.0.0" source-map "^0.5.0" +babel-eslint@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f" + dependencies: + babel-code-frame "^6.22.0" + babel-traverse "^6.23.1" + babel-types "^6.23.0" + babylon "^6.16.1" + babel-generator@^6.18.0, babel-generator@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5" @@ -816,10 +825,14 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23 lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0: +babylon@^6.11.0: version "6.15.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" +babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1: + version "6.16.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" + backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" -- cgit v1.2.1 From 3f3993c3d3da135ef32ba0abbff66d910a189bb6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 14 Jun 2017 15:08:02 -0500 Subject: dynamically set webpack publicPath when relative_url_root enabled --- app/assets/javascripts/main.js | 8 ++++++++ config/webpack.config.js | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index d27b4ec78c6..c8761e8fe63 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -164,6 +164,14 @@ import './visibility_select'; import './wikis'; import './zen_mode'; +// set url root for webpack async chunks (assumes config.output.publicPath is an absolute path) +if (gon && gon.relative_url_root) { + const basePath = gon.relative_url_root.replace(/\/$/, ''); + + // eslint-disable-next-line camelcase, no-undef + __webpack_public_path__ = basePath + __webpack_public_path__; +} + // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); diff --git a/config/webpack.config.js b/config/webpack.config.js index 2e8c94655c1..39e665d5c14 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -244,7 +244,6 @@ if (IS_DEV_SERVER) { hot: DEV_SERVER_LIVERELOAD, inline: DEV_SERVER_LIVERELOAD }; - config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath; config.plugins.push( // watch node_modules for changes if we encounter a missing module compile error new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')) -- cgit v1.2.1 From f26d4778656c32f550391d1986c4af1ed150364d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 26 Jun 2017 22:43:32 -0500 Subject: dynamically import emoji helpers for AwardsHandler class --- app/assets/javascripts/awards_handler.js | 79 ++++++++++++++++++-------------- app/assets/javascripts/main.js | 4 +- app/assets/javascripts/notes.js | 10 +++- spec/javascripts/awards_handler_spec.js | 15 +++--- 4 files changed, 61 insertions(+), 47 deletions(-) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index c34d80f0601..18cd04b176a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,7 +2,6 @@ /* global Flash */ import Cookies from 'js-cookie'; -import * as Emoji from './emoji'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -24,27 +23,9 @@ const categoryLabelMap = { flags: 'Flags', }; -function renderCategory(name, emojiList, opts = {}) { - return ` -
- ${name} -
-
    - ${emojiList.map(emojiName => ` -
  • - -
  • - `).join('\n')} -
- `; -} - -export default class AwardsHandler { - constructor() { +class AwardsHandler { + constructor(emoji) { + this.emoji = emoji; this.eventListeners = []; // If the user shows intent let's pre-build the menu this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { @@ -78,10 +59,10 @@ export default class AwardsHandler { const $target = $(e.currentTarget); const $glEmojiElement = $target.find('gl-emoji'); const $spriteIconElement = $target.find('.icon'); - const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); + const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); $target.closest('.js-awards-block').addClass('current'); - this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); + this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName); }); } @@ -139,16 +120,16 @@ export default class AwardsHandler { this.isCreatingEmojiMenu = true; // Render the first category - const categoryMap = Emoji.getEmojiCategoryMap(); + const categoryMap = this.emoji.getEmojiCategoryMap(); const categoryNameKey = Object.keys(categoryMap)[0]; const emojisInCategory = categoryMap[categoryNameKey]; - const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); + const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); // Render the frequently used const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); let frequentlyUsedCatgegory = ''; if (frequentlyUsedEmojis.length > 0) { - frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { + frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, { menuListClass: 'frequent-emojis', }); } @@ -179,7 +160,7 @@ export default class AwardsHandler { } this.isAddingRemainingEmojiMenuCategories = true; - const categoryMap = Emoji.getEmojiCategoryMap(); + const categoryMap = this.emoji.getEmojiCategoryMap(); // Avoid the jank and render the remaining categories separately // This will take more time, but makes UI more responsive @@ -191,7 +172,7 @@ export default class AwardsHandler { promiseChain.then(() => new Promise((resolve) => { const emojisInCategory = categoryMap[categoryNameKey]; - const categoryMarkup = renderCategory( + const categoryMarkup = this.renderCategory( categoryLabelMap[categoryNameKey], emojisInCategory, ); @@ -216,6 +197,25 @@ export default class AwardsHandler { }); } + renderCategory(name, emojiList, opts = {}) { + return ` +
+ ${name} +
+
    + ${emojiList.map(emojiName => ` +
  • + +
  • + `).join('\n')} +
+ `; + } + positionMenu($menu, $addBtn) { const position = $addBtn.data('position'); // The menu could potentially be off-screen or in a hidden overflow element @@ -234,7 +234,7 @@ export default class AwardsHandler { } addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { - const normalizedEmoji = Emoji.normalizeEmojiName(emoji); + const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); @@ -249,7 +249,7 @@ export default class AwardsHandler { this.checkMutuality(votesBlock, emoji); } this.addEmojiToFrequentlyUsedList(emoji); - const normalizedEmoji = Emoji.normalizeEmojiName(emoji); + const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); if ($emojiButton.length > 0) { if (this.isActive($emojiButton)) { @@ -374,7 +374,7 @@ export default class AwardsHandler { createAwardButtonForVotesBlock(votesBlock, emojiName) { const buttonHtml = ` `; @@ -440,7 +440,7 @@ export default class AwardsHandler { } addEmojiToFrequentlyUsedList(emoji) { - if (Emoji.isEmojiNameValid(emoji)) { + if (this.emoji.isEmojiNameValid(emoji)) { this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); } @@ -450,7 +450,7 @@ export default class AwardsHandler { return this.frequentlyUsedEmojis || (() => { const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( - inputName => Emoji.isEmojiNameValid(inputName), + inputName => this.emoji.isEmojiNameValid(inputName), ); return this.frequentlyUsedEmojis; @@ -493,7 +493,7 @@ export default class AwardsHandler { } findMatchingEmojiElements(query) { - const emojiMatches = Emoji.filterEmojiNamesByAlias(query); + const emojiMatches = this.emoji.filterEmojiNamesByAlias(query); const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); const $matchingElements = $emojiElements .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0); @@ -507,3 +507,12 @@ export default class AwardsHandler { $('.emoji-menu').remove(); } } + +let awardsHandlerPromise = null; +export default function loadAwardsHandler(reload = false) { + if (!awardsHandlerPromise || reload) { + awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji') + .then(Emoji => new AwardsHandler(Emoji)); + } + return awardsHandlerPromise; +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index c8761e8fe63..e11d11f87af 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -70,7 +70,7 @@ import './ajax_loading_spinner'; import './api'; import './aside'; import './autosave'; -import AwardsHandler from './awards_handler'; +import loadAwardsHandler from './awards_handler'; import './breakpoints'; import './broadcast_message'; import './build'; @@ -363,7 +363,7 @@ $(function () { $window.off('resize.app').on('resize.app', function () { return fitSidebarForSize(); }); - gl.awardsHandler = new AwardsHandler(); + loadAwardsHandler(); new Aside(); gl.utils.initTimeagoTimeout(); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 34476f3303f..f636b7602cb 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; import CommentTypeToggle from './comment_type_toggle'; +import loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; import './task_list'; @@ -291,8 +292,13 @@ export default class Notes { if ('emoji_award' in noteEntity.commands_changes) { votesBlock = $('.js-awards-block').eq(0); - gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); - return gl.awardsHandler.scrollToAwards(); + + loadAwardsHandler().then((awardsHandler) => { + awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); + awardsHandler.scrollToAwards(); + }).catch(() => { + // ignore + }); } } } diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 3fc03324d16..8e056882108 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,7 +1,7 @@ /* 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 Cookies from 'js-cookie'; -import AwardsHandler from '~/awards_handler'; +import loadAwardsHandler from '~/awards_handler'; import '~/lib/utils/common_utils'; @@ -26,14 +26,13 @@ import '~/lib/utils/common_utils'; describe('AwardsHandler', function() { preloadFixtures('issues/issue_with_comment.html.raw'); - beforeEach(function() { + beforeEach(function(done) { loadFixtures('issues/issue_with_comment.html.raw'); - awardsHandler = new AwardsHandler; - spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { - return function(button, url, emoji, cb) { - return cb(); - }; - })(this)); + loadAwardsHandler(true).then((obj) => { + awardsHandler = obj; + spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); + done(); + }).catch(fail); let isEmojiMenuBuilt = false; openAndWaitForEmojiMenu = function() { -- cgit v1.2.1 From 2874bab422f5b8342884c019bc39e0922beca469 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 27 Jun 2017 01:26:38 -0500 Subject: dynamically import emoji helpers for GfmAutoComplete class --- app/assets/javascripts/gfm_auto_complete.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index f99bac7da1a..1ff175f981c 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,4 +1,3 @@ -import { validEmojiNames, glEmojiTag } from './emoji'; import glRegexp from './lib/utils/regexp'; import AjaxCache from './lib/utils/ajax_cache'; @@ -373,7 +372,12 @@ class GfmAutoComplete { if (this.cachedData[at]) { this.loadData($input, at, this.cachedData[at]); } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { - this.loadData($input, at, validEmojiNames); + import(/* webpackChunkName: 'emoji' */ './emoji') + .then(({ validEmojiNames, glEmojiTag }) => { + this.loadData($input, at, validEmojiNames); + GfmAutoComplete.glEmojiTag = glEmojiTag; + }) + .catch(() => { this.isLoadingData[at] = false; }); } else { AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) .then((data) => { @@ -421,12 +425,14 @@ GfmAutoComplete.atTypeMap = { }; // Emoji +GfmAutoComplete.glEmojiTag = null; GfmAutoComplete.Emoji = { templateFunction(name) { - return `
  • - ${name} ${glEmojiTag(name)} -
  • - `; + // glEmojiTag helper is loaded on-demand in fetchData() + if (GfmAutoComplete.glEmojiTag) { + return `
  • ${name} ${GfmAutoComplete.glEmojiTag(name)}
  • `; + } + return `
  • ${name}
  • `; }, }; // Team Members -- cgit v1.2.1 From 8bb2013279a0f9116c01374b55401f3ca0d51c24 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 27 Jun 2017 01:27:06 -0500 Subject: dynamically import emoji helpers for gl-emoji custom tag prototype --- app/assets/javascripts/behaviors/gl_emoji.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 8156e491a42..7e98e04303a 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -1,5 +1,4 @@ import installCustomElements from 'document-register-element'; -import { emojiImageTag, emojiFallbackImageSrc } from '../emoji'; import isEmojiUnicodeSupported from '../emoji/support'; installCustomElements(window); @@ -32,11 +31,19 @@ export default function installGlEmojiElement() { // 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 = emojiFallbackImageSrc(name); - this.innerHTML = emojiImageTag(name, src); + import(/* webpackChunkName: 'emoji' */ '../emoji') + .then(({ emojiImageTag, emojiFallbackImageSrc }) => { + if (hasImageFallback) { + this.innerHTML = emojiImageTag(name, fallbackSrc); + } else { + const src = emojiFallbackImageSrc(name); + this.innerHTML = emojiImageTag(name, src); + } + }) + .catch(() => { + // do nothing + }); } } }; -- cgit v1.2.1 From 9c7f3fab973ea132c2be406c03cba1771e9933ef Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Mon, 26 Jun 2017 10:53:14 -0700 Subject: 32838 Add wells to admin dashboard overview to fix spacing problems --- app/assets/stylesheets/framework/wells.scss | 14 + app/views/admin/dashboard/index.html.haml | 322 ++++++++++----------- .../unreleased/32838-admin-panel-spacing.yml | 4 + 3 files changed, 179 insertions(+), 161 deletions(-) create mode 100644 changelogs/unreleased/32838-admin-panel-spacing.yml diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index 1c1392f8f67..b1ff2659131 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -3,6 +3,7 @@ color: $gl-text-color; border: 1px solid $border-color; border-radius: $border-radius-default; + margin-bottom: $gl-padding; .well-segment { padding: $gl-padding; @@ -21,6 +22,11 @@ font-size: 12px; } } + + &.admin-well h4 { + border-bottom: 1px solid $border-color; + padding-bottom: 8px; + } } .icon-container { @@ -53,6 +59,14 @@ padding: 15px; } +.dark-well { + background-color: $gray-normal; + + .btn { + width: 100%; + } +} + .well-centered { h1 { font-weight: normal; diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 3c9f932a225..128b5dc01ab 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -5,182 +5,182 @@ .admin-dashboard.prepend-top-default .row .col-md-4 - %h4 Statistics - %hr - %p - Forks - %span.light.pull-right - = number_with_delimiter(ForkedProjectLink.count) - %p - Issues - %span.light.pull-right - = number_with_delimiter(Issue.count) - %p - Merge Requests - %span.light.pull-right - = number_with_delimiter(MergeRequest.count) - %p - Notes - %span.light.pull-right - = number_with_delimiter(Note.count) - %p - Snippets - %span.light.pull-right - = number_with_delimiter(Snippet.count) - %p - SSH Keys - %span.light.pull-right - = number_with_delimiter(Key.count) - %p - Milestones - %span.light.pull-right - = number_with_delimiter(Milestone.count) - %p - Active Users - %span.light.pull-right - = number_with_delimiter(User.active.count) + .info-well + .well-segment.admin-well + %h4 Statistics + %p + Forks + %span.light.pull-right + = number_with_delimiter(ForkedProjectLink.count) + %p + Issues + %span.light.pull-right + = number_with_delimiter(Issue.count) + %p + Merge Requests + %span.light.pull-right + = number_with_delimiter(MergeRequest.count) + %p + Notes + %span.light.pull-right + = number_with_delimiter(Note.count) + %p + Snippets + %span.light.pull-right + = number_with_delimiter(Snippet.count) + %p + SSH Keys + %span.light.pull-right + = number_with_delimiter(Key.count) + %p + Milestones + %span.light.pull-right + = number_with_delimiter(Milestone.count) + %p + Active Users + %span.light.pull-right + = number_with_delimiter(User.active.count) .col-md-4 - %h4 - Features - %hr - - sign_up = "Sign up" - %p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") } - = sign_up - %span.light.pull-right - = boolean_to_icon signup_enabled? - - ldap = "LDAP" - %p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") } - = ldap - %span.light.pull-right - = boolean_to_icon Gitlab.config.ldap.enabled - - gravatar = "Gravatar" - %p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") } - = gravatar - %span.light.pull-right - = boolean_to_icon gravatar_enabled? - - omniauth = "OmniAuth" - %p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") } - = omniauth - %span.light.pull-right - = boolean_to_icon Gitlab.config.omniauth.enabled - - reply_email = "Reply by email" - %p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") } - = reply_email - %span.light.pull-right - = boolean_to_icon Gitlab::IncomingEmail.enabled? - - container_reg = "Container Registry" - %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") } - = container_reg - %span.light.pull-right - = boolean_to_icon Gitlab.config.registry.enabled - - gitlab_pages = 'GitLab Pages' - - gitlab_pages_enabled = Gitlab.config.pages.enabled - %p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") } - = gitlab_pages - %span.light.pull-right - = boolean_to_icon gitlab_pages_enabled - - gitlab_shared_runners = 'Shared Runners' - - gitlab_shared_runners_enabled = Gitlab.config.gitlab_ci.shared_runners_enabled - %p{ "aria-label" => "#{gitlab_shared_runners}: status " + (gitlab_shared_runners_enabled ? "on" : "off") } - = gitlab_shared_runners - %span.light.pull-right - = boolean_to_icon gitlab_shared_runners_enabled - + .info-well + .well-segment.admin-well + %h4 Features + - sign_up = "Sign up" + %p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") } + = sign_up + %span.light.pull-right + = boolean_to_icon signup_enabled? + - ldap = "LDAP" + %p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") } + = ldap + %span.light.pull-right + = boolean_to_icon Gitlab.config.ldap.enabled + - gravatar = "Gravatar" + %p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") } + = gravatar + %span.light.pull-right + = boolean_to_icon gravatar_enabled? + - omniauth = "OmniAuth" + %p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") } + = omniauth + %span.light.pull-right + = boolean_to_icon Gitlab.config.omniauth.enabled + - reply_email = "Reply by email" + %p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") } + = reply_email + %span.light.pull-right + = boolean_to_icon Gitlab::IncomingEmail.enabled? + - container_reg = "Container Registry" + %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") } + = container_reg + %span.light.pull-right + = boolean_to_icon Gitlab.config.registry.enabled + - gitlab_pages = 'GitLab Pages' + - gitlab_pages_enabled = Gitlab.config.pages.enabled + %p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") } + = gitlab_pages + %span.light.pull-right + = boolean_to_icon gitlab_pages_enabled + - gitlab_shared_runners = 'Shared Runners' + - gitlab_shared_runners_enabled = Gitlab.config.gitlab_ci.shared_runners_enabled + %p{ "aria-label" => "#{gitlab_shared_runners}: status " + (gitlab_shared_runners_enabled ? "on" : "off") } + = gitlab_shared_runners + %span.light.pull-right + = boolean_to_icon gitlab_shared_runners_enabled .col-md-4 - %h4 - Components - - if current_application_settings.version_check_enabled - .pull-right - = version_status_badge - - %hr - %p - GitLab - %span.pull-right - = Gitlab::VERSION - %p - GitLab Shell - %span.pull-right - = Gitlab::Shell.new.version - %p - GitLab Workhorse - %span.pull-right - = gitlab_workhorse_version - %p - GitLab API - %span.pull-right - = API::API::version - %p - Git - %span.pull-right - = Gitlab::Git.version - %p - Ruby - %span.pull-right - #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} - - %p - Rails - %span.pull-right - #{Rails::VERSION::STRING} - - %p - = Gitlab::Database.adapter_name - %span.pull-right - = Gitlab::Database.version - %hr + .info-well + .well-segment.admin-well + %h4 + Components + - if current_application_settings.version_check_enabled + .pull-right + = version_status_badge + %p + GitLab + %span.pull-right + = Gitlab::VERSION + %p + GitLab Shell + %span.pull-right + = Gitlab::Shell.new.version + %p + GitLab Workhorse + %span.pull-right + = gitlab_workhorse_version + %p + GitLab API + %span.pull-right + = API::API::version + %p + Git + %span.pull-right + = Gitlab::Git.version + %p + Ruby + %span.pull-right + #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} + %p + Rails + %span.pull-right + #{Rails::VERSION::STRING} + %p + = Gitlab::Database.adapter_name + %span.pull-right + = Gitlab::Database.version .row .col-sm-4 - .light-well.well-centered - %h4 Projects - .data + .info-well.dark-well + .well-segment.well-centered = link_to admin_projects_path do - %h1= number_with_delimiter(Project.cached_count) + %h3.text-center + Projects: + = number_with_delimiter(Project.cached_count) %hr = link_to('New project', new_project_path, class: "btn btn-new") .col-sm-4 - .light-well.well-centered - %h4 Users - .data + .info-well.dark-well + .well-segment.well-centered = link_to admin_users_path do - %h1= number_with_delimiter(User.count) + %h3.text-center + Users: + = number_with_delimiter(User.count) %hr = link_to 'New user', new_admin_user_path, class: "btn btn-new" .col-sm-4 - .light-well.well-centered - %h4 Groups - .data + .info-well.dark-well + .well-segment.well-centered = link_to admin_groups_path do - %h1= number_with_delimiter(Group.count) + %h3.text-center + Groups + = number_with_delimiter(Group.count) %hr = link_to 'New group', new_admin_group_path, class: "btn btn-new" - - .row.prepend-top-10 + .row .col-md-4 - %h4 Latest projects - %hr - - @projects.each do |project| - %p - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' - %span.light.pull-right - #{time_ago_with_tooltip(project.created_at)} - + .info-well + .well-segment.admin-well + %h4 Latest projects + - @projects.each do |project| + %p + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' + %span.light.pull-right + #{time_ago_with_tooltip(project.created_at)} .col-md-4 - %h4 Latest users - %hr - - @users.each do |user| - %p - = link_to [:admin, user], class: 'str-truncated-60' do - = user.name - %span.light.pull-right - #{time_ago_with_tooltip(user.created_at)} - + .info-well + .well-segment.admin-well + %h4 Latest users + - @users.each do |user| + %p + = link_to [:admin, user], class: 'str-truncated-60' do + = user.name + %span.light.pull-right + #{time_ago_with_tooltip(user.created_at)} .col-md-4 - %h4 Latest groups - %hr - - @groups.each do |group| - %p - = link_to [:admin, group], class: 'str-truncated-60' do - = group.full_name - %span.light.pull-right - #{time_ago_with_tooltip(group.created_at)} + .info-well + .well-segment.admin-well + %h4 Latest groups + - @groups.each do |group| + %p + = link_to [:admin, group], class: 'str-truncated-60' do + = group.full_name + %span.light.pull-right + #{time_ago_with_tooltip(group.created_at)} diff --git a/changelogs/unreleased/32838-admin-panel-spacing.yml b/changelogs/unreleased/32838-admin-panel-spacing.yml new file mode 100644 index 00000000000..ccd703fa43f --- /dev/null +++ b/changelogs/unreleased/32838-admin-panel-spacing.yml @@ -0,0 +1,4 @@ +--- +title: Add wells to admin dashboard overview to fix spacing problems +merge_request: +author: -- cgit v1.2.1 From 5b43aa955737f002be3d197c9f7b4d3374d0ad69 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 28 Jun 2017 15:15:50 -0500 Subject: move webpack publicPath setup earlier in the bootstrap processes to avoid ES module execution order issues --- app/assets/javascripts/main.js | 8 -------- app/assets/javascripts/webpack.js | 13 +++++++++++++ app/views/layouts/_head.html.haml | 2 +- config/webpack.config.js | 3 ++- 4 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/webpack.js diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index e11d11f87af..a4bcb14b636 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -164,14 +164,6 @@ import './visibility_select'; import './wikis'; import './zen_mode'; -// set url root for webpack async chunks (assumes config.output.publicPath is an absolute path) -if (gon && gon.relative_url_root) { - const basePath = gon.relative_url_root.replace(/\/$/, ''); - - // eslint-disable-next-line camelcase, no-undef - __webpack_public_path__ = basePath + __webpack_public_path__; -} - // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js new file mode 100644 index 00000000000..37420dcafa0 --- /dev/null +++ b/app/assets/javascripts/webpack.js @@ -0,0 +1,13 @@ +/** + * This is the first script loaded by webpack's runtime. It is used to manually configure + * config.output.publicPath to account for relative_url_root settings which cannot be baked-in + * to our webpack bundles. + */ + +if (gon && gon.relative_url_root) { + // this assumes config.output.publicPath is an absolute path + const basePath = gon.relative_url_root.replace(/\/$/, ''); + + // eslint-disable-next-line camelcase, no-undef + __webpack_public_path__ = basePath + __webpack_public_path__; +} diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index eabc9a3b01c..a966349523f 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -36,7 +36,7 @@ = Gon::Base.render_data - = webpack_bundle_tag "runtime" + = webpack_bundle_tag "webpack_runtime" = webpack_bundle_tag "common" = webpack_bundle_tag "locale" = webpack_bundle_tag "main" diff --git a/config/webpack.config.js b/config/webpack.config.js index 39e665d5c14..a2e7a93d501 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -71,6 +71,7 @@ var config = { vue_merge_request_widget: './vue_merge_request_widget/index.js', test: './test.js', peek: './peek.js', + webpack_runtime: './webpack.js', }, output: { @@ -189,7 +190,7 @@ var config = { // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ - names: ['main', 'locale', 'common', 'runtime'], + names: ['main', 'locale', 'common', 'webpack_runtime'], }), ], -- cgit v1.2.1 From 78c52a3a9bbb5e273e77feaf5ecf84140a938f8d Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 28 Jun 2017 18:41:06 -0300 Subject: improve site search for "issues" --- doc/user/project/issues/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index fe87e6f9495..e55e2aea023 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -1,4 +1,4 @@ -# Issues documentation +# Issues The GitLab Issue Tracker is an advanced and complete tool for tracking the evolution of a new idea or the process -- cgit v1.2.1 From 2c298473a682c567bbeb5dd7debfd767c5a6edcd Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 28 Jun 2017 17:52:58 -0400 Subject: Perform unzip quietly in UpdatePagesService Closes https://gitlab.com/gitlab-org/gitlab-ee/issues/225 --- app/services/projects/update_pages_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 17cf71cf098..e60b854f916 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -93,10 +93,11 @@ module Projects end # Requires UnZip at least 6.00 Info-ZIP. + # -qq be (very) quiet # -n never overwrite existing files # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories site_path = File.join(SITE_PATH, '*') - unless system(*%W(unzip -n #{artifacts} #{site_path} -d #{temp_path})) + unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path})) raise 'pages failed to extract' end end -- cgit v1.2.1 From 93646acc1c63baa03c2406d09e3a11036e520d6f Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 28 Jun 2017 19:48:26 -0300 Subject: capitalize feature name, add overview and use cases w/ multiple boards (EE) --- doc/user/project/issue_board.md | 42 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index ebea7062ecb..89819189864 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -1,4 +1,4 @@ -# Issue board +# Issue Board >**Notes:** - [Introduced][ce-5554] in GitLab 8.11. @@ -22,6 +22,42 @@ With the Issue Board you can have a different view of your issues while also maintaining the same filtering and sorting abilities you see across the issue tracker. +With [Multiple Issue Boards](#multiple-issue-boards), available only in [GitLab +Enterprise Edition](https://about.gitlab.com/gitlab-ee/), your workflow gets +empowered with the ability to create multiple boards per project. + +## Use-cases + +GitLab Workflow allows you to discuss proposals in issues, categorize them +with labels, and and from there organize and prioritize them in Issue Boards. + +- For example, let's consider this simplified development workflow: +you have a repository hosting your app's codebase +and your team actively contributing to code. Your backend team starts working a new +implementation, gathers feedback and approval, and pass it to frontend. +From there, when frontend is complete, the new feature +is deployed to staging to be tested. When successful, it goes to production. If we have +the labels "backend", "frontend", "staging", and "production", and an Issue Board with +a list for each, we can: + - Visualize the entire flow of implementations since the +beginning of the dev lifecycle until deployed to production + - Prioritize the issues in a list by moving them vertically + - Move issues between lists to organize them according to the labels you've set + +To enhance the workflow exemplified above, with [Multiple Issue Boards](#multiple-issue-boards), +available only in [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/), +each team (frontend and backend) can have their own boards to organize their flow among the +members of their teams. For that, we could have, therefore, three Issue Boards for this case: + + - **Backend**, for the backend team and their own labels and workflow + - **Frontend**, same as above, for the frontend team + - **Deployment**, for the entire process (backend > frontend > staging > production) + +For a broader use-case, please check the blog post +[GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario). + +## Issue Board terminology + Below is a table of the definitions used for GitLab's Issue Board. | What we call it | What it means | @@ -57,7 +93,7 @@ In short, here's a list of actions you can take in an Issue Board: If you are not able to perform one or more of the things above, make sure you have the right [permissions](#permissions). -## First time using the issue board +## First time using the Issue Board The first time you navigate to your Issue Board, you will be presented with a default list (**Done**) and a welcoming message that gives @@ -98,7 +134,7 @@ list view that is removed. You can always add it back later if you need. ## Adding issues to a list You can add issues to a list by clicking the **Add issues** button that is -present in the upper right corner of the issue board. This will open up a modal +present in the upper right corner of the Issue Board. This will open up a modal window where you can see all the issues that do not belong to any list. Select one or more issues by clicking on the cards and then click **Add issues** -- cgit v1.2.1 From 07b9f48fb3b4eaf7c3c5ca83109fa758a7c23b13 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 28 Jun 2017 20:51:59 -0300 Subject: remove multiple IB part --- doc/user/project/issue_board.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 89819189864..ebd7b72d404 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -22,10 +22,6 @@ With the Issue Board you can have a different view of your issues while also maintaining the same filtering and sorting abilities you see across the issue tracker. -With [Multiple Issue Boards](#multiple-issue-boards), available only in [GitLab -Enterprise Edition](https://about.gitlab.com/gitlab-ee/), your workflow gets -empowered with the ability to create multiple boards per project. - ## Use-cases GitLab Workflow allows you to discuss proposals in issues, categorize them @@ -44,15 +40,6 @@ beginning of the dev lifecycle until deployed to production - Prioritize the issues in a list by moving them vertically - Move issues between lists to organize them according to the labels you've set -To enhance the workflow exemplified above, with [Multiple Issue Boards](#multiple-issue-boards), -available only in [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/), -each team (frontend and backend) can have their own boards to organize their flow among the -members of their teams. For that, we could have, therefore, three Issue Boards for this case: - - - **Backend**, for the backend team and their own labels and workflow - - **Frontend**, same as above, for the frontend team - - **Deployment**, for the entire process (backend > frontend > staging > production) - For a broader use-case, please check the blog post [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario). -- cgit v1.2.1 From 4a5eef99018579ba0b306ab52c72a3583297c769 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 28 Jun 2017 20:56:48 -0300 Subject: copyedit --- doc/user/project/issue_board.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index ebd7b72d404..7b2755a049a 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -25,7 +25,7 @@ issue tracker. ## Use-cases GitLab Workflow allows you to discuss proposals in issues, categorize them -with labels, and and from there organize and prioritize them in Issue Boards. +with labels, and and from there organize and prioritize them with Issue Boards. - For example, let's consider this simplified development workflow: you have a repository hosting your app's codebase -- cgit v1.2.1 From 025aca03d23b3f6c6aca65b2c923adc022d6e942 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 28 Jun 2017 21:58:17 -0300 Subject: explain that use-cases are just examples --- doc/user/project/issue_board.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 7b2755a049a..6ae9fd57ad4 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -24,6 +24,9 @@ issue tracker. ## Use-cases +There are numerous use-cases for the use of Issue Boards, we will just +exemplify with a couple situations, but the possibilities go where our creativity goes. + GitLab Workflow allows you to discuss proposals in issues, categorize them with labels, and and from there organize and prioritize them with Issue Boards. -- cgit v1.2.1 From 055163f22390c17b940427319bf9b34c5bf6540e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Thu, 29 Jun 2017 11:16:01 +0800 Subject: optimize translation content based on comments --- locale/zh_TW/gitlab.po | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index 799b50c086c..bb2b84c67b0 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -11,7 +11,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2017-06-27 11:32-0400\n" +"PO-Revision-Date: 2017-06-28 11:13-0400\n" "Last-Translator: Huang Tao \n" "Language-Team: Chinese (Taiwan) (https://translate.zanata.org/project/view/GitLab)\n" "Language: zh-TW\n" @@ -88,7 +88,7 @@ msgid "ChangeTypeActionLabel|Revert in branch" msgstr "還原分支 (branch) " msgid "ChangeTypeAction|Cherry-pick" -msgstr "優選" +msgstr "挑選" msgid "ChangeTypeAction|Revert" msgstr "還原" @@ -778,16 +778,16 @@ msgid "" "The production stage shows the total time it takes between creating an issue " "and deploying the code to production. The data will be automatically added " "once you have completed the full idea to production cycle." -msgstr "營運階段顯示從建立一個議題 (issue) 到部署程式至營運的總時間。當完成從想法到產品實現的循環後,資料將自動填入。" +msgstr "營運階段顯示從建立議題 (issue) 到部署程式上線所花的時間。完成從發想到上線的完整開發週期後,資料將自動填入。" msgid "The project can be accessed by any logged in user." -msgstr "該專案允許已登入的用戶存取。" +msgstr "本專案可讓任何已登入的使用者存取" msgid "The project can be accessed without any authentication." -msgstr "該專案允許任何人存取。" +msgstr "本專案可讓任何人存取" msgid "The repository for this project does not exist." -msgstr "此專案的檔案庫 (repository)不存在。" +msgstr "本專案沒有檔案庫 (repository) " msgid "" "The review stage shows the time from creating the merge request to merging " @@ -822,7 +822,7 @@ msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間 msgid "" "This means you can not push code until you create an empty repository or " "import existing one." -msgstr "在建立一個空的檔案庫 (repository) 或匯入現有檔案庫之後,才可以推送档案。" +msgstr "這代表在您建立一個空的檔案庫 (repository) 或是匯入一個現存的檔案庫之前,您將無法上傳更新 (push) 。" msgid "Time before an issue gets scheduled" msgstr "議題 (issue) 被列入日程表的時間" @@ -861,10 +861,10 @@ msgid "Timeago|%s seconds remaining" msgstr "剩下 %s 秒" msgid "Timeago|%s weeks ago" -msgstr " %s 星期前" +msgstr " %s 週前" msgid "Timeago|%s weeks remaining" -msgstr "剩下 %s 星期" +msgstr "剩下 %s 週" msgid "Timeago|%s years ago" msgstr " %s 年前" @@ -885,7 +885,7 @@ msgid "Timeago|1 month remaining" msgstr "剩下 1 個月" msgid "Timeago|1 week remaining" -msgstr "剩下 1 星期" +msgstr "剩下 1 週" msgid "Timeago|1 year remaining" msgstr "剩下 1 年" @@ -900,7 +900,7 @@ msgid "Timeago|a month ago" msgstr " 1 個月前" msgid "Timeago|a week ago" -msgstr " 1 星期前" +msgstr " 1 週前" msgid "Timeago|a while" msgstr "剛剛" @@ -933,7 +933,7 @@ msgid "Timeago|in %s seconds" msgstr " %s 秒後" msgid "Timeago|in %s weeks" -msgstr " %s 星期後" +msgstr " %s 週後" msgid "Timeago|in %s years" msgstr " %s 年後" @@ -948,10 +948,10 @@ msgid "Timeago|in 1 minute" msgstr " 1 分鐘後" msgid "Timeago|in 1 month" -msgstr " 1 月後" +msgstr " 1 個月後" msgid "Timeago|in 1 week" -msgstr " 1 星期後" +msgstr " 1 週後" msgid "Timeago|in 1 year" msgstr " 1 年後" @@ -992,7 +992,7 @@ msgid "VisibilityLevel|Internal" msgstr "內部" msgid "VisibilityLevel|Private" -msgstr "私人" +msgstr "私有" msgid "VisibilityLevel|Public" msgstr "公開" @@ -1004,7 +1004,7 @@ msgid "We don't have enough data to show this stage." msgstr "因該階段的資料不足而無法顯示相關資訊" msgid "Withdraw Access Request" -msgstr "取消權限申请" +msgstr "取消權限申請" msgid "" "You are going to remove %{project_name_with_namespace}.\n" @@ -1034,7 +1034,7 @@ msgid "You have reached your project limit" msgstr "您已達到專案數量限制" msgid "You must sign in to star a project" -msgstr "必須登錄才能收藏專案" +msgstr "必須登入才能收藏專案" msgid "You need permission." msgstr "需要權限才能這麼做。" @@ -1061,12 +1061,12 @@ msgid "" "You won't be able to pull or push project code via %{protocol} until you " "%{set_password_link} on your account" msgstr "" -"在帳號上 %{set_password_link} 之前,將無法使用 %{protocol} 來上傳 (push) 或下載 (pull) 專案更新。" +"在帳號上 %{set_password_link} 之前, 將無法使用 %{protocol} 上傳 (push) 或下載 (pull) 程式碼。" msgid "" "You won't be able to pull or push project code via SSH until you " "%{add_ssh_key_link} to your profile" -msgstr "在個人帳號中 %{add_ssh_key_link} 之前,將無法使用 SSH 來上傳 (push) 或下載 (pull) 專案更新。" +msgstr "在個人帳號中 %{add_ssh_key_link} 之前, 將無法使用 SSH 上傳 (push) 或下載 (pull) 程式碼。" msgid "Your name" msgstr "您的名字" @@ -1083,5 +1083,5 @@ msgstr "通知信" msgid "parent" msgid_plural "parents" -msgstr[0] "父級" +msgstr[0] "上層" -- cgit v1.2.1 From 3137b886b84e2c3670b305a8194a3532afe541be Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 28 Jun 2017 21:57:35 -0500 Subject: configure webpack publicPath dynamically to account for CDN or relative path settings --- app/assets/javascripts/webpack.js | 12 ++++-------- app/helpers/webpack_helper.rb | 23 ++++++++++++++++------- lib/gitlab/gon_helper.rb | 3 +++ 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js index 37420dcafa0..9a9cf395fb8 100644 --- a/app/assets/javascripts/webpack.js +++ b/app/assets/javascripts/webpack.js @@ -1,13 +1,9 @@ /** * This is the first script loaded by webpack's runtime. It is used to manually configure - * config.output.publicPath to account for relative_url_root settings which cannot be baked-in - * to our webpack bundles. + * config.output.publicPath to account for relative_url_root or CDN settings which cannot be + * baked-in to our webpack bundles. */ -if (gon && gon.relative_url_root) { - // this assumes config.output.publicPath is an absolute path - const basePath = gon.relative_url_root.replace(/\/$/, ''); - - // eslint-disable-next-line camelcase, no-undef - __webpack_public_path__ = basePath + __webpack_public_path__; +if (gon && gon.webpack_public_path) { + __webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line } diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 6bacda9fe75..0386df22374 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -11,20 +11,29 @@ module WebpackHelper paths = Webpack::Rails::Manifest.asset_paths(source) if extension - paths = paths.select { |p| p.ends_with? ".#{extension}" } + paths.select! { |p| p.ends_with? ".#{extension}" } end - # include full webpack-dev-server url for rspec tests running locally + force_host = webpack_public_host + if force_host + paths.map! { |p| "#{force_host}#{p}" } + end + + paths + end + + def webpack_public_host if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled host = Rails.configuration.webpack.dev_server.host port = Rails.configuration.webpack.dev_server.port protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http' - - paths.map! do |p| - "#{protocol}://#{host}:#{port}#{p}" - end + "#{protocol}://#{host}:#{port}" + else + ActionController::Base.asset_host.try(:chomp, '/') end + end - paths + def webpack_public_path + "#{webpack_public_host}/#{Rails.application.config.webpack.public_path}/" end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 319633656ff..2d1ae6a5925 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -2,11 +2,14 @@ module Gitlab module GonHelper + include WebpackHelper + def add_gon_variables 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 + gon.webpack_public_path = webpack_public_path gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.shortcuts_path = help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class -- cgit v1.2.1 From b8ec1f4201c74c500e4f7010b238c7920599da7a Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 28 Jun 2017 07:12:23 +0000 Subject: Extract a `Gitlab::Scope` class. - To represent an authorization scope, such as `api` or `read_user` - This is a better abstraction than the hash we were previously using. --- app/services/access_token_validation_service.rb | 17 ++++++++-------- lib/api/api_guard.rb | 2 +- lib/api/scope.rb | 23 ++++++++++++++++++++++ lib/gitlab/auth.rb | 4 ++-- .../access_token_validation_service_spec.rb | 20 ++++++++----------- 5 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 lib/api/scope.rb diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index ee2e93a0d63..bf5aef0055e 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -28,17 +28,16 @@ class AccessTokenValidationService end # True if the token's scope contains any of the passed scopes. - def include_any_scope?(scopes) - if scopes.blank? + def include_any_scope?(required_scopes) + if required_scopes.blank? true else - # Remove any scopes whose `if` condition does not return `true` - scopes = scopes.select { |scope| scope.if.nil? || scope.if.call(request) } - - # Check whether the token is allowed access to any of the required scopes. - passed_scope_names = scopes.map { |scope| scope.name.to_sym } - token_scope_names = token.scopes.map(&:to_sym) - Set.new(passed_scope_names).intersection(Set.new(token_scope_names)).present? + # We're comparing each required_scope against all token scopes, which would + # take quadratic time. This consideration is irrelevant here because of the + # small number of records involved. + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12300/#note_33689006 + token_scopes = token.scopes.map(&:to_sym) + required_scopes.any? { |scope| scope.sufficient?(token_scopes, request) } end end end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 56f6da57555..0d2d71e336a 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -31,7 +31,7 @@ module API # the scopes are all aggregated. def allow_access_with_scope(scopes, options = {}) Array(scopes).each do |scope| - allowed_scopes << OpenStruct.new(name: scope.to_sym, if: options[:if]) + allowed_scopes << Scope.new(scope, options) end end diff --git a/lib/api/scope.rb b/lib/api/scope.rb new file mode 100644 index 00000000000..c23846d1e7d --- /dev/null +++ b/lib/api/scope.rb @@ -0,0 +1,23 @@ +# Encapsulate a scope used for authorization, such as `api`, or `read_user` +module API + class Scope + attr_reader :name, :if + + def initialize(name, options = {}) + @name = name.to_sym + @if = options[:if] + end + + # Are the `scopes` passed in sufficient to adequately authorize the passed + # request for the scope represented by the current instance of this class? + def sufficient?(scopes, request) + verify_if_condition(request) && scopes.include?(self.name) + end + + private + + def verify_if_condition(request) + self.if.nil? || self.if.call(request) + end + end +end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index ec73255b20a..6d0d638ba14 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -136,11 +136,11 @@ module Gitlab end def valid_oauth_token?(token) - token && token.accessible? && valid_scoped_token?(token, ['api']) + token && token.accessible? && valid_scoped_token?(token, [:api]) end def valid_scoped_token?(token, scopes) - scopes = scopes.map { |scope| OpenStruct.new(name: scope) } + scopes = scopes.map { |scope| API::Scope.new(scope) } AccessTokenValidationService.new(token).include_any_scope?(scopes) end diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index 279f4ed93ac..660a05e0b6d 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -1,37 +1,33 @@ require 'spec_helper' describe AccessTokenValidationService, services: true do - def scope(data) - OpenStruct.new(data) - end - describe ".include_any_scope?" do let(:request) { double("request") } it "returns true if the required scope is present in the token's scopes" do token = double("token", scopes: [:api, :read_user]) - scopes = [scope({ name: :api })] + scopes = [API::Scope.new(:api)] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if more than one of the required scopes is present in the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - scopes = [scope({ name: :api }), scope({ name: :other_scope })] + scopes = [API::Scope.new(:api), API::Scope.new(:other_scope)] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes is an exact match for the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - scopes = [scope({ name: :api }), scope({ name: :read_user }), scope({ name: :other_scope })] + scopes = [API::Scope.new(:api), API::Scope.new(:read_user), API::Scope.new(:other_scope)] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do token = double("token", scopes: [:api, :read_user]) - scopes = [scope({ name: :api }), scope({ name: :read_user }), scope({ name: :other_scope })] + scopes = [API::Scope.new(:api), API::Scope.new(:read_user), API::Scope.new(:other_scope)] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end @@ -45,7 +41,7 @@ describe AccessTokenValidationService, services: true do it "returns false if there are no scopes in common between the required scopes and the token scopes" do token = double("token", scopes: [:api, :read_user]) - scopes = [scope({ name: :other_scope })] + scopes = [API::Scope.new(:other_scope)] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(false) end @@ -53,21 +49,21 @@ describe AccessTokenValidationService, services: true do context "conditions" do it "ignores any scopes whose `if` condition returns false" do token = double("token", scopes: [:api, :read_user]) - scopes = [scope({ name: :api, if: ->(_) { false } })] + scopes = [API::Scope.new(:api, if: ->(_) { false })] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(false) end it "does not ignore scopes whose `if` condition is not set" do token = double("token", scopes: [:api, :read_user]) - scopes = [scope({ name: :api, if: ->(_) { false } }), scope({ name: :read_user })] + scopes = [API::Scope.new(:api, if: ->(_) { false }), API::Scope.new(:read_user)] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "does not ignore scopes whose `if` condition returns true" do token = double("token", scopes: [:api, :read_user]) - scopes = [scope({ name: :api, if: ->(_) { true } }), scope({ name: :read_user, if: ->(_) { false } })] + scopes = [API::Scope.new(:api, if: ->(_) { true }), API::Scope.new(:read_user, if: ->(_) { false })] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end -- cgit v1.2.1 From 598baa554c29c7f69191b4ea46a049c558c3dbe5 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 29 Jun 2017 02:31:09 -0500 Subject: add CHANGELOG.md entry for !12032 --- changelogs/unreleased/enable-webpack-code-splitting.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/enable-webpack-code-splitting.yml diff --git a/changelogs/unreleased/enable-webpack-code-splitting.yml b/changelogs/unreleased/enable-webpack-code-splitting.yml new file mode 100644 index 00000000000..d61c3b97d11 --- /dev/null +++ b/changelogs/unreleased/enable-webpack-code-splitting.yml @@ -0,0 +1,5 @@ +--- +title: Enable support for webpack code-splitting by dynamically setting publicPath + at runtime +merge_request: 12032 +author: -- cgit v1.2.1 From c20568a884bd5b65a8d8de05910eb2d3796293f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Wed, 28 Jun 2017 18:06:21 +0800 Subject: supplement traditional chinese in taiwan translation Fix #33443 --- ...ional_chinese_in_taiwan_translation_of_i18n.yml | 4 + locale/zh_TW/gitlab.po | 965 +++++++++++++++++++-- 2 files changed, 883 insertions(+), 86 deletions(-) create mode 100644 changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml diff --git a/changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml b/changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml new file mode 100644 index 00000000000..d6b1b2524c6 --- /dev/null +++ b/changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml @@ -0,0 +1,4 @@ +--- +title: Supplement Traditional Chinese in Taiwan translation of Project Page & Repository Page +merge_request: 12514 +author: Huang Tao diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index 5130572d7ed..799b50c086c 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -1,128 +1,460 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the gitlab package. -# FIRST AUTHOR , YEAR. -# +# Huang Tao , 2017. #zanata +# Lin Jen-Shin , 2017. +# Hazel Yang , 2017. +# TzeKei Lee , 2017. +# Jerry Ho , 2017. msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2017-05-04 19:24-0500\n" -"Last-Translator: HuangTao , 2017\n" -"Language-Team: Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/751" -"77/zh_TW/)\n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: zh_TW\n" -"Plural-Forms: nplurals=1; plural=0;\n" +"PO-Revision-Date: 2017-06-27 11:32-0400\n" +"Last-Translator: Huang Tao \n" +"Language-Team: Chinese (Taiwan) (https://translate.zanata.org/project/view/GitLab)\n" +"Language: zh-TW\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=1; plural=0\n" + +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "%{commit_author_link} 在 %{commit_timeago} 送交" + +msgid "About auto deploy" +msgstr "關於自動部署" + +msgid "Active" +msgstr "啟用" + +msgid "Activity" +msgstr "活動" + +msgid "Add Changelog" +msgstr "新增更新日誌" + +msgid "Add Contribution guide" +msgstr "新增協作指南" + +msgid "Add License" +msgstr "新增授權條款" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "請先新增 SSH 金鑰到您的個人帳號,才能使用 SSH 來上傳 (push) 或下載 (pull) 。" + +msgid "Add new directory" +msgstr "新增目錄" + +msgid "Archived project! Repository is read-only" +msgstr "此專案已封存!檔案庫 (repository) 為唯讀狀態" msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "確定要刪除此流水線 (pipeline) 排程嗎?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "拖放檔案到此處或者 %{upload_link}" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "分支 (branch) " + +msgid "" +"Branch %{branch_name} was created. To set up auto deploy, " +"choose a GitLab CI Yaml template and commit your changes. " +"%{link_to_autodeploy_doc}" msgstr "" +"已建立分支 (branch) %{branch_name} 。如要設定自動部署, 請選擇合適的 GitLab CI " +"Yaml 模板,然後記得要送交 (commit) 您的編輯內容。%{link_to_autodeploy_doc}\n" + +msgid "Branches" +msgstr "分支 (branch) " + +msgid "Browse files" +msgstr "瀏覽檔案" msgid "ByAuthor|by" -msgstr "作者:" +msgstr "作者:" + +msgid "CI configuration" +msgstr "CI 組態" msgid "Cancel" -msgstr "" +msgstr "取消" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "挑選到分支 (branch) " + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "還原分支 (branch) " + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "優選" + +msgid "ChangeTypeAction|Revert" +msgstr "還原" + +msgid "Changelog" +msgstr "更新日誌" + +msgid "Charts" +msgstr "統計圖" + +msgid "Cherry-pick this commit" +msgstr "挑選此更動記錄 (commit) " + +msgid "Cherry-pick this merge request" +msgstr "挑選此合併請求 (merge request) " + +msgid "CiStatusLabel|canceled" +msgstr "已取消" + +msgid "CiStatusLabel|created" +msgstr "已建立" + +msgid "CiStatusLabel|failed" +msgstr "失敗" + +msgid "CiStatusLabel|manual action" +msgstr "手動操作" + +msgid "CiStatusLabel|passed" +msgstr "已通過" + +msgid "CiStatusLabel|passed with warnings" +msgstr "通過,但有警告訊息" + +msgid "CiStatusLabel|pending" +msgstr "等待中" + +msgid "CiStatusLabel|skipped" +msgstr "已跳過" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "等待手動操作" + +msgid "CiStatusText|blocked" +msgstr "已阻擋" + +msgid "CiStatusText|canceled" +msgstr "已取消" + +msgid "CiStatusText|created" +msgstr "已建立" + +msgid "CiStatusText|failed" +msgstr "失敗" + +msgid "CiStatusText|manual" +msgstr "手動操作" + +msgid "CiStatusText|passed" +msgstr "已通過" + +msgid "CiStatusText|pending" +msgstr "等待中" + +msgid "CiStatusText|skipped" +msgstr "已跳過" + +msgid "CiStatus|running" +msgstr "執行中" msgid "Commit" msgid_plural "Commits" -msgstr[0] "送交" +msgstr[0] "更動記錄 (commit) " + +msgid "Commit message" +msgstr "更動說明 (commit) " + +msgid "CommitBoxTitle|Commit" +msgstr "送交" + +msgid "CommitMessage|Add %{file_name}" +msgstr "建立 %{file_name}" + +msgid "Commits" +msgstr "更動記錄 (commit) " + +msgid "Commits|History" +msgstr "過去更動 (commit) " + +msgid "Committed by" +msgstr "送交者為 " + +msgid "Compare" +msgstr "比較" + +msgid "Contribution guide" +msgstr "協作指南" + +msgid "Contributors" +msgstr "協作者" + +msgid "Copy URL to clipboard" +msgstr "複製網址到剪貼簿" + +msgid "Copy commit SHA to clipboard" +msgstr "複製更動記錄 (commit) 的 SHA 值到剪貼簿" + +msgid "Create New Directory" +msgstr "建立新目錄" + +msgid "Create directory" +msgstr "建立目錄" + +msgid "Create empty bare repository" +msgstr "建立一個新的 bare repository" + +msgid "Create merge request" +msgstr "發出合併請求 (merge request) " + +msgid "Create new..." +msgstr "建立..." + +msgid "CreateNewFork|Fork" +msgstr "分支 (fork) " + +msgid "CreateTag|Tag" +msgstr "建立標籤" msgid "Cron Timezone" +msgstr "Cron 時區" + +msgid "Cron syntax" +msgstr "Cron 語法" + +msgid "Custom notification events" +msgstr "自訂事件通知" + +msgid "" +"Custom notification levels are the same as participating levels. With custom " +"notification levels you will also receive notifications for select events. " +"To find out more, check out %{notification_link}." msgstr "" +"自訂通知層級相當於參與度設定。使用自訂通知層級,您可以只收到特定的事件通知。請參照 %{notification_link} 以獲得更多訊息。" -msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." -msgstr "週期分析概述了你的專案從想法到產品實現,各階段所需的時間。" +msgid "Cycle Analytics" +msgstr "週期分析" + +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." +msgstr "週期分析讓您可以有效的釐清專案從發想到產品推出所花的時間長短。" msgid "CycleAnalyticsStage|Code" msgstr "程式開發" msgid "CycleAnalyticsStage|Issue" -msgstr "議題" +msgstr "議題 (issue) " msgid "CycleAnalyticsStage|Plan" msgstr "計劃" msgid "CycleAnalyticsStage|Production" -msgstr "上線" +msgstr "營運" msgid "CycleAnalyticsStage|Review" msgstr "複閱" msgid "CycleAnalyticsStage|Staging" -msgstr "預備" +msgstr "試營運" msgid "CycleAnalyticsStage|Test" msgstr "測試" +msgid "Define a custom pattern with cron syntax" +msgstr "使用 Cron 語法自訂排程" + msgid "Delete" -msgstr "" +msgstr "刪除" msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" msgid "Description" -msgstr "" +msgstr "描述" + +msgid "Directory name" +msgstr "目錄名稱" + +msgid "Don't show again" +msgstr "不再顯示" + +msgid "Download" +msgstr "下載" + +msgid "Download tar" +msgstr "下載 tar" + +msgid "Download tar.bz2" +msgstr "下載 tar.bz2" + +msgid "Download tar.gz" +msgstr "下載 tar.gz" + +msgid "Download zip" +msgstr "下載 zip" + +msgid "DownloadArtifacts|Download" +msgstr "下載" + +msgid "DownloadCommit|Email Patches" +msgstr "電子郵件修補檔案 (patch)" + +msgid "DownloadCommit|Plain Diff" +msgstr "差異檔 (diff)" + +msgid "DownloadSource|Download" +msgstr "下載原始碼" msgid "Edit" -msgstr "" +msgstr "編輯" msgid "Edit Pipeline Schedule %{id}" -msgstr "" +msgstr "編輯 %{id} 流水線 (pipeline) 排程" + +msgid "Every day (at 4:00am)" +msgstr "每日執行(淩晨四點)" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "每月執行(每月一日淩晨四點)" + +msgid "Every week (Sundays at 4:00am)" +msgstr "每週執行(週日淩晨 四點)" msgid "Failed to change the owner" -msgstr "" +msgstr "無法變更所有權" msgid "Failed to remove the pipeline schedule" -msgstr "" +msgstr "無法刪除流水線 (pipeline) 排程" -msgid "Filter" -msgstr "" +msgid "Files" +msgstr "檔案" + +msgid "Find by path" +msgstr "以路徑搜尋" + +msgid "Find file" +msgstr "搜尋檔案" msgid "FirstPushedBy|First" -msgstr "首次推送" +msgstr "首次推送 (push) " msgid "FirstPushedBy|pushed by" -msgstr "推送者:" +msgstr "推送者 (push) :" + +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "分支 (fork) " + +msgid "ForkedFromProjectPath|Forked from" +msgstr "分支 (fork) 自" msgid "From issue creation until deploy to production" -msgstr "從議題建立至線上部署" +msgstr "從議題 (issue) 建立直到部署至營運環境" msgid "From merge request merge until deploy to production" -msgstr "從請求被合併後至線上部署" +msgstr "從請求被合併後 (merge request merged) 直到部署至營運環境" + +msgid "Go to your fork" +msgstr "前往您的分支 (fork) " + +msgid "GoToYourFork|Fork" +msgstr "前往您的分支 (fork) " + +msgid "Home" +msgstr "首頁" + +msgid "Housekeeping successfully started" +msgstr "已開始維護" + +msgid "Import repository" +msgstr "匯入檔案庫 (repository)" msgid "Interval Pattern" -msgstr "" +msgstr "循環週期" msgid "Introducing Cycle Analytics" msgstr "週期分析簡介" +msgid "LFSStatus|Disabled" +msgstr "停用" + +msgid "LFSStatus|Enabled" +msgstr "啟用" + msgid "Last %d day" msgid_plural "Last %d days" -msgstr[0] "最後 %d 天" +msgstr[0] "最近 %d 天" msgid "Last Pipeline" -msgstr "" +msgstr "最新流水線 (pipeline) " + +msgid "Last Update" +msgstr "最後更新" + +msgid "Last commit" +msgstr "最後更動記錄 (commit) " + +msgid "Learn more in the" +msgstr "了解更多" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "流水線 (pipeline) 排程說明文件" + +msgid "Leave group" +msgstr "退出群組" + +msgid "Leave project" +msgstr "退出專案" msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" -msgstr[0] "最多顯示 %d 個事件" +msgstr[0] "限制最多顯示 %d 個事件" msgid "Median" msgstr "中位數" +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "新增 SSH 金鑰" + msgid "New Issue" msgid_plural "New Issues" -msgstr[0] "新議題" +msgstr[0] "建立議題 (issue) " msgid "New Pipeline Schedule" -msgstr "" +msgstr "建立流水線 (pipeline) 排程" + +msgid "New branch" +msgstr "新分支 (branch) " + +msgid "New directory" +msgstr "新增目錄" + +msgid "New file" +msgstr "新增檔案" + +msgid "New issue" +msgstr "新增議題 (issue) " + +msgid "New merge request" +msgstr "新增合併請求 (merge request) " + +msgid "New schedule" +msgstr "新增排程" + +msgid "New snippet" +msgstr "新文字片段" + +msgid "New tag" +msgstr "新增標籤" + +msgid "No repository" +msgstr "找不到檔案庫 (repository)" msgid "No schedules" -msgstr "" +msgstr "沒有排程" msgid "Not available" msgstr "無法使用" @@ -130,135 +462,502 @@ msgstr "無法使用" msgid "Not enough data" msgstr "資料不足" +msgid "Notification events" +msgstr "事件通知" + +msgid "NotificationEvent|Close issue" +msgstr "關閉議題 (issue) " + +msgid "NotificationEvent|Close merge request" +msgstr "關閉合併請求 (merge request) " + +msgid "NotificationEvent|Failed pipeline" +msgstr "流水線 (pipeline) 失敗" + +msgid "NotificationEvent|Merge merge request" +msgstr "合併請求 (merge request) 被合併" + +msgid "NotificationEvent|New issue" +msgstr "新增議題 (issue) " + +msgid "NotificationEvent|New merge request" +msgstr "新增合併請求 (merge request) " + +msgid "NotificationEvent|New note" +msgstr "新增評論" + +msgid "NotificationEvent|Reassign issue" +msgstr "重新指派議題 (issue) " + +msgid "NotificationEvent|Reassign merge request" +msgstr "重新指派合併請求 (merge request) " + +msgid "NotificationEvent|Reopen issue" +msgstr "重啟議題 (issue)" + +msgid "NotificationEvent|Successful pipeline" +msgstr "流水線 (pipeline) 成功完成" + +msgid "NotificationLevel|Custom" +msgstr "自訂" + +msgid "NotificationLevel|Disabled" +msgstr "停用" + +msgid "NotificationLevel|Global" +msgstr "全域" + +msgid "NotificationLevel|On mention" +msgstr "提及" + +msgid "NotificationLevel|Participate" +msgstr "參與" + +msgid "NotificationLevel|Watch" +msgstr "關注" + +msgid "OfSearchInADropdown|Filter" +msgstr "篩選" + msgid "OpenedNDaysAgo|Opened" msgstr "開始於" +msgid "Options" +msgstr "選項" + msgid "Owner" -msgstr "" +msgstr "所有權" + +msgid "Pipeline" +msgstr "流水線 (pipeline) " msgid "Pipeline Health" -msgstr "流水線健康指標" +msgstr "流水線 (pipeline) 健康指數" msgid "Pipeline Schedule" -msgstr "" +msgstr "流水線 (pipeline) 排程" msgid "Pipeline Schedules" -msgstr "" +msgstr "流水線 (pipeline) 排程" msgid "PipelineSchedules|Activated" -msgstr "" +msgstr "是否啟用" msgid "PipelineSchedules|Active" -msgstr "" +msgstr "已啟用" msgid "PipelineSchedules|All" -msgstr "" +msgstr "所有" msgid "PipelineSchedules|Inactive" -msgstr "" +msgstr "未啟用" msgid "PipelineSchedules|Next Run" -msgstr "" +msgstr "下次執行時間" msgid "PipelineSchedules|None" -msgstr "" +msgstr "無" msgid "PipelineSchedules|Provide a short description for this pipeline" -msgstr "" +msgstr "請簡單說明此流水線 (pipeline) " msgid "PipelineSchedules|Take ownership" -msgstr "" +msgstr "取得所有權" msgid "PipelineSchedules|Target" -msgstr "" +msgstr "目標" + +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "自訂" + +msgid "Pipeline|with stage" +msgstr "於階段" + +msgid "Pipeline|with stages" +msgstr "於階段" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "專案 '%{project_name}' 已加入刪除佇列。" + +msgid "Project '%{project_name}' was successfully created." +msgstr "專案 '%{project_name}' 建立完成。" + +msgid "Project '%{project_name}' was successfully updated." +msgstr "專案 '%{project_name}' 更新完成。" + +msgid "Project '%{project_name}' will be deleted." +msgstr "專案 '%{project_name}' 將被刪除。" + +msgid "Project access must be granted explicitly to each user." +msgstr "專案權限必須一一指派給每個使用者。" + +msgid "Project export could not be deleted." +msgstr "匯出的專案無法被刪除。" + +msgid "Project export has been deleted." +msgstr "匯出的專案已被刪除。" + +msgid "" +"Project export link has expired. Please generate a new export from your " +"project settings." +msgstr "專案的匯出連結已失效。請到專案設定中產生新的連結。" + +msgid "Project export started. A download link will be sent by email." +msgstr "專案導出已開始。完成後下載連結會送到您的信箱。" + +msgid "Project home" +msgstr "專案首頁" + +msgid "ProjectFeature|Disabled" +msgstr "停用" + +msgid "ProjectFeature|Everyone with access" +msgstr "任何人都可存取" + +msgid "ProjectFeature|Only team members" +msgstr "只有團隊成員可以存取" + +msgid "ProjectFileTree|Name" +msgstr "名稱" + +msgid "ProjectLastActivity|Never" +msgstr "從未" msgid "ProjectLifecycle|Stage" -msgstr "專案生命週期" +msgstr "階段" + +msgid "ProjectNetworkGraph|Graph" +msgstr "分支圖" msgid "Read more" -msgstr "了解更多" +msgstr "瞭解更多" + +msgid "Readme" +msgstr "說明檔" + +msgid "RefSwitcher|Branches" +msgstr "分支 (branch) " + +msgid "RefSwitcher|Tags" +msgstr "標籤" msgid "Related Commits" -msgstr "相關的送交" +msgstr "相關的更動記錄 (commit) " msgid "Related Deployed Jobs" msgstr "相關的部署作業" msgid "Related Issues" -msgstr "相關的議題" +msgstr "相關的議題 (issue) " msgid "Related Jobs" msgstr "相關的作業" msgid "Related Merge Requests" -msgstr "相關的合併請求" +msgstr "相關的合併請求 (merge request) " msgid "Related Merged Requests" msgstr "相關已合併的請求" +msgid "Remind later" +msgstr "稍後提醒" + +msgid "Remove project" +msgstr "刪除專案" + +msgid "Request Access" +msgstr "申請權限" + +msgid "Revert this commit" +msgstr "還原此更動記錄 (commit)" + +msgid "Revert this merge request" +msgstr "還原此合併請求 (merge request) " + msgid "Save pipeline schedule" -msgstr "" +msgstr "保存流水線 (pipeline) 排程" msgid "Schedule a new pipeline" -msgstr "" +msgstr "建立流水線 (pipeline) 排程" + +msgid "Scheduling Pipelines" +msgstr "流水線 (pipeline) 計劃" + +msgid "Search branches and tags" +msgstr "搜尋分支 (branch) 和標籤" + +msgid "Select Archive Format" +msgstr "選擇下載格式" msgid "Select a timezone" -msgstr "" +msgstr "選擇時區" msgid "Select target branch" -msgstr "" +msgstr "選擇目標分支 (branch) " + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "請先設定密碼,才能使用 %{protocol} 來上傳 (push) 或下載 (pull) 。" + +msgid "Set up CI" +msgstr "設定 CI" + +msgid "Set up Koding" +msgstr "設定 Koding" + +msgid "Set up auto deploy" +msgstr "設定自動部署" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "設定密碼" msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "顯示 %d 個事件" +msgid "Source code" +msgstr "原始碼" + +msgid "StarProject|Star" +msgstr "收藏" + +msgid "Start a %{new_merge_request} with these changes" +msgstr "以這些改動建立一個新的 %{new_merge_request} " + +msgid "Switch branch/tag" +msgstr "切換分支 (branch) 或標籤" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "標籤" + +msgid "Tags" +msgstr "標籤" + msgid "Target Branch" -msgstr "" +msgstr "目標分支 (branch) " -msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." -msgstr "程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。" +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." +msgstr "" +"程式開發階段顯示從第一次更動記錄 (commit) 到建立合併請求 (merge request) 的時間。建立第一個合併請求後,資料將自動填入。" msgid "The collection of events added to the data gathered for that stage." -msgstr "與該階段相關的事件。" +msgstr "該階段中的相關事件集合。" + +msgid "The fork relationship has been removed." +msgstr "分支與主幹間的關聯 (fork relationship) 已被刪除。" -msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." -msgstr "議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。" +msgid "" +"The issue stage shows the time it takes from creating an issue to assigning " +"the issue to a milestone, or add the issue to a list on your Issue Board. " +"Begin creating issues to see data for this stage." +msgstr "" +"議題 (issue) 階段顯示從議題建立到設定里程碑所花的時間,或是議題被分類到議題看板 (issue board) " +"中所花的時間。建立第一個議題後,資料將自動填入。" msgid "The phase of the development lifecycle." -msgstr "專案開發生命週期的各個階段。" +msgstr "專案開發週期的各個階段。" -msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." -msgstr "計劃階段所顯示的是議題被排程後至第一個送交被推送的時間。一旦完成(或執行)首次的推送,資料將自動填入。" +msgid "" +"The pipelines schedule runs pipelines in the future, repeatedly, for " +"specific branches or tags. Those scheduled pipelines will inherit limited " +"project access based on their associated user." +msgstr "" +"在指定了特定分支 (branch) 或標籤後,此處的流水線 (pipeline) 排程會不斷地重複執行。\n" +"流水線排程的存取權限與專案本身相同。" -msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." -msgstr "上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。" +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "計劃階段顯示從更動記錄 (commit) 被排程至第一個推送的時間。第一次推送之後,資料將自動填入。" + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." +msgstr "營運階段顯示從建立一個議題 (issue) 到部署程式至營運的總時間。當完成從想法到產品實現的循環後,資料將自動填入。" -msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." -msgstr "複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。" +msgid "The project can be accessed by any logged in user." +msgstr "該專案允許已登入的用戶存取。" + +msgid "The project can be accessed without any authentication." +msgstr "該專案允許任何人存取。" + +msgid "The repository for this project does not exist." +msgstr "此專案的檔案庫 (repository)不存在。" + +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." +msgstr "" +"複閱階段顯示從合併請求 (merge request) 建立後至被合併的時間。當建立第一個合併請求 (merge request) 後,資料將自動填入。" -msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." -msgstr "預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。" +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." +msgstr "試營運段顯示從合併請求 (merge request) 被合併後至部署營運的時間。當第一次部署營運後,資料將自動填入" -msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." -msgstr "測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。" +msgid "" +"The testing stage shows the time GitLab CI takes to run every pipeline for " +"the related merge request. The data will automatically be added after your " +"first pipeline finishes running." +msgstr "" +"測試階段顯示相關合併請求 (merge request) 的流水線 (pipeline) 所花的時間。當第一個流水線 (pipeline) " +"執行完畢後,資料將自動填入。" msgid "The time taken by each data entry gathered by that stage." -msgstr "每筆該階段相關資料所花的時間。" +msgstr "該階段中每一個資料項目所花的時間。" -msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。" +msgid "" +"This means you can not push code until you create an empty repository or " +"import existing one." +msgstr "在建立一個空的檔案庫 (repository) 或匯入現有檔案庫之後,才可以推送档案。" + msgid "Time before an issue gets scheduled" -msgstr "議題等待排程的時間" +msgstr "議題 (issue) 被列入日程表的時間" msgid "Time before an issue starts implementation" -msgstr "議題等待開始實作的時間" +msgstr "議題 (issue) 等待開始實作的時間" msgid "Time between merge request creation and merge/close" -msgstr "合併請求被合併或是關閉的時間" +msgstr "合併請求 (merge request) 從建立到被合併或是關閉的時間" msgid "Time until first merge request" -msgstr "第一個合併請求被建立前的時間" +msgstr "第一個合併請求 (merge request) 被建立前的時間" + +msgid "Timeago|%s days ago" +msgstr " %s 天前" + +msgid "Timeago|%s days remaining" +msgstr "剩下 %s 天" + +msgid "Timeago|%s hours remaining" +msgstr "剩下 %s 小時" + +msgid "Timeago|%s minutes ago" +msgstr " %s 分鐘前" + +msgid "Timeago|%s minutes remaining" +msgstr "剩下 %s 分鐘" + +msgid "Timeago|%s months ago" +msgstr " %s 個月前" + +msgid "Timeago|%s months remaining" +msgstr "剩下 %s 月" + +msgid "Timeago|%s seconds remaining" +msgstr "剩下 %s 秒" + +msgid "Timeago|%s weeks ago" +msgstr " %s 星期前" + +msgid "Timeago|%s weeks remaining" +msgstr "剩下 %s 星期" + +msgid "Timeago|%s years ago" +msgstr " %s 年前" + +msgid "Timeago|%s years remaining" +msgstr "剩下 %s 年" + +msgid "Timeago|1 day remaining" +msgstr "剩下 1 天" + +msgid "Timeago|1 hour remaining" +msgstr "剩下 1 小時" + +msgid "Timeago|1 minute remaining" +msgstr "剩下 1 分鐘" + +msgid "Timeago|1 month remaining" +msgstr "剩下 1 個月" + +msgid "Timeago|1 week remaining" +msgstr "剩下 1 星期" + +msgid "Timeago|1 year remaining" +msgstr "剩下 1 年" + +msgid "Timeago|Past due" +msgstr "逾期" + +msgid "Timeago|a day ago" +msgstr " 1 天前" + +msgid "Timeago|a month ago" +msgstr " 1 個月前" + +msgid "Timeago|a week ago" +msgstr " 1 星期前" + +msgid "Timeago|a while" +msgstr "剛剛" + +msgid "Timeago|a year ago" +msgstr " 1 年前" + +msgid "Timeago|about %s hours ago" +msgstr "約 %s 小時前" + +msgid "Timeago|about a minute ago" +msgstr "約 1 分鐘前" + +msgid "Timeago|about an hour ago" +msgstr "約 1 小時前" + +msgid "Timeago|in %s days" +msgstr " %s 天後" + +msgid "Timeago|in %s hours" +msgstr " %s 小時後" + +msgid "Timeago|in %s minutes" +msgstr " %s 分鐘後" + +msgid "Timeago|in %s months" +msgstr " %s 個月後" + +msgid "Timeago|in %s seconds" +msgstr " %s 秒後" + +msgid "Timeago|in %s weeks" +msgstr " %s 星期後" + +msgid "Timeago|in %s years" +msgstr " %s 年後" + +msgid "Timeago|in 1 day" +msgstr " 1 天後" + +msgid "Timeago|in 1 hour" +msgstr " 1 小時後" + +msgid "Timeago|in 1 minute" +msgstr " 1 分鐘後" + +msgid "Timeago|in 1 month" +msgstr " 1 月後" + +msgid "Timeago|in 1 week" +msgstr " 1 星期後" + +msgid "Timeago|in 1 year" +msgstr " 1 年後" + +msgid "Timeago|less than a minute ago" +msgstr "不到 1 分鐘前" msgid "Time|hr" msgid_plural "Time|hrs" @@ -275,7 +974,28 @@ msgid "Total Time" msgstr "總時間" msgid "Total test time for all commits/merges" -msgstr "所有送交和合併的總測試時間" +msgstr "合併 (merge) 與更動記錄 (commit) 的總測試時間" + +msgid "Unstar" +msgstr "取消收藏" + +msgid "Upload New File" +msgstr "上傳新檔案" + +msgid "Upload file" +msgstr "上傳檔案" + +msgid "Use your global notification setting" +msgstr "使用全域通知設定" + +msgid "VisibilityLevel|Internal" +msgstr "內部" + +msgid "VisibilityLevel|Private" +msgstr "私人" + +msgid "VisibilityLevel|Public" +msgstr "公開" msgid "Want to see the data? Please ask an administrator for access." msgstr "權限不足。如需查看相關資料,請向管理員申請權限。" @@ -283,12 +1003,85 @@ msgstr "權限不足。如需查看相關資料,請向管理員申請權限。 msgid "We don't have enough data to show this stage." msgstr "因該階段的資料不足而無法顯示相關資訊" -msgid "You have reached your project limit" +msgid "Withdraw Access Request" +msgstr "取消權限申请" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" msgstr "" +"即將要刪除 %{project_name_with_namespace}。\n" +"被刪除的專案完全無法救回來喔!\n" +"真的「100%確定」要這麼做嗎?" + +msgid "" +"You are going to remove the fork relationship to source project " +"%{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "" +"將要刪除本分支專案與主幹的所有關聯 (fork relationship) 。 %{forked_from_project} " +"真的「100%確定」要這麼做嗎?" + +msgid "" +"You are going to transfer %{project_name_with_namespace} to another owner. " +"Are you ABSOLUTELY sure?" +msgstr "將要把 %{project_name_with_namespace} 的所有權轉移給另一個人。真的「100%確定」要這麼做嗎?" + +msgid "You can only add files when you are on a branch" +msgstr "只能在分支 (branch) 上建立檔案" + +msgid "You have reached your project limit" +msgstr "您已達到專案數量限制" + +msgid "You must sign in to star a project" +msgstr "必須登錄才能收藏專案" msgid "You need permission." -msgstr "您需要相關的權限。" +msgstr "需要權限才能這麼做。" + +msgid "You will not get any notifications via email" +msgstr "不會收到任何通知郵件" + +msgid "You will only receive notifications for the events you choose" +msgstr "只接收您選擇的事件通知" + +msgid "" +"You will only receive notifications for threads you have participated in" +msgstr "只接收參與主題的通知" + +msgid "You will receive notifications for any activity" +msgstr "接收所有活動的通知" + +msgid "" +"You will receive notifications only for comments in which you were " +"@mentioned" +msgstr "只接收評論中提及(@)您的通知" + +msgid "" +"You won't be able to pull or push project code via %{protocol} until you " +"%{set_password_link} on your account" +msgstr "" +"在帳號上 %{set_password_link} 之前,將無法使用 %{protocol} 來上傳 (push) 或下載 (pull) 專案更新。" + +msgid "" +"You won't be able to pull or push project code via SSH until you " +"%{add_ssh_key_link} to your profile" +msgstr "在個人帳號中 %{add_ssh_key_link} 之前,將無法使用 SSH 來上傳 (push) 或下載 (pull) 專案更新。" + +msgid "Your name" +msgstr "您的名字" msgid "day" msgid_plural "days" msgstr[0] "天" + +msgid "new merge request" +msgstr "建立合併請求" + +msgid "notification emails" +msgstr "通知信" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "父級" + -- cgit v1.2.1 From 5e7b441dc3496c71db014998f4aa304044549bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Thu, 29 Jun 2017 11:16:01 +0800 Subject: optimize translation content based on comments --- locale/zh_TW/gitlab.po | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index 799b50c086c..bb2b84c67b0 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -11,7 +11,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2017-06-27 11:32-0400\n" +"PO-Revision-Date: 2017-06-28 11:13-0400\n" "Last-Translator: Huang Tao \n" "Language-Team: Chinese (Taiwan) (https://translate.zanata.org/project/view/GitLab)\n" "Language: zh-TW\n" @@ -88,7 +88,7 @@ msgid "ChangeTypeActionLabel|Revert in branch" msgstr "還原分支 (branch) " msgid "ChangeTypeAction|Cherry-pick" -msgstr "優選" +msgstr "挑選" msgid "ChangeTypeAction|Revert" msgstr "還原" @@ -778,16 +778,16 @@ msgid "" "The production stage shows the total time it takes between creating an issue " "and deploying the code to production. The data will be automatically added " "once you have completed the full idea to production cycle." -msgstr "營運階段顯示從建立一個議題 (issue) 到部署程式至營運的總時間。當完成從想法到產品實現的循環後,資料將自動填入。" +msgstr "營運階段顯示從建立議題 (issue) 到部署程式上線所花的時間。完成從發想到上線的完整開發週期後,資料將自動填入。" msgid "The project can be accessed by any logged in user." -msgstr "該專案允許已登入的用戶存取。" +msgstr "本專案可讓任何已登入的使用者存取" msgid "The project can be accessed without any authentication." -msgstr "該專案允許任何人存取。" +msgstr "本專案可讓任何人存取" msgid "The repository for this project does not exist." -msgstr "此專案的檔案庫 (repository)不存在。" +msgstr "本專案沒有檔案庫 (repository) " msgid "" "The review stage shows the time from creating the merge request to merging " @@ -822,7 +822,7 @@ msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間 msgid "" "This means you can not push code until you create an empty repository or " "import existing one." -msgstr "在建立一個空的檔案庫 (repository) 或匯入現有檔案庫之後,才可以推送档案。" +msgstr "這代表在您建立一個空的檔案庫 (repository) 或是匯入一個現存的檔案庫之前,您將無法上傳更新 (push) 。" msgid "Time before an issue gets scheduled" msgstr "議題 (issue) 被列入日程表的時間" @@ -861,10 +861,10 @@ msgid "Timeago|%s seconds remaining" msgstr "剩下 %s 秒" msgid "Timeago|%s weeks ago" -msgstr " %s 星期前" +msgstr " %s 週前" msgid "Timeago|%s weeks remaining" -msgstr "剩下 %s 星期" +msgstr "剩下 %s 週" msgid "Timeago|%s years ago" msgstr " %s 年前" @@ -885,7 +885,7 @@ msgid "Timeago|1 month remaining" msgstr "剩下 1 個月" msgid "Timeago|1 week remaining" -msgstr "剩下 1 星期" +msgstr "剩下 1 週" msgid "Timeago|1 year remaining" msgstr "剩下 1 年" @@ -900,7 +900,7 @@ msgid "Timeago|a month ago" msgstr " 1 個月前" msgid "Timeago|a week ago" -msgstr " 1 星期前" +msgstr " 1 週前" msgid "Timeago|a while" msgstr "剛剛" @@ -933,7 +933,7 @@ msgid "Timeago|in %s seconds" msgstr " %s 秒後" msgid "Timeago|in %s weeks" -msgstr " %s 星期後" +msgstr " %s 週後" msgid "Timeago|in %s years" msgstr " %s 年後" @@ -948,10 +948,10 @@ msgid "Timeago|in 1 minute" msgstr " 1 分鐘後" msgid "Timeago|in 1 month" -msgstr " 1 月後" +msgstr " 1 個月後" msgid "Timeago|in 1 week" -msgstr " 1 星期後" +msgstr " 1 週後" msgid "Timeago|in 1 year" msgstr " 1 年後" @@ -992,7 +992,7 @@ msgid "VisibilityLevel|Internal" msgstr "內部" msgid "VisibilityLevel|Private" -msgstr "私人" +msgstr "私有" msgid "VisibilityLevel|Public" msgstr "公開" @@ -1004,7 +1004,7 @@ msgid "We don't have enough data to show this stage." msgstr "因該階段的資料不足而無法顯示相關資訊" msgid "Withdraw Access Request" -msgstr "取消權限申请" +msgstr "取消權限申請" msgid "" "You are going to remove %{project_name_with_namespace}.\n" @@ -1034,7 +1034,7 @@ msgid "You have reached your project limit" msgstr "您已達到專案數量限制" msgid "You must sign in to star a project" -msgstr "必須登錄才能收藏專案" +msgstr "必須登入才能收藏專案" msgid "You need permission." msgstr "需要權限才能這麼做。" @@ -1061,12 +1061,12 @@ msgid "" "You won't be able to pull or push project code via %{protocol} until you " "%{set_password_link} on your account" msgstr "" -"在帳號上 %{set_password_link} 之前,將無法使用 %{protocol} 來上傳 (push) 或下載 (pull) 專案更新。" +"在帳號上 %{set_password_link} 之前, 將無法使用 %{protocol} 上傳 (push) 或下載 (pull) 程式碼。" msgid "" "You won't be able to pull or push project code via SSH until you " "%{add_ssh_key_link} to your profile" -msgstr "在個人帳號中 %{add_ssh_key_link} 之前,將無法使用 SSH 來上傳 (push) 或下載 (pull) 專案更新。" +msgstr "在個人帳號中 %{add_ssh_key_link} 之前, 將無法使用 SSH 上傳 (push) 或下載 (pull) 程式碼。" msgid "Your name" msgstr "您的名字" @@ -1083,5 +1083,5 @@ msgstr "通知信" msgid "parent" msgid_plural "parents" -msgstr[0] "父級" +msgstr[0] "上層" -- cgit v1.2.1 From af89b19d69c043b2e9d40ef260adf97e00d1f791 Mon Sep 17 00:00:00 2001 From: Alexander Randa Date: Thu, 29 Jun 2017 13:30:33 +0300 Subject: Replaces 'dashboard/new-project.feature' spinach with rspec --- ...23036-replace-dashboard-new-project-spinach.yml | 4 + features/dashboard/new_project.feature | 30 -------- features/steps/dashboard/new_project.rb | 59 -------------- features/steps/dashboard/starred_projects.rb | 15 ---- spec/features/dashboard/projects_spec.rb | 16 +++- spec/features/projects/new_project_spec.rb | 89 +++++++++++++++++----- 6 files changed, 85 insertions(+), 128 deletions(-) create mode 100644 changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml delete mode 100644 features/dashboard/new_project.feature delete mode 100644 features/steps/dashboard/new_project.rb delete mode 100644 features/steps/dashboard/starred_projects.rb diff --git a/changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml b/changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml new file mode 100644 index 00000000000..a5f78202c93 --- /dev/null +++ b/changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml @@ -0,0 +1,4 @@ +--- +title: Replace 'dashboard/new-project.feature' spinach with rspec +merge_request: 12550 +author: Alexander Randa (@randaalex) diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature deleted file mode 100644 index 046e2815d4e..00000000000 --- a/features/dashboard/new_project.feature +++ /dev/null @@ -1,30 +0,0 @@ -@dashboard -Feature: New Project -Background: - Given I sign in as a user - And I own project "Shop" - And I visit dashboard page - And I click "New project" link - - @javascript - Scenario: I should see New Projects page - Then I see "New Project" page - Then I see all possible import options - - @javascript - Scenario: I should see instructions on how to import from Git URL - Given I see "New Project" page - When I click on "Repo by URL" - Then I see instructions on how to import from Git URL - - @javascript - Scenario: I should see instructions on how to import from GitHub - Given I see "New Project" page - When I click on "Import project from GitHub" - Then I am redirected to the GitHub import page - - @javascript - Scenario: I should see Google Code import page - Given I see "New Project" page - When I click on "Google Code" - Then I redirected to Google Code import page diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb deleted file mode 100644 index 530fd6f7bdb..00000000000 --- a/features/steps/dashboard/new_project.rb +++ /dev/null @@ -1,59 +0,0 @@ -class Spinach::Features::NewProject < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - step 'I click "New project" link' do - page.within '#content-body' do - click_link "New project" - end - end - - step 'I click "New project" in top right menu' do - page.within '.header-content' do - click_link "New project" - end - end - - step 'I see "New Project" page' do - expect(page).to have_content('Project path') - expect(page).to have_content('Project name') - end - - step 'I see all possible import options' do - expect(page).to have_link('GitHub') - expect(page).to have_link('Bitbucket') - expect(page).to have_link('GitLab.com') - expect(page).to have_link('Google Code') - expect(page).to have_button('Repo by URL') - expect(page).to have_link('GitLab export') - end - - step 'I click on "Import project from GitHub"' do - first('.import_github').click - end - - step 'I am redirected to the GitHub import page' do - expect(page).to have_content('Import Projects from GitHub') - expect(current_path).to eq new_import_github_path - end - - step 'I click on "Repo by URL"' do - first('.import_git').click - end - - step 'I see instructions on how to import from Git URL' do - git_import_instructions = first('.js-toggle-content') - expect(git_import_instructions).to be_visible - expect(git_import_instructions).to have_content "Git repository URL" - end - - step 'I click on "Google Code"' do - first('.import_google_code').click - end - - step 'I redirected to Google Code import page' do - expect(page).to have_content('Import projects from Google Code') - expect(current_path).to eq new_import_google_code_path - end -end diff --git a/features/steps/dashboard/starred_projects.rb b/features/steps/dashboard/starred_projects.rb deleted file mode 100644 index c33813e550b..00000000000 --- a/features/steps/dashboard/starred_projects.rb +++ /dev/null @@ -1,15 +0,0 @@ -class Spinach::Features::DashboardStarredProjects < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - step 'I starred project "Community"' do - current_user.toggle_star(Project.find_by(name: 'Community')) - end - - step 'I should not see project "Shop"' do - page.within '.projects-list' do - expect(page).not_to have_content('Shop') - end - end -end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index f29186f368d..e9ef5d7983a 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -RSpec.describe 'Dashboard Projects', feature: true do +feature 'Dashboard Projects' do let(:user) { create(:user) } - let(:project) { create(:project, name: "awesome stuff") } + let(:project) { create(:project, name: 'awesome stuff') } let(:project2) { create(:project, :public, name: 'Community project') } before do @@ -15,6 +15,14 @@ RSpec.describe 'Dashboard Projects', feature: true do expect(page).to have_content('awesome stuff') end + it 'shows "New project" button' do + visit dashboard_projects_path + + page.within '#content-body' do + expect(page).to have_link('New project') + end + end + context 'when last_repository_updated_at, last_activity_at and update_at are present' do it 'shows the last_repository_updated_at attribute as the update date' do project.update_attributes!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago) @@ -47,8 +55,8 @@ RSpec.describe 'Dashboard Projects', feature: true do end end - describe "with a pipeline", redis: true do - let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) } + describe 'with a pipeline', redis: true do + let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) } before do # Since the cache isn't updated when a new pipeline is created diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 37d9a97033b..22fb1223739 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -1,13 +1,27 @@ -require "spec_helper" +require 'spec_helper' -feature "New project", feature: true do +feature 'New project' do let(:user) { create(:admin) } before do - gitlab_sign_in(user) + sign_in(user) end - context "Visibility level selector" do + it 'shows "New project" page' do + visit new_project_path + + expect(page).to have_content('Project path') + expect(page).to have_content('Project name') + + expect(page).to have_link('GitHub') + expect(page).to have_link('Bitbucket') + expect(page).to have_link('GitLab.com') + expect(page).to have_link('Google Code') + expect(page).to have_button('Repo by URL') + expect(page).to have_link('GitLab export') + end + + context 'Visibility level selector' do Gitlab::VisibilityLevel.options.each do |key, level| it "sets selector to #{key}" do stub_application_setting(default_project_visibility: level) @@ -28,20 +42,20 @@ feature "New project", feature: true do end end - context "Namespace selector" do - context "with user namespace" do + context 'Namespace selector' do + context 'with user namespace' do before do visit new_project_path end - it "selects the user namespace" do - namespace = find("#project_namespace_id") + it 'selects the user namespace' do + namespace = find('#project_namespace_id') expect(namespace.text).to eq user.username end end - context "with group namespace" do + context 'with group namespace' do let(:group) { create(:group, :private, owner: user) } before do @@ -49,13 +63,13 @@ feature "New project", feature: true do visit new_project_path(namespace_id: group.id) end - it "selects the group namespace" do - namespace = find("#project_namespace_id option[selected]") + it 'selects the group namespace' do + namespace = find('#project_namespace_id option[selected]') expect(namespace.text).to eq group.name end - context "on validation error" do + context 'on validation error' do before do fill_in('project_path', with: 'private-group-project') choose('Internal') @@ -64,15 +78,15 @@ feature "New project", feature: true do expect(page).to have_css '.project-edit-errors .alert.alert-danger' end - it "selects the group namespace" do - namespace = find("#project_namespace_id option[selected]") + it 'selects the group namespace' do + namespace = find('#project_namespace_id option[selected]') expect(namespace.text).to eq group.name end end end - context "with subgroup namespace" do + context 'with subgroup namespace' do let(:group) { create(:group, :private, owner: user) } let(:subgroup) { create(:group, parent: group) } @@ -81,8 +95,8 @@ feature "New project", feature: true do visit new_project_path(namespace_id: subgroup.id) end - it "selects the group namespace" do - namespace = find("#project_namespace_id option[selected]") + it 'selects the group namespace' do + namespace = find('#project_namespace_id option[selected]') expect(namespace.text).to eq subgroup.full_path end @@ -94,10 +108,45 @@ feature "New project", feature: true do visit new_project_path end - it 'does not autocomplete sensitive git repo URL' do - autocomplete = find('#project_import_url')['autocomplete'] + context 'from git repository url' do + before do + first('.import_git').click + end + + it 'does not autocomplete sensitive git repo URL' do + autocomplete = find('#project_import_url')['autocomplete'] + + expect(autocomplete).to eq('off') + end + + it 'shows import instructions' do + git_import_instructions = first('.js-toggle-content') - expect(autocomplete).to eq('off') + expect(git_import_instructions).to be_visible + expect(git_import_instructions).to have_content 'Git repository URL' + end + end + + context 'from GitHub' do + before do + first('.import_github').click + end + + it 'shows import instructions' do + expect(page).to have_content('Import Projects from GitHub') + expect(current_path).to eq new_import_github_path + end + end + + context 'from Google Code' do + before do + first('.import_google_code').click + end + + it 'shows import instructions' do + expect(page).to have_content('Import projects from Google Code') + expect(current_path).to eq new_import_google_code_path + end end end end -- cgit v1.2.1 From 93bf4263b99b9c6ccb1d3a75b052f16939ed91db Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 29 Jun 2017 11:16:03 -0300 Subject: Axil's review --- doc/user/project/issue_board.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 6ae9fd57ad4..439b9a9fcc6 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -24,11 +24,11 @@ issue tracker. ## Use-cases -There are numerous use-cases for the use of Issue Boards, we will just -exemplify with a couple situations, but the possibilities go where our creativity goes. +There are numerous use-cases for Issue Boards, we will just +exemplify with a couple situations. GitLab Workflow allows you to discuss proposals in issues, categorize them -with labels, and and from there organize and prioritize them with Issue Boards. +with labels, and from there organize and prioritize them with Issue Boards. - For example, let's consider this simplified development workflow: you have a repository hosting your app's codebase -- cgit v1.2.1 From 76be9f9c68199f258ca7f82966471ff6cf24fe3e Mon Sep 17 00:00:00 2001 From: Diego de Souza Mendes Date: Thu, 29 Jun 2017 11:23:05 -0300 Subject: updated version of issues baord images (doc) --- doc/user/project/img/issue_board.png | Bin 76461 -> 51439 bytes doc/user/project/img/issue_board_add_list.png | Bin 23632 -> 17312 bytes .../project/img/issue_board_welcome_message.png | Bin 120751 -> 26533 bytes .../project/img/issue_boards_add_issues_modal.png | Bin 177057 -> 29176 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png index b636cb294b8..cf7f519f783 100644 Binary files a/doc/user/project/img/issue_board.png and b/doc/user/project/img/issue_board.png differ diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png index cdfc466d23f..973d9f7cde4 100644 Binary files a/doc/user/project/img/issue_board_add_list.png and b/doc/user/project/img/issue_board_add_list.png differ diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png index 5318e6ea4a9..127b9b08cc7 100644 Binary files a/doc/user/project/img/issue_board_welcome_message.png and b/doc/user/project/img/issue_board_welcome_message.png differ diff --git a/doc/user/project/img/issue_boards_add_issues_modal.png b/doc/user/project/img/issue_boards_add_issues_modal.png index 33049dce74f..bedaf724a15 100644 Binary files a/doc/user/project/img/issue_boards_add_issues_modal.png and b/doc/user/project/img/issue_boards_add_issues_modal.png differ -- cgit v1.2.1 From 3f89d9c4303e16014c3fafe4e020d584b30aca67 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 29 Jun 2017 22:23:08 +0800 Subject: Update changelog entry to reflect that we're renaming --- changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml b/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml index 3414f1ed8ca..4948d415bed 100644 --- a/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml +++ b/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml @@ -1,5 +1,5 @@ --- -title: Remove duplicated variables with the same key for projects. Add environment_scope +title: Rename duplicated variables with the same key for projects. Add environment_scope column to variables and add unique constraint to make sure that no variables could be created with the same key within a project merge_request: 12363 -- cgit v1.2.1 From ac089ba5af5387067739248ac7205af8ff0dcfab Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 29 Jun 2017 22:24:28 +0800 Subject: Only indent if the subsequent line is a subquery --- ...0170622135451_rename_duplicated_variable_key.rb | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/db/migrate/20170622135451_rename_duplicated_variable_key.rb b/db/migrate/20170622135451_rename_duplicated_variable_key.rb index 1005a212131..368718ab0ce 100644 --- a/db/migrate/20170622135451_rename_duplicated_variable_key.rb +++ b/db/migrate/20170622135451_rename_duplicated_variable_key.rb @@ -8,18 +8,18 @@ class RenameDuplicatedVariableKey < ActiveRecord::Migration def up execute(<<~SQL) UPDATE ci_variables - SET #{key} = CONCAT(#{key}, #{underscore}, id) - WHERE id IN ( - SELECT * - FROM ( -- MySQL requires an extra layer - SELECT dup.id - FROM ci_variables dup - INNER JOIN (SELECT max(id) AS id, #{key}, project_id - FROM ci_variables tmp - GROUP BY #{key}, project_id) var - USING (#{key}, project_id) where dup.id <> var.id - ) dummy - ) + SET #{key} = CONCAT(#{key}, #{underscore}, id) + WHERE id IN ( + SELECT * + FROM ( -- MySQL requires an extra layer + SELECT dup.id + FROM ci_variables dup + INNER JOIN (SELECT max(id) AS id, #{key}, project_id + FROM ci_variables tmp + GROUP BY #{key}, project_id) var + USING (#{key}, project_id) where dup.id <> var.id + ) dummy + ) SQL end -- cgit v1.2.1 From 2c74e73f6f994346192acb2e723b026c2ec55f8b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 29 Jun 2017 22:25:31 +0800 Subject: We no longer test the presence of the key --- spec/models/ci/variable_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 77201c6f419..50f7c029af8 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -7,7 +7,6 @@ describe Ci::Variable, models: true do describe 'validations' do it { is_expected.to include_module(HasVariable) } - it { is_expected.to validate_presence_of(:key) } it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope) } it { is_expected.to validate_length_of(:key).is_at_most(255) } it { is_expected.to allow_value('foo').for(:key) } -- cgit v1.2.1 From 651e32fd5ae382e6afa3a46993b0f62659b35f6d Mon Sep 17 00:00:00 2001 From: leungpeng Date: Thu, 29 Jun 2017 15:37:55 +0000 Subject: Fixed typo in gitlab_flow.md --- doc/workflow/gitlab_flow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index e10ccc4fc46..ea28968fbb2 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -300,7 +300,7 @@ If there are no merge conflicts and the feature branches are short lived the ris If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests. If you have long lived feature branches that last for more than a few days you should make your issues smaller. -## Working wih feature branches +## Working with feature branches ![Shell output showing git pull output](git_pull.png) -- cgit v1.2.1 From a67bf93b78ce8aa0bc61aab5a2008983746d4a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 16 Jun 2017 17:27:57 +0200 Subject: Fix the performance bar spec that was not asserting the right thing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/features/user_can_display_performance_bar_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb index 1bd7e038939..24fff1a3052 100644 --- a/spec/features/user_can_display_performance_bar_spec.rb +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'User can display performacne bar', :js do +describe 'User can display performance bar', :js do shared_examples 'performance bar is disabled' do it 'does not show the performance bar by default' do expect(page).not_to have_css('#peek') @@ -27,8 +27,8 @@ describe 'User can display performacne bar', :js do find('body').native.send_keys('pb') end - it 'does not show the performance bar by default' do - expect(page).not_to have_css('#peek') + it 'shows the performance bar' do + expect(page).to have_css('#peek') end end end -- cgit v1.2.1 From 7bbb2777c84f19da6998492c34d7b7316e3c4447 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 29 Jun 2017 17:16:34 +0100 Subject: Fixed new navgiation bar logo height in Safari --- app/assets/stylesheets/new_nav.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 441bfc479f6..3ce5b4fd073 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -11,20 +11,19 @@ header.navbar-gitlab-new { padding-left: 0; .title-container { + align-items: stretch; padding-top: 0; overflow: visible; } .title { - display: block; - height: 100%; + display: flex; padding-right: 0; color: currentColor; > a { display: flex; align-items: center; - height: 100%; padding-top: 3px; padding-right: $gl-padding; padding-left: $gl-padding; -- cgit v1.2.1 From fa93156528bca4306e040a1b73720b6411942fcf Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 23 Jun 2017 17:02:33 -0700 Subject: Defer project destroys within a namespace in Groups::DestroyService#async_execute Group#destroy would actually hard-delete all associated projects even though the acts_as_paranoia gem is used, preventing Projects::DestroyService from doing any work. We first noticed this while trying to log all projects deletion to the Geo log. --- app/models/namespace.rb | 6 +++ app/services/groups/destroy_service.rb | 3 +- .../sh-fix-project-destroy-in-namespace.yml | 4 ++ spec/models/namespace_spec.rb | 11 +++++ spec/services/groups/destroy_service_spec.rb | 52 +++++++++++++--------- 5 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 changelogs/unreleased/sh-fix-project-destroy-in-namespace.yml diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 743e0513e02..672eab94c07 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -219,6 +219,12 @@ class Namespace < ActiveRecord::Base parent.present? end + def soft_delete_without_removing_associations + # We can't use paranoia's `#destroy` since this will hard-delete projects. + # Project uses `pending_delete` instead of the acts_as_paranoia gem. + self.deleted_at = Time.now + end + private def repository_storage_paths diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index d40d280140a..80c51cb5a72 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -1,8 +1,7 @@ module Groups class DestroyService < Groups::BaseService def async_execute - # Soft delete via paranoia gem - group.destroy + group.soft_delete_without_removing_associations job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") end diff --git a/changelogs/unreleased/sh-fix-project-destroy-in-namespace.yml b/changelogs/unreleased/sh-fix-project-destroy-in-namespace.yml new file mode 100644 index 00000000000..9309f961345 --- /dev/null +++ b/changelogs/unreleased/sh-fix-project-destroy-in-namespace.yml @@ -0,0 +1,4 @@ +--- +title: Defer project destroys within a namespace in Groups::DestroyService#async_execute +merge_request: +author: diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index d4f898f6d9f..62c4ea01ce1 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -342,6 +342,17 @@ describe Namespace, models: true do end end + describe '#soft_delete_without_removing_associations' do + let(:project1) { create(:project_empty_repo, namespace: namespace) } + + it 'updates the deleted_at timestamp but preserves projects' do + namespace.soft_delete_without_removing_associations + + expect(Project.all).to include(project1) + expect(namespace.deleted_at).not_to be_nil + end + end + describe '#user_ids_for_project_authorizations' do it 'returns the user IDs for which to refresh authorizations' do expect(namespace.user_ids_for_project_authorizations) diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index a37257d1bf4..d59b37bee36 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -15,6 +15,14 @@ describe Groups::DestroyService, services: true do group.add_user(user, Gitlab::Access::OWNER) end + def destroy_group(group, user, async) + if async + Groups::DestroyService.new(group, user).async_execute + else + Groups::DestroyService.new(group, user).execute + end + end + shared_examples 'group destruction' do |async| context 'database records' do before do @@ -30,30 +38,14 @@ describe Groups::DestroyService, services: true do context 'file system' do context 'Sidekiq inline' do before do - # Run sidekiq immediatly to check that renamed dir will be removed + # Run sidekiq immediately to check that renamed dir will be removed Sidekiq::Testing.inline! { destroy_group(group, user, async) } end - it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } - it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey } - end - - context 'Sidekiq fake' do - before do - # Don't run sidekiq to check if renamed repository exists - Sidekiq::Testing.fake! { destroy_group(group, user, async) } + it 'verifies that paths have been deleted' do + expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey + expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey end - - it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } - it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy } - end - end - - def destroy_group(group, user, async) - if async - Groups::DestroyService.new(group, user).async_execute - else - Groups::DestroyService.new(group, user).execute end end end @@ -61,6 +53,26 @@ describe Groups::DestroyService, services: true do describe 'asynchronous delete' do it_behaves_like 'group destruction', true + context 'Sidekiq fake' do + before do + # Don't run Sidekiq to verify that group and projects are not actually destroyed + Sidekiq::Testing.fake! { destroy_group(group, user, true) } + end + + after do + # Clean up stale directories + gitlab_shell.rm_namespace(project.repository_storage_path, group.path) + gitlab_shell.rm_namespace(project.repository_storage_path, remove_path) + end + + it 'verifies original paths and projects still exist' do + expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_truthy + expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey + expect(Project.unscoped.count).to eq(1) + expect(Group.unscoped.count).to eq(2) + end + end + context 'potential race conditions' do context "when the `GroupDestroyWorker` task runs immediately" do it "deletes the group" do -- cgit v1.2.1 From 687775e7348cff3cbbd5bbcd02e7b88d592f2b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 29 Jun 2017 18:32:35 +0200 Subject: Make clear that Go 1.8 is required since GitLab 9.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable --- doc/update/9.1-to-9.2.md | 41 ++++++++++++++++++++++++++++++----------- doc/update/9.2-to-9.3.md | 9 ++++----- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md index e7d97fde14e..225a4dcc924 100644 --- a/doc/update/9.1-to-9.2.md +++ b/doc/update/9.1-to-9.2.md @@ -70,7 +70,27 @@ curl --location https://yarnpkg.com/install.sh | bash - More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). -### 5. Get latest code +### 5. Update Go + +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code ```bash cd /home/git/gitlab @@ -97,7 +117,7 @@ cd /home/git/gitlab sudo -u git -H git checkout 9-2-stable-ee ``` -### 6. Update gitlab-shell +### 7. Update gitlab-shell ```bash cd /home/git/gitlab-shell @@ -107,11 +127,10 @@ sudo -u git -H git checkout v$( Date: Mon, 12 Jun 2017 14:43:21 -0400 Subject: Render add-diff-note button with server. This commit moves the rendering of the button back to the server, and shows/hides it using opacity rather than display. It also removes the transform applied to the button on hover (scale). Previously, both of these factors automatically triggered a reflow, which creates a performance bottleneck on pages with larger DOM size. MR: !12103 --- app/assets/javascripts/diff.js | 5 +- .../diff_notes/components/diff_note_avatars.js | 4 +- app/assets/javascripts/files_comment_button.js | 193 +++++++-------------- app/assets/javascripts/notes.js | 9 +- app/assets/javascripts/single_file_diff.js | 4 + app/assets/stylesheets/pages/diff.scss | 8 +- app/assets/stylesheets/pages/notes.scss | 10 +- app/helpers/notes_helper.rb | 12 ++ app/views/projects/diffs/_line.html.haml | 3 +- app/views/projects/diffs/_parallel_view.html.haml | 7 +- features/steps/shared/diff_note.rb | 2 +- spec/features/expand_collapse_diffs_spec.rb | 2 +- 12 files changed, 105 insertions(+), 154 deletions(-) diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 725ec7b9c70..1be9df19c81 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this */ import './lib/utils/url_utility'; +import FilesCommentButton from './files_comment_button'; const UNFOLD_COUNT = 20; let isBound = false; @@ -8,8 +9,10 @@ let isBound = false; class Diff { constructor() { const $diffFile = $('.files .diff-file'); + $diffFile.singleFileDiff(); - $diffFile.filesCommentButton(); + + FilesCommentButton.init($diffFile); $diffFile.each((index, file) => new gl.ImageFile(file)); diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 517bdb6be09..c37249c060a 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({ const notesCount = this.notesCount; $(this.$el).closest('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0) + .toggleClass('no-comment-btn', notesCount > 0) .nextUntil('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0); + .toggleClass('no-comment-btn', notesCount > 0); }, toggleDiscussionsToggleState() { const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 534e651b030..d02e4cd5876 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,150 +1,73 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ -/* global FilesCommentButton */ /* global notes */ -let $commentButtonTemplate; - -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'; - - LINE_HOLDER_CLASS = '.line_holder'; - - LINE_NUMBER_CLASS = 'diff-line-num'; - - LINE_CONTENT_CLASS = 'line_content'; - - UNFOLDABLE_LINE_CLASS = 'js-unfold'; - - EMPTY_CELL_CLASS = 'empty-cell'; - - OLD_LINE_CLASS = 'old_line'; - - LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; - - TEXT_FILE_SELECTOR = '.text-file'; - - function FilesCommentButton(filesContainerElement) { - this.render = this.render.bind(this); - this.hideButton = this.hideButton.bind(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; +/* Developer beware! Do not add logic to showButton or hideButton + * that will force a reflow. Doing so will create a signficant performance + * bottleneck for pages with large diffs. For a comprehensive list of what + * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a + */ + +const LINE_NUMBER_CLASS = 'diff-line-num'; +const UNFOLDABLE_LINE_CLASS = 'js-unfold'; +const NO_COMMENT_CLASS = 'no-comment-btn'; +const EMPTY_CELL_CLASS = 'empty-cell'; +const OLD_LINE_CLASS = 'old_line'; +const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`; +const DIFF_CONTAINER_SELECTOR = '.files'; +const DIFF_EXPANDED_CLASS = 'diff-expanded'; + +export default { + init($diffFile) { + /* Caching is used only when the following members are *true*. This is because there are likely to be + * differently configured versions of diffs in the same session. However if these values are true, they + * will be true in all cases */ + + if (!this.userCanCreateNote) { + // data-can-create-note is an empty string when true, otherwise undefined + this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; } - textFileElement = this.getTextFileElement($currentTarget); - buttonParentElement.append(this.buildButton({ - discussionID: lineContentElement.attr('data-discussion-id'), - lineType: lineContentElement.attr('data-line-type'), - - noteableType: textFileElement.attr('data-noteable-type'), - noteableID: textFileElement.attr('data-noteable-id'), - commitID: textFileElement.attr('data-commit-id'), - noteType: lineContentElement.attr('data-note-type'), - - // LegacyDiffNote - lineCode: lineContentElement.attr('data-line-code'), - - // DiffNote - position: lineContentElement.attr('data-position') - })); - }; - - 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-discussion-id': buttonAttributes.discussionID, - 'data-line-type': buttonAttributes.lineType, - - 'data-noteable-type': buttonAttributes.noteableType, - 'data-noteable-id': buttonAttributes.noteableID, - 'data-commit-id': buttonAttributes.commitID, - 'data-note-type': buttonAttributes.noteType, - - // LegacyDiffNote - 'data-line-code': buttonAttributes.lineCode, - - // DiffNote - 'data-position': buttonAttributes.position - }); - }; - - FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { - return hoveredElement.closest(TEXT_FILE_SELECTOR); - }; - - 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); + if (typeof notes !== 'undefined' && !this.isParallelView) { + this.isParallelView = notes.isParallelView && notes.isParallelView(); } - }; - 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); + if (this.userCanCreateNote) { + $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) + .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e)); } - }; + }, - FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { - return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); - }; + showButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { - return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== ''; - }; + if (!this.validateButtonParent(buttonParentElement)) return; - return FilesCommentButton; -})(); + buttonParentElement.classList.add('is-over'); + buttonParentElement.nextElementSibling.classList.add('is-over'); + }, -$.fn.filesCommentButton = function() { - $commentButtonTemplate = $(''); + hideButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - 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))); + buttonParentElement.classList.remove('is-over'); + buttonParentElement.nextElementSibling.classList.remove('is-over'); + }, + + getButtonParent(hoveredElement, isParallelView) { + if (isParallelView) { + if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) { + return hoveredElement.previousElementSibling; + } + } else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) { + return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`); } - }); + return hoveredElement; + }, + + validateButtonParent(buttonParentElement) { + return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) && + !buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) && + !buttonParentElement.classList.contains(NO_COMMENT_CLASS) && + !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS); + }, }; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 34476f3303f..46d77b31ffd 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -829,6 +829,8 @@ export default class Notes { */ setupDiscussionNoteForm(dataHolder, form) { // setup note target + const diffFileData = dataHolder.closest('.text-file'); + var discussionID = dataHolder.data('discussionId'); if (discussionID) { @@ -839,9 +841,10 @@ export default class Notes { form.attr('data-line-code', dataHolder.data('lineCode')); form.find('#line_type').val(dataHolder.data('lineType')); - form.find('#note_noteable_type').val(dataHolder.data('noteableType')); - form.find('#note_noteable_id').val(dataHolder.data('noteableId')); - form.find('#note_commit_id').val(dataHolder.data('commitId')); + form.find('#note_noteable_type').val(diffFileData.data('noteableType')); + form.find('#note_noteable_id').val(diffFileData.data('noteableId')); + form.find('#note_commit_id').val(diffFileData.data('commitId')); + form.find('#note_type').val(dataHolder.data('noteType')); // LegacyDiffNote diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index c44892dae3d..9316a2af0b7 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ +import FilesCommentButton from './files_comment_button'; + (function() { window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; @@ -78,6 +80,8 @@ gl.diffNotesCompileComponents(); } + FilesCommentButton.init($(_this.file)); + if (cb) cb(); }; })(this)); diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b58922626fa..631649b363f 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -476,6 +476,7 @@ height: 19px; width: 19px; margin-left: -15px; + z-index: 100; &:hover { .diff-comment-avatar, @@ -491,7 +492,7 @@ transform: translateX((($i * $x-pos) - $x-pos)); &:hover { - transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); + transform: translateX((($i * $x-pos) - $x-pos)); } } } @@ -542,6 +543,7 @@ height: 19px; padding: 0; transition: transform .1s ease-out; + z-index: 100; svg { position: absolute; @@ -555,10 +557,6 @@ fill: $white-light; } - &:hover { - transform: scale(1.2); - } - &:focus { outline: 0; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 53d5cf2f7bc..303425041df 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -628,8 +628,14 @@ ul.notes { * Line note button on the side of diffs */ +.line_holder .is-over:not(.no-comment-btn) { + .add-diff-note { + opacity: 1; + } +} + .add-diff-note { - display: none; + opacity: 0; margin-top: -2px; border-radius: 50%; background: $white-light; @@ -642,13 +648,11 @@ ul.notes { width: 23px; height: 23px; border: 1px solid $blue-500; - transition: transform .1s ease-in-out; &:hover { background: $blue-500; border-color: $blue-600; color: $white-light; - transform: scale(1.15); } &:active { diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 64ad7b280cb..ecc6cd6c6c5 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -47,6 +47,18 @@ module NotesHelper data end + def add_diff_note_button(line_code, position, line_type) + return if @diff_notes_disabled + + button_tag '', + class: 'add-diff-note js-add-diff-note-button', + type: 'submit', name: 'button', + data: diff_view_line_data(line_code, position, line_type), + title: 'Add a comment to this line' do + icon('comment-o') + end + end + def link_to_reply_discussion(discussion, line_type = nil) return unless current_user diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 43708d22a0c..cd0fb21f8a7 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -19,6 +19,7 @@ - if plain = link_text - else + = add_diff_note_button(line_code, diff_file.position(line), type) %a{ href: "##{line_code}", data: { linenumber: link_text } } - discussion = line_discussions.try(:first) - if discussion && discussion.resolvable? && !plain @@ -29,7 +30,7 @@ = link_text - else %a{ href: "##{line_code}", data: { linenumber: link_text } } - %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< + %td.line_content.noteable_line{ class: type }< - if email %pre= line.text - else diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 8e5f4d2573d..56d63250714 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,4 +1,5 @@ / Side-by-side diff view + .text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - diff_file.parallel_diff_lines.each do |line| @@ -18,11 +19,12 @@ - left_line_code = diff_file.line_code(left) - 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 } } + = add_diff_note_button(left_line_code, left_position, 'old') %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } - discussion_left = discussions_left.try(:first) - 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) + %td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel @@ -38,11 +40,12 @@ - right_line_code = diff_file.line_code(right) - 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 } } + = add_diff_note_button(right_line_code, right_position, 'new') %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } - discussion_right = discussions_right.try(:first) - 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) + %td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 36fc315599e..2c59ec5bb06 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -232,7 +232,7 @@ module SharedDiffNote end def click_parallel_diff_line(code, line_type) - find(".line_content.parallel.#{line_type}[data-line-code='#{code}']").trigger 'mouseover' + find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover' find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' end end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index ea749528c11..d492a15ea17 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -129,7 +129,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do before do large_diff.find('.diff-line-num', match: :prefer_exact).hover - large_diff.find('.add-diff-note').click + large_diff.find('.add-diff-note', match: :prefer_exact).click large_diff.find('.note-textarea').send_keys comment_text large_diff.find_button('Comment').click wait_for_requests -- cgit v1.2.1 From 0664715d491311f02074110c639f60fbf80b0e1d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 29 Jun 2017 16:53:56 +0000 Subject: Cleanup codeclimate.json file generated by CI --- .gitlab-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e52b656599c..8d1a9ce8014 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -474,9 +474,10 @@ codeclimate: services: - docker:dind script: + - docker pull stedolan/jq - docker pull codeclimate/codeclimate - - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json - - sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' codeclimate.json + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json + - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name},{fingerprint},{location})' > codeclimate.json artifacts: paths: [codeclimate.json] -- cgit v1.2.1 From ed503c988a44244603c3a4f3251e401b095e468c Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 29 Jun 2017 14:22:19 -0300 Subject: fix spelling --- doc/user/project/issue_board.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 439b9a9fcc6..74e10dfb07c 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -22,9 +22,9 @@ With the Issue Board you can have a different view of your issues while also maintaining the same filtering and sorting abilities you see across the issue tracker. -## Use-cases +## Use cases -There are numerous use-cases for Issue Boards, we will just +There are numerous use cases for Issue Boards, we will just exemplify with a couple situations. GitLab Workflow allows you to discuss proposals in issues, categorize them -- cgit v1.2.1 From 55c6be2fb08b53ddf98307c0cc4667b1385a2ced Mon Sep 17 00:00:00 2001 From: Taurie Davis Date: Thu, 29 Jun 2017 17:34:06 +0000 Subject: Clean up issuable lists --- app/assets/stylesheets/pages/issuable.scss | 55 ++++++++++++++++ app/assets/stylesheets/pages/labels.scss | 6 +- app/views/projects/issues/_issue.html.haml | 66 ++++++++++--------- .../merge_requests/_merge_request.html.haml | 76 +++++++++++----------- app/views/shared/_issuable_meta_data.html.haml | 8 +-- changelogs/unreleased/issueable-list-cleanup.yml | 4 ++ features/steps/project/merge_requests.rb | 2 +- .../issues/filtered_search/filter_issues_spec.rb | 2 +- 8 files changed, 145 insertions(+), 74 deletions(-) create mode 100644 changelogs/unreleased/issueable-list-cleanup.yml diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index e3ebcc8af6c..057d457b3a2 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -597,7 +597,38 @@ .issue-info-container { -webkit-flex: 1; flex: 1; + display: flex; padding-right: $gl-padding; + + .issue-main-info { + flex: 1 auto; + margin-right: 10px; + } + + .issuable-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + flex: 1 0 auto; + + .controls { + margin-bottom: 2px; + line-height: 20px; + padding: 0; + } + + .issue-updated-at { + line-height: 20px; + } + } + + @media(max-width: $screen-xs-max) { + .issuable-meta { + .controls li { + margin-right: 0; + } + } + } } .issue-check { @@ -609,6 +640,30 @@ vertical-align: text-top; } } + + .issuable-milestone, + .issuable-info, + .task-status, + .issuable-updated-at { + font-weight: normal; + color: $gl-text-color-secondary; + + a { + color: $gl-text-color; + + .fa { + color: $gl-text-color-secondary; + } + } + } + + @media(max-width: $screen-md-max) { + .task-status, + .issuable-due-date, + .project-ref-path { + display: none; + } + } } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index b158416b940..ee48f7a3626 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -279,5 +279,9 @@ .label-link { display: inline-block; - vertical-align: text-top; + vertical-align: top; + + .label { + vertical-align: inherit; + } } diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 9e4e6934ca9..6a0d96f50cd 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -4,43 +4,49 @@ .issue-check.hidden = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" .issue-info-container - .issue-title.title - %span.issue-title-text - = confidential_icon(issue) - = link_to issue.title, issue_path(issue) + .issue-main-info + .issue-title.title + %span.issue-title-text + = confidential_icon(issue) + = link_to issue.title, issue_path(issue) + - if issue.tasks? + %span.task-status.hidden-xs +   + = issue.task_status + + .issuable-info + %span.issuable-reference + #{issuable_reference(issue)} + %span.issuable-authored.hidden-xs + · + opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} + by #{link_to_member(@project, issue.author, avatar: false)} + - if issue.milestone + %span.issuable-milestone.hidden-xs +   + = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do + = icon('clock-o') + = issue.milestone.title + - if issue.due_date + %span.issuable-due-date.hidden-xs{ class: "#{'cred' if issue.overdue?}" } +   + = icon('calendar') + = issue.due_date.to_s(:medium) + - if issue.labels.any? +   + - issue.labels.each do |label| + = link_to_label(label, subject: issue.project, css_class: 'label-link') + + .issuable-meta %ul.controls - if issue.closed? - %li + %li.issuable-status CLOSED - - if issue.assignees.any? %li = render 'shared/issuable/assignees', project: @project, issue: issue = render 'shared/issuable_meta_data', issuable: issue - .issue-info - #{issuable_reference(issue)} · - opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} - by #{link_to_member(@project, issue.author, avatar: false)} - - if issue.milestone -   - = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do - = icon('clock-o') - = issue.milestone.title - - if issue.due_date - %span{ class: "#{'cred' if issue.overdue?}" } -   - = icon('calendar') - = issue.due_date.to_s(:medium) - - if issue.labels.any? -   - - issue.labels.each do |label| - = link_to_label(label, subject: issue.project, css_class: 'label-link') - - if issue.tasks? -   - %span.task-status - = issue.task_status - - .pull-right.issue-updated-at + .pull-right.issuable-updated-at.hidden-xs %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')} diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index c13110deb16..3599f2271b5 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -4,58 +4,60 @@ = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" .issue-info-container - .merge-request-title.title - %span.merge-request-title-text - = link_to merge_request.title, merge_request_path(merge_request) + .issue-main-info + .merge-request-title.title + %span.merge-request-title-text + = link_to merge_request.title, merge_request_path(merge_request) + - if merge_request.tasks? + %span.task-status.hidden-xs +   + = merge_request.task_status + + .issuable-info + %span.issuable-reference + #{issuable_reference(merge_request)} + %span.issuable-authored.hidden-xs + · + opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} + by #{link_to_member(@project, merge_request.author, avatar: false)} + - if merge_request.milestone + %span.issuable-milestone.hidden-xs +   + = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do + = icon('clock-o') + = merge_request.milestone.title + - if merge_request.target_project.default_branch != merge_request.target_branch + %span.project-ref-path +   + = link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do + = icon('code-fork') + = merge_request.target_branch + - if merge_request.labels.any? +   + - merge_request.labels.each do |label| + = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link') + + .issuable-meta %ul.controls - if merge_request.merged? - %li + %li.issuable-status.hidden-xs MERGED - elsif merge_request.closed? - %li + %li.issuable-status.hidden-xs = icon('ban') CLOSED - - if merge_request.head_pipeline - %li + %li.issuable-pipeline-status.hidden-xs = render_pipeline_status(merge_request.head_pipeline) - - if merge_request.open? && merge_request.broken? - %li + %li.issuable-pipeline-broken.hidden-xs = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do = icon('exclamation-triangle') - - if merge_request.assignee %li = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") = render 'shared/issuable_meta_data', issuable: merge_request - .merge-request-info - #{issuable_reference(merge_request)} · - opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} - by #{link_to_member(@project, merge_request.author, avatar: false)} - - if merge_request.target_project.default_branch != merge_request.target_branch -   - = link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do - = icon('code-fork') - = merge_request.target_branch - - - if merge_request.milestone -   - = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do - = icon('clock-o') - = merge_request.milestone.title - - - if merge_request.labels.any? -   - - merge_request.labels.each do |label| - = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link') - - - if merge_request.tasks? -   - %span.task-status - = merge_request.task_status - - .pull-right.hidden-xs + .pull-right.issuable-updated-at.hidden-xs %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')} diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 1d4fd71522d..435acbc634c 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -5,21 +5,21 @@ - issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count - if issuable_mr > 0 - %li + %li.issuable-mr.hidden-xs = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged') = issuable_mr - if upvotes > 0 - %li + %li.issuable-upvotes.hidden-xs = icon('thumbs-up') = upvotes - if downvotes > 0 - %li + %li.issuable-downvotes.hidden-xs = icon('thumbs-down') = downvotes -%li +%li.issuable-comments.hidden-xs = link_to issuable_url, class: ('no-comments' if note_count.zero?) do = icon('comments') = note_count diff --git a/changelogs/unreleased/issueable-list-cleanup.yml b/changelogs/unreleased/issueable-list-cleanup.yml new file mode 100644 index 00000000000..d3d67d04574 --- /dev/null +++ b/changelogs/unreleased/issueable-list-cleanup.yml @@ -0,0 +1,4 @@ +--- +title: Clean up UI of issuable lists and make more responsive +merge_request: +author: diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 69f5d0f8410..dceeed5aafe 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -65,7 +65,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should not see "master" branch' do - expect(find('.merge-request-info')).not_to have_content "master" + expect(find('.issuable-info')).not_to have_content "master" end step 'I should see "feature_conflict" branch' do diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 863f8f75cd8..4cb728cc82b 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -459,7 +459,7 @@ describe 'Filter issues', js: true, feature: true do context 'issue label clicked' do before do - find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click + find('.issues-list .issue .issue-main-info .issuable-info a .label', text: multiple_words_label.title).click end it 'filters' do -- cgit v1.2.1 From 6cdbb1e687bcb787f749b51c472ec8449d0ca0e4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 29 Jun 2017 10:19:54 -0500 Subject: Fix 'New merge request' button for users who don't have push access to canonical project --- app/views/projects/merge_requests/index.html.haml | 8 +++++--- changelogs/unreleased/dm-empty-state-new-merge-request.yml | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/dm-empty-state-new-merge-request.yml diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 86996e488a1..1e30cc09894 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -13,6 +13,9 @@ = render 'projects/last_push' +- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) +- new_merge_request_path = namespace_project_new_merge_request_path(merge_project.namespace, merge_project) if merge_project + - if @project.merge_requests.exists? %div{ class: container_class } .top-area @@ -20,9 +23,8 @@ .nav-controls - if @can_bulk_update = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - if merge_project - = link_to namespace_project_new_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do + = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do New merge request = render 'shared/issuable/search_bar', type: :merge_requests @@ -33,4 +35,4 @@ .merge-requests-holder = render 'merge_requests' - else - = render 'shared/empty_states/merge_requests', button_path: namespace_project_new_merge_request_path(@project.namespace, @project) + = render 'shared/empty_states/merge_requests', button_path: new_merge_request_path diff --git a/changelogs/unreleased/dm-empty-state-new-merge-request.yml b/changelogs/unreleased/dm-empty-state-new-merge-request.yml new file mode 100644 index 00000000000..5fad7a0f883 --- /dev/null +++ b/changelogs/unreleased/dm-empty-state-new-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Fix 'New merge request' button for users who don't have push access to canonical + project +merge_request: +author: -- cgit v1.2.1 From 9bdd113035be84220d5c1d66ef52e86972ef23ba Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Thu, 29 Jun 2017 18:09:21 +0000 Subject: Resolve "Select branch dropdown is too close to branch name" --- app/assets/stylesheets/pages/tree.scss | 6 +++++ app/views/projects/commits/show.html.haml | 37 ++++++++++++++++--------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 9b2ed0d68a1..dc88cf3e699 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,4 +1,5 @@ .tree-holder { + .nav-block { margin: 10px 0; @@ -15,6 +16,11 @@ .btn-group { margin-left: 10px; } + + .control { + float: left; + margin-left: 10px; + } } .tree-ref-holder { diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index fabd825aec8..7ed7e441344 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -8,27 +8,28 @@ = render "head" %div{ class: container_class } - .row-content-block.second-block.content-component-block.flex-container-block - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'commits' + .tree-holder + .nav-block + .tree-ref-container + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'commits' + + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs + .tree-controls.hidden-xs.hidden-sm + - if @merge_request.present? + .control + = link_to _("View open merge request"), namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' + - elsif create_mr_button?(@repository.root_ref, @ref) + .control + = link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs - - .block-controls.hidden-xs.hidden-sm - - if @merge_request.present? .control - = link_to _("View open merge request"), namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' - - elsif create_mr_button?(@repository.root_ref, @ref) + = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do + = search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } .control - = link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' - - .control - = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do - = search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } - .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do - = icon("rss") + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do + = icon("rss") %div{ id: dom_id(@project) } %ol#commits-list.list-unstyled.content_list -- cgit v1.2.1 From a7335c1188d32edd58aebdb818dbf4c49899149b Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Wed, 28 Jun 2017 13:16:40 -0500 Subject: Refactored tests and added a breakpoint to the merge_request_tabs --- app/assets/javascripts/merge_request_tabs.js | 3 +++ spec/features/issuables/user_sees_sidebar_spec.rb | 30 ++++++++++++++++++++++ spec/features/issues/issue_sidebar_spec.rb | 17 ------------ .../features/issuable_sidebar_shared_examples.rb | 9 +++++++ 4 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 spec/features/issuables/user_sees_sidebar_spec.rb create mode 100644 spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index c25d9a95d14..0cbac94e98c 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -144,6 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; this.resetViewContainer(); this.mountPipelinesView(); } else { + if (Breakpoints.get().getBreakpointSize() !== 'xs') { + this.expandView(); + } this.resetViewContainer(); this.destroyPipelinesView(); } diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb new file mode 100644 index 00000000000..4d7a7dc1806 --- /dev/null +++ b/spec/features/issuables/user_sees_sidebar_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe 'Issue Sidebar on Mobile' do + include MobileHelpers + + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:issue) { create(:issue, project: project) } + let!(:user) { create(:user)} + + before do + sign_in(user) + end + + context 'mobile sidebar on merge requests', js: true do + before do + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + end + + it_behaves_like "issue sidebar stays collapsed on mobile" + end + + context 'mobile sidebar on issues', js: true do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like "issue sidebar stays collapsed on mobile" + end +end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index cd06b5af675..09724781a27 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -6,7 +6,6 @@ feature 'Issue Sidebar', feature: true do let(:group) { create(:group, :nested) } let(:project) { create(:project, :public, namespace: group) } let(:issue) { create(:issue, project: project) } - let(:merge_request) { create(:merge_request, source_project: project) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project, title: 'bug') } @@ -155,22 +154,6 @@ feature 'Issue Sidebar', feature: true do end end - context 'as a allowed mobile user', js: true do - before do - project.team << [user, :developer] - resize_screen_xs - end - - context 'mobile sidebar' do - it 'collapses the sidebar for small screens on an issue/merge_request' do - visit_issue(project, issue) - expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') - visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) - expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') - end - end - end - context 'as a guest' do before do project.team << [user, :guest] diff --git a/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb new file mode 100644 index 00000000000..96c821b26f7 --- /dev/null +++ b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb @@ -0,0 +1,9 @@ +shared_examples 'issue sidebar stays collapsed on mobile' do + before do + resize_screen_xs + end + + it 'keeps the sidebar collapsed' do + expect(page).not_to have_css('.right-sidebar.right-sidebar-collapsed') + end +end -- cgit v1.2.1 From c051d47de83ed0446a05d3972a806f953a6d4bb9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 29 Jun 2017 18:42:45 +0000 Subject: Fix codeclimate job in .gitlab-ci.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8d1a9ce8014..5723b836c76 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -477,7 +477,7 @@ codeclimate: - docker pull stedolan/jq - docker pull codeclimate/codeclimate - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json - - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name},{fingerprint},{location})' > codeclimate.json + - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json artifacts: paths: [codeclimate.json] -- cgit v1.2.1 From def0f6281029659503f32c4b93e90ac6cbfd6884 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 26 Jun 2017 15:28:09 -0400 Subject: Remove initTimeagoTimeout and let timeago.js update timeagos internally. MR: !12468 --- .../javascripts/lib/utils/datetime_utility.js | 24 +++------------------- app/assets/javascripts/main.js | 2 +- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index bfcc50996cc..034a1ec2054 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -112,29 +112,11 @@ window.dateFormat = dateFormat; return timefor; }; - w.gl.utils.cachedTimeagoElements = []; w.gl.utils.renderTimeago = function($els) { - if (!$els && !w.gl.utils.cachedTimeagoElements.length) { - w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render')); - } else if ($els) { - w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray()); - } - - w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText); - }; - - w.gl.utils.updateTimeagoText = function(el) { - const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang); - - if (el.textContent !== formattedDate) { - el.textContent = formattedDate; - } - }; - - w.gl.utils.initTimeagoTimeout = function() { - gl.utils.renderTimeago(); + const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); - gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000); + // timeago.js sets timeouts internally for each timeago value to be updated in real time + gl.utils.getTimeago().render(timeagoEls); }; w.gl.utils.getDayDifference = function(a, b) { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index d27b4ec78c6..de1b658f602 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -358,7 +358,7 @@ $(function () { gl.awardsHandler = new AwardsHandler(); new Aside(); - gl.utils.initTimeagoTimeout(); + gl.utils.renderTimeago(); $(document).trigger('init.scrolling-tabs'); }); -- cgit v1.2.1 From 7765dd6a1d04189da49b8a36ffc5bb8d22e5184f Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 29 Jun 2017 11:57:59 -0700 Subject: bugfix: use `require_dependency` to bring in DeclarativePolicy --- app/models/ability.rb | 2 +- app/policies/base_policy.rb | 2 +- lib/api/projects.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index d2b8a8447b5..0b6bcbde5d9 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,4 +1,4 @@ -require 'declarative_policy' +require_dependency 'declarative_policy' class Ability class << self diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 00067ce756e..191c2e78a08 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -1,4 +1,4 @@ -require 'declarative_policy' +require_dependency 'declarative_policy' class BasePolicy < DeclarativePolicy::Base desc "User is an instance admin" diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 886e97a2638..d0bd64b2972 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -1,4 +1,4 @@ -require 'declarative_policy' +require_dependency 'declarative_policy' module API # Projects API -- cgit v1.2.1 From f4e6aba1bbeca043a29b4903cef2f5b99a1faac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 29 Jun 2017 15:22:40 -0400 Subject: Set the GL_REPOSITORY env variable on Gitlab::Git::Hook --- app/services/git_hooks_service.rb | 6 ++-- app/services/git_operation_service.rb | 2 +- lib/gitlab/git/hook.rb | 8 +++-- .../projects/import_export/import_file_spec.rb | 2 +- spec/lib/gitlab/git/hook_spec.rb | 38 ++++++++++++++++------ .../lib/gitlab/import_export/repo_restorer_spec.rb | 2 +- spec/models/repository_spec.rb | 15 +++------ spec/services/git_hooks_service_spec.rb | 7 ++-- 8 files changed, 47 insertions(+), 33 deletions(-) diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index d222d1e63aa..eab65d09299 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -3,8 +3,8 @@ class GitHooksService attr_accessor :oldrev, :newrev, :ref - def execute(user, repo_path, oldrev, newrev, ref) - @repo_path = repo_path + def execute(user, project, oldrev, newrev, ref) + @project = project @user = Gitlab::GlId.gl_id(user) @oldrev = oldrev @newrev = newrev @@ -26,7 +26,7 @@ class GitHooksService private def run_hook(name) - hook = Gitlab::Git::Hook.new(name, @repo_path) + hook = Gitlab::Git::Hook.new(name, @project) hook.trigger(@user, oldrev, newrev, ref) end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index ed6ea638235..43636fde0be 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -120,7 +120,7 @@ class GitOperationService def with_hooks(ref, newrev, oldrev) GitHooksService.new.execute( user, - repository.path_to_repo, + repository.project, oldrev, newrev, ref) do |service| diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index bd90d24a2ec..5042916343b 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -4,9 +4,10 @@ module Gitlab GL_PROTOCOL = 'web'.freeze attr_reader :name, :repo_path, :path - def initialize(name, repo_path) + def initialize(name, project) @name = name - @repo_path = repo_path + @project = project + @repo_path = project.repository.path @path = File.join(repo_path.strip, 'hooks', name) end @@ -38,7 +39,8 @@ module Gitlab vars = { 'GL_ID' => gl_id, 'PWD' => repo_path, - 'GL_PROTOCOL' => GL_PROTOCOL + 'GL_PROTOCOL' => GL_PROTOCOL, + 'GL_REPOSITORY' => Gitlab::GlRepository.gl_repository(@project, false) } options = { diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index a111aa87c52..3f8d2255298 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -98,6 +98,6 @@ feature 'Import/Export - project import integration test', feature: true, js: tr end def project_hook_exists?(project) - Gitlab::Git::Hook.new('post-receive', project.repository.path).exists? + Gitlab::Git::Hook.new('post-receive', project).exists? end end diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index 3f279c21865..73518656bde 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -4,18 +4,20 @@ require 'fileutils' describe Gitlab::Git::Hook, lib: true do describe "#trigger" do let(:project) { create(:project, :repository) } + let(:repo_path) { project.repository.path } let(:user) { create(:user) } + let(:gl_id) { Gitlab::GlId.gl_id(user) } def create_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write('exit 0') end end def create_failing_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write(<<-HOOK) echo 'regular message from the hook' echo 'error message from the hook' 1>&2 @@ -27,13 +29,29 @@ describe Gitlab::Git::Hook, lib: true do ['pre-receive', 'post-receive', 'update'].each do |hook_name| context "when triggering a #{hook_name} hook" do context "when the hook is successful" do + let(:hook_path) { File.join(repo_path, 'hooks', hook_name) } + let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) } + let(:env) do + { + 'GL_ID' => gl_id, + 'PWD' => repo_path, + 'GL_PROTOCOL' => 'web', + 'GL_REPOSITORY' => gl_repository + } + end + it "returns success with no errors" do create_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = Gitlab::Git::Hook.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + if hook_name != 'update' + expect(Open3).to receive(:popen3) + .with(env, hook_path, chdir: repo_path).and_call_original + end + + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_blank end @@ -42,11 +60,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook is unsuccessful" do it "returns failure with errors" do create_failing_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = Gitlab::Git::Hook.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be false expect(errors).to eq("error message from the hook\n") end @@ -56,11 +74,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook doesn't exist" do it "returns success with no errors" do - hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path) + hook = Gitlab::Git::Hook.new('unknown_hook', project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_nil end diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index 168a59e5139..30b6a0d8845 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::ImportExport::RepoRestorer, services: true do it 'has the webhooks' do restorer.restore - expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist + expect(Gitlab::Git::Hook.new('post-receive', project)).to exist end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3e984ec7588..c69f0a495db 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -780,7 +780,7 @@ describe Repository, models: true do context 'when pre hooks were successful' do it 'runs without errors' do expect_any_instance_of(GitHooksService).to receive(:execute) - .with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature') + .with(user, project, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -823,12 +823,7 @@ describe Repository, models: true do service = GitHooksService.new expect(GitHooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with( - user, - repository.path_to_repo, - old_rev, - new_rev, - 'refs/heads/feature') + .with(user, project, old_rev, new_rev, 'refs/heads/feature') .and_yield(service).and_return(true) end @@ -1474,9 +1469,9 @@ describe Repository, models: true do it 'passes commit SHA to pre-receive and update hooks,\ and tag SHA to post-receive hook' do - pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo) - update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo) - post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo) + pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project) + update_hook = Gitlab::Git::Hook.new('update', project) + post_receive_hook = Gitlab::Git::Hook.new('post-receive', project) allow(Gitlab::Git::Hook).to receive(:new) .and_return(pre_receive_hook, update_hook, post_receive_hook) diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index ac7ccfbaab0..213678c27f5 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -12,7 +12,6 @@ describe GitHooksService, services: true do @oldrev = sample_commit.parent_id @newrev = sample_commit.id @ref = 'refs/heads/feature' - @repo_path = project.repository.path_to_repo end describe '#execute' do @@ -21,7 +20,7 @@ describe GitHooksService, services: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - service.execute(user, @repo_path, @blankrev, @newrev, @ref) { } + service.execute(user, project, @blankrev, @newrev, @ref) { } end end @@ -31,7 +30,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end @@ -43,7 +42,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end -- cgit v1.2.1 From dfcf1b5a44a544ed4b253d88dffac0d35c576211 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Thu, 29 Jun 2017 11:43:47 +0100 Subject: Backport changes to Projects::IssuesController and the search bar --- app/controllers/projects/issues_controller.rb | 20 ++++++++++++++++---- app/views/shared/_sort_dropdown.html.haml | 4 +++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index dfc6baa34a4..ca483c105b6 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -267,10 +267,22 @@ class Projects::IssuesController < Projects::ApplicationController end def issue_params - params.require(:issue).permit( - :title, :assignee_id, :position, :description, :confidential, - :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: [] - ) + params.require(:issue).permit(*issue_params_attributes) + end + + def issue_params_attributes + %i[ + title + assignee_id + position + description + confidential + milestone_id + due_date + state_event + task_num + lock_version + ] + [{ label_ids: [], assignee_ids: [] }] end def authenticate_user! diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index a212c714826..785a500e44e 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,3 +1,5 @@ +- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues' + .dropdown.inline.prepend-left-10 %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } } - if @sort.present? @@ -23,7 +25,7 @@ = sort_title_milestone_soon = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do = sort_title_milestone_later - - if controller.controller_name == 'issues' || controller.action_name == 'issues' + - if viewing_issues = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do = sort_title_due_date_soon = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do -- cgit v1.2.1 From 2446252cfd1f5a7d7329099e8d6371fcffa99971 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 29 Jun 2017 18:53:32 -0300 Subject: Expires full_path cache after project is renamed --- app/models/concerns/routable.rb | 11 +++++++++-- app/models/project.rb | 1 + spec/models/concerns/routable_spec.rb | 12 ++++++++++++ spec/models/project_spec.rb | 2 ++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index ec7796a9dbb..2305e01d3f1 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -103,8 +103,11 @@ module Routable def full_path return uncached_full_path unless RequestStore.active? - key = "routable/full_path/#{self.class.name}/#{self.id}" - RequestStore[key] ||= uncached_full_path + RequestStore[full_path_key] ||= uncached_full_path + end + + def expires_full_path_cache + RequestStore.delete(full_path_key) if RequestStore.active? end def build_full_path @@ -135,6 +138,10 @@ module Routable path_changed? || parent_changed? end + def full_path_key + @full_path_key ||= "routable/full_path/#{self.class.name}/#{self.id}" + end + def build_full_name if parent && name parent.human_name + ' / ' + name diff --git a/app/models/project.rb b/app/models/project.rb index a75c5209955..862ee027e54 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -963,6 +963,7 @@ class Project < ActiveRecord::Base begin gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") send_move_instructions(old_path_with_namespace) + expires_full_path_cache @old_path_with_namespace = old_path_with_namespace diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 65f05121b40..be82a601b36 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -132,6 +132,18 @@ describe Group, 'Routable' do end end + describe '#expires_full_path_cache' do + context 'with RequestStore active', :request_store do + it 'expires the full_path cache' do + expect(group).to receive(:uncached_full_path).twice.and_call_original + + 3.times { group.full_path } + group.expires_full_path_cache + 3.times { group.full_path } + end + end + end + describe '#full_name' do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1390848ff4a..6ff4ec3d417 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1216,6 +1216,8 @@ describe Project, models: true do expect(project).to receive(:expire_caches_before_rename) + expect(project).to receive(:expires_full_path_cache) + project.rename_repo end -- cgit v1.2.1 From b3b034b849800e71ce8872a6f4db8fb6b9b91b4e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 29 Jun 2017 19:11:33 -0300 Subject: Expires full_path cache after repository is transferred --- app/services/projects/transfer_service.rb | 1 + spec/services/projects/transfer_service_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index fd701e33524..4bb98e5cb4e 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -78,6 +78,7 @@ module Projects Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) project.old_path_with_namespace = @old_path + project.expires_full_path_cache execute_system_hooks end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 76c52d55ae5..441a5276c56 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -30,6 +30,12 @@ describe Projects::TransferService, services: true do transfer_project(project, user, group) end + it 'expires full_path cache' do + expect(project).to receive(:expires_full_path_cache) + + transfer_project(project, user, group) + end + it 'executes system hooks' do expect_any_instance_of(Projects::TransferService).to receive(:execute_system_hooks) -- cgit v1.2.1 From 7fe97d940ee49afb85f5c8df0e9ab267ba40391d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 29 Jun 2017 18:53:56 -0300 Subject: Add CHANGELOG --- changelogs/unreleased/fix-2801.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-2801.yml diff --git a/changelogs/unreleased/fix-2801.yml b/changelogs/unreleased/fix-2801.yml new file mode 100644 index 00000000000..4f0aa06b6ba --- /dev/null +++ b/changelogs/unreleased/fix-2801.yml @@ -0,0 +1,4 @@ +--- +title: Expires full_path cache after a repository is renamed/transferred +merge_request: +author: -- cgit v1.2.1 From 553346a4f4ffff4ed8cfabc090529a02021f1ca4 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Thu, 29 Jun 2017 22:31:58 +0000 Subject: Remove empty afterEach() from issue_show app_spec.js --- spec/javascripts/issue_show/components/app_spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 9df92318864..bc13373a27e 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -42,9 +42,6 @@ describe('Issuable output', () => { }).$mount(); }); - afterEach(() => { - }); - it('should render a title/description/edited and update title/description/edited on update', (done) => { vm.poll.options.successCallback({ json() { -- cgit v1.2.1 From 790c740cce8487d1155607355d06f42ee2e83fac Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 29 Jun 2017 10:54:10 -0700 Subject: Increase CI retries to 4 for these examples By default it is 2 tries in CI. --- spec/lib/gitlab/health_checks/fs_shards_check_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 61c10d47434..c8c402b4f71 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -97,6 +97,12 @@ describe Gitlab::HealthChecks::FsShardsCheck do }.with_indifferent_access end + # Unsolved intermittent failure in CI https://gitlab.com/gitlab-org/gitlab-ce/issues/31128 + around(:each) do |example| + times_to_try = ENV['CI'] ? 4 : 1 + example.run_with_retry retry: times_to_try + end + it { is_expected.to all(have_attributes(labels: { shard: :default })) } it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) } -- cgit v1.2.1 From 53c409cb07d295043c5e1cff738c84b3aa7405a8 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 29 Jun 2017 16:53:41 -0700 Subject: =?UTF-8?q?Rspec/AroundBlock=20doesn=E2=80=99t=20know=20about=20rs?= =?UTF-8?q?pec-retry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/lib/gitlab/health_checks/fs_shards_check_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index c8c402b4f71..fbacbc4a338 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -98,7 +98,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do end # Unsolved intermittent failure in CI https://gitlab.com/gitlab-org/gitlab-ce/issues/31128 - around(:each) do |example| + around(:each) do |example| # rubocop:disable RSpec/AroundBlock times_to_try = ENV['CI'] ? 4 : 1 example.run_with_retry retry: times_to_try end -- cgit v1.2.1 From bd1c596264db1b6c9d3aa46026ab6f2acbe33520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 30 Jun 2017 09:58:47 +0800 Subject: add Italian translation to I18N translated of Cycle Analytics Page translated of Project Page translated of Repository Page add Changelog Closes #34544 --- ...ytics-page-&-project-page-&-repository-page.yml | 4 + lib/gitlab/i18n.rb | 3 +- locale/it/gitlab.po | 1142 ++++++++++++++++++++ locale/it/gitlab.po.time_stamp | 0 4 files changed, 1148 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/34544-add-italian-translation-of-cycle-analytics-page-&-project-page-&-repository-page.yml create mode 100644 locale/it/gitlab.po create mode 100644 locale/it/gitlab.po.time_stamp diff --git a/changelogs/unreleased/34544-add-italian-translation-of-cycle-analytics-page-&-project-page-&-repository-page.yml b/changelogs/unreleased/34544-add-italian-translation-of-cycle-analytics-page-&-project-page-&-repository-page.yml new file mode 100644 index 00000000000..31f4262c9f9 --- /dev/null +++ b/changelogs/unreleased/34544-add-italian-translation-of-cycle-analytics-page-&-project-page-&-repository-page.yml @@ -0,0 +1,4 @@ +--- +title: Add Italian translation of Cycle Analytics Page & Project Page & Repository Page +merge_request: 12578 +author: Huang Tao diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index db7cdf4b5c7..f3d489aad0d 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -12,7 +12,8 @@ module Gitlab 'zh_HK' => '繁體中文(香港)', 'zh_TW' => '繁體中文(臺灣)', 'bg' => 'български', - 'eo' => 'Esperanto' + 'eo' => 'Esperanto', + 'it' => 'Italiano' }.freeze def available_locales diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po new file mode 100644 index 00000000000..19848b767de --- /dev/null +++ b/locale/it/gitlab.po @@ -0,0 +1,1142 @@ +# Paolo Falomo , 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-29 09:40-0400\n" +"Last-Translator: Paolo Falomo \n" +"Language-Team: Italian (https://translate.zanata.org/project/view/GitLab)\n" +"Language: it\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "%{commit_author_link} ha committato %{commit_timeago}" + +msgid "About auto deploy" +msgstr "Riguardo il rilascio automatico" + +msgid "Active" +msgstr "Attivo" + +msgid "Activity" +msgstr "Attività" + +msgid "Add Changelog" +msgstr "Aggiungi Changelog" + +msgid "Add Contribution guide" +msgstr "Aggiungi Guida per contribuire" + +msgid "Add License" +msgstr "Aggiungi Licenza" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "" +"Aggiungi una chiave SSH al tuo profilo per eseguire pull o push tramite SSH" + +msgid "Add new directory" +msgstr "Aggiungi una directory (cartella)" + +msgid "Archived project! Repository is read-only" +msgstr "Progetto archiviato! La Repository è sola-lettura" + +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "Sei sicuro di voler cancellare questa pipeline programmata?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "" +"Aggiungi un file tramite trascina & rilascia ( drag & drop) o " +"%{upload_link}" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "Branch" +msgstr[1] "Branches" + +msgid "" +"Branch %{branch_name} was created. To set up auto deploy, " +"choose a GitLab CI Yaml template and commit your changes. " +"%{link_to_autodeploy_doc}" +msgstr "" +"La branch %{branch_name} è stata creata. Per impostare un " +"rilascio automatico scegli un template CI di Gitlab e committa le tue " +"modifiche %{link_to_autodeploy_doc}" + +msgid "Branches" +msgstr "Branches" + +msgid "Browse files" +msgstr "Guarda i files" + +msgid "ByAuthor|by" +msgstr "PerAutore|per" + +msgid "CI configuration" +msgstr "Configurazione CI (Integrazione Continua)" + +msgid "Cancel" +msgstr "Cancella" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "CambiaEtichettaDelTipoDiAzione|Preleva nella branch" + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "CambiaEtichettaDelTipoDiAzione|Ripristina nella branch" + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "CambiaTipoDiAzione|Cherry-pick" + +msgid "ChangeTypeAction|Revert" +msgstr "CambiaTipoDiAzione|Ripristina" + +msgid "Changelog" +msgstr "Changelog" + +msgid "Charts" +msgstr "Grafici" + +msgid "Cherry-pick this commit" +msgstr "Cherry-pick this commit" + +msgid "Cherry-pick this merge request" +msgstr "Cherry-pick questa richiesta di merge" + +msgid "CiStatusLabel|canceled" +msgstr "CiStatusLabel|cancellato" + +msgid "CiStatusLabel|created" +msgstr "CiStatusLabel|creato" + +msgid "CiStatusLabel|failed" +msgstr "CiStatusLabel|fallito" + +msgid "CiStatusLabel|manual action" +msgstr "CiStatusLabel|azione manuale" + +msgid "CiStatusLabel|passed" +msgstr "CiStatusLabel|superata" + +msgid "CiStatusLabel|passed with warnings" +msgstr "CiStatusLabel|superata con avvisi" + +msgid "CiStatusLabel|pending" +msgstr "CiStatusLabel|in coda" + +msgid "CiStatusLabel|skipped" +msgstr "CiStatusLabel|saltata" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "CiStatusLabel|in attesa di azione manuale" + +msgid "CiStatusText|blocked" +msgstr "CiStatusText|bloccata" + +msgid "CiStatusText|canceled" +msgstr "CiStatusText|cancellata" + +msgid "CiStatusText|created" +msgstr "CiStatusText|creata" + +msgid "CiStatusText|failed" +msgstr "CiStatusText|fallita" + +msgid "CiStatusText|manual" +msgstr "CiStatusText|manuale" + +msgid "CiStatusText|passed" +msgstr "CiStatusText|superata" + +msgid "CiStatusText|pending" +msgstr "CiStatusText|in coda" + +msgid "CiStatusText|skipped" +msgstr "CiStatusText|saltata" + +msgid "CiStatus|running" +msgstr "CiStatus|in corso" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "Commit" +msgstr[1] "Commits" + +msgid "Commit message" +msgstr "Messaggio del commit" + +msgid "CommitBoxTitle|Commit" +msgstr "CommitBoxTitle|Commit" + +msgid "CommitMessage|Add %{file_name}" +msgstr "CommitMessage|Aggiungi %{file_name}" + +msgid "Commits" +msgstr "Commits" + +msgid "Commits|History" +msgstr "Commits|Cronologia" + +msgid "Committed by" +msgstr "Committato da " + +msgid "Compare" +msgstr "Confronta" + +msgid "Contribution guide" +msgstr "Guida per contribuire" + +msgid "Contributors" +msgstr "Collaboratori" + +msgid "Copy URL to clipboard" +msgstr "Copia URL negli appunti" + +msgid "Copy commit SHA to clipboard" +msgstr "Copia l'SHA del commit negli appunti" + +msgid "Create New Directory" +msgstr "Crea una nuova cartella" + +msgid "Create directory" +msgstr "Crea cartella" + +msgid "Create empty bare repository" +msgstr "Crea una repository vuota" + +msgid "Create merge request" +msgstr "Crea una richiesta di merge" + +msgid "Create new..." +msgstr "Crea nuovo..." + +msgid "CreateNewFork|Fork" +msgstr "CreateNewFork|Fork" + +msgid "CreateTag|Tag" +msgstr "CreateTag|Tag" + +msgid "Cron Timezone" +msgstr "Cron Timezone" + +msgid "Cron syntax" +msgstr "Sintassi Cron" + +msgid "Custom notification events" +msgstr "Eventi-Notifica personalizzati" + +msgid "" +"Custom notification levels are the same as participating levels. With custom " +"notification levels you will also receive notifications for select events. " +"To find out more, check out %{notification_link}." +msgstr "" +"I livelli di notifica personalizzati sono uguali a quelli di partecipazione. " +"Con i livelli di notifica personalizzati riceverai anche notifiche per gli " +"eventi da te scelti %{notification_link}." + +msgid "Cycle Analytics" +msgstr "Statistiche Cicliche" + +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." +msgstr "" +"L'Analisi Ciclica fornisce una panoramica sul tempo che trascorre tra l'idea " +"ed il rilascio in produzione del tuo progetto" + +msgid "CycleAnalyticsStage|Code" +msgstr "StadioAnalisiCiclica|Codice" + +msgid "CycleAnalyticsStage|Issue" +msgstr "StadioAnalisiCiclica|Issue" + +msgid "CycleAnalyticsStage|Plan" +msgstr "StadioAnalisiCiclica|Pianificazione" + +msgid "CycleAnalyticsStage|Production" +msgstr "StadioAnalisiCiclica|Produzione" + +msgid "CycleAnalyticsStage|Review" +msgstr "StadioAnalisiCiclica|Revisione" + +msgid "CycleAnalyticsStage|Staging" +msgstr "StadioAnalisiCiclica|Pre-rilascio" + +msgid "CycleAnalyticsStage|Test" +msgstr "StadioAnalisiCiclica|Test" + +msgid "Define a custom pattern with cron syntax" +msgstr "Definisci un patter personalizzato mediante la sintassi cron" + +msgid "Delete" +msgstr "Elimina" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "Rilascio" +msgstr[1] "Rilasci" + +msgid "Description" +msgstr "Descrizione" + +msgid "Directory name" +msgstr "Nome cartella" + +msgid "Don't show again" +msgstr "Non mostrare più" + +msgid "Download" +msgstr "Scarica" + +msgid "Download tar" +msgstr "Scarica tar" + +msgid "Download tar.bz2" +msgstr "Scarica tar.bz2" + +msgid "Download tar.gz" +msgstr "Scarica tar.gz" + +msgid "Download zip" +msgstr "Scarica zip" + +msgid "DownloadArtifacts|Download" +msgstr "DownloadArtifacts|Scarica" + +msgid "DownloadCommit|Email Patches" +msgstr "DownloadCommit|Email Patches" + +msgid "DownloadCommit|Plain Diff" +msgstr "DownloadCommit|Differenze" + +msgid "DownloadSource|Download" +msgstr "DownloadSource|Scarica" + +msgid "Edit" +msgstr "Modifica" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "Cambia programmazione della pipeline %{id}" + +msgid "Every day (at 4:00am)" +msgstr "Ogni giorno (alle 4 del mattino)" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "Ogni primo giorno del mese (alle 4 del mattino)" + +msgid "Every week (Sundays at 4:00am)" +msgstr "Ogni settimana (Di domenica alle 4 del mattino)" + +msgid "Failed to change the owner" +msgstr "Impossibile cambiare owner" + +msgid "Failed to remove the pipeline schedule" +msgstr "Impossibile rimuovere la pipeline pianificata" + +msgid "Files" +msgstr "Files" + +msgid "Find by path" +msgstr "Trova in percorso" + +msgid "Find file" +msgstr "Trova file" + +msgid "FirstPushedBy|First" +msgstr "PrimoPushDel|Primo" + +msgid "FirstPushedBy|pushed by" +msgstr "PrimoPushDel|Push di" + +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "Fork" +msgstr[1] "Forks" + +msgid "ForkedFromProjectPath|Forked from" +msgstr "ForkedFromProjectPath|Fork da" + +msgid "From issue creation until deploy to production" +msgstr "Dalla creazione di un issue fino al rilascio in produzione" + +msgid "From merge request merge until deploy to production" +msgstr "" +"Dalla richiesta di merge fino effettua il merge fino al rilascio in " +"produzione" + +msgid "Go to your fork" +msgstr "Vai il tuo fork" + +msgid "GoToYourFork|Fork" +msgstr "GoToYourFork|Fork" + +msgid "Home" +msgstr "Home" + +msgid "Housekeeping successfully started" +msgstr "Housekeeping iniziato con successo" + +msgid "Import repository" +msgstr "Importa repository" + +msgid "Interval Pattern" +msgstr "Intervallo di Pattern" + +msgid "Introducing Cycle Analytics" +msgstr "Introduzione delle Analisi Cicliche" + +msgid "LFSStatus|Disabled" +msgstr "LFSStatus|Disabilitato" + +msgid "LFSStatus|Enabled" +msgstr "LFSStatus|Abilitato" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "L'ultimo %d giorno" +msgstr[1] "Gli ultimi %d giorni" + +msgid "Last Pipeline" +msgstr "Ultima Pipeline" + +msgid "Last Update" +msgstr "Ultimo Aggiornamento" + +msgid "Last commit" +msgstr "Ultimo Commit" + +msgid "Learn more in the" +msgstr "Leggi di più su" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "Leggi di più su|documentazione sulla pianificazione delle pipelines" + +msgid "Leave group" +msgstr "Abbandona il gruppo" + +msgid "Leave project" +msgstr "Abbandona il progetto" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "Limita visualizzazione %d d'evento" +msgstr[1] "Limita visualizzazione %d di eventi" + +msgid "Median" +msgstr "Mediano" + +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "MissingSSHKeyWarningLink|aggiungi una chiave SSH" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "Nuovo Issue" +msgstr[1] "Nuovi Issues" + +msgid "New Pipeline Schedule" +msgstr "Nuova pianificazione Pipeline" + +msgid "New branch" +msgstr "Nuova Branch" + +msgid "New directory" +msgstr "Nuova directory" + +msgid "New file" +msgstr "Nuovo file" + +msgid "New issue" +msgstr "Nuovo Issue" + +msgid "New merge request" +msgstr "Nuova richiesta di merge" + +msgid "New schedule" +msgstr "Nuova pianficazione" + +msgid "New snippet" +msgstr "Nuovo snippet" + +msgid "New tag" +msgstr "Nuovo tag" + +msgid "No repository" +msgstr "Nessuna Repository" + +msgid "No schedules" +msgstr "Nessuna pianificazione" + +msgid "Not available" +msgstr "Non disponibile" + +msgid "Not enough data" +msgstr "Dati insufficienti " + +msgid "Notification events" +msgstr "Notifica eventi" + +msgid "NotificationEvent|Close issue" +msgstr "NotificationEvent|Chiudi issue" + +msgid "NotificationEvent|Close merge request" +msgstr "NotificationEvent|Chiudi richiesta di merge" + +msgid "NotificationEvent|Failed pipeline" +msgstr "NotificationEvent|Pipeline fallita" + +msgid "NotificationEvent|Merge merge request" +msgstr "NotificationEvent|Completa la richiesta di merge" + +msgid "NotificationEvent|New issue" +msgstr "NotificationEvent|Nuovo issue" + +msgid "NotificationEvent|New merge request" +msgstr "NotificationEvent|Nuova richiesta di merge" + +msgid "NotificationEvent|New note" +msgstr "NotificationEvent|Nuova nota" + +msgid "NotificationEvent|Reassign issue" +msgstr "NotificationEvent|Riassegna issue" + +msgid "NotificationEvent|Reassign merge request" +msgstr "NotificationEvent|Riassegna richiesta di Merge" + +msgid "NotificationEvent|Reopen issue" +msgstr "NotificationEvent|Riapri issue" + +msgid "NotificationEvent|Successful pipeline" +msgstr "NotificationEvent|Pipeline Completata" + +msgid "NotificationLevel|Custom" +msgstr "NotificationLevel|Personalizzato" + +msgid "NotificationLevel|Disabled" +msgstr "NotificationLevel|Disabilitato" + +msgid "NotificationLevel|Global" +msgstr "NotificationLevel|Globale" + +msgid "NotificationLevel|On mention" +msgstr "NotificationLevel|Se menzionato" + +msgid "NotificationLevel|Participate" +msgstr "NotificationLevel|Partecipa" + +msgid "NotificationLevel|Watch" +msgstr "NotificationLevel|Osserva" + +msgid "OfSearchInADropdown|Filter" +msgstr "OfSearchInADropdown|Filtra" + +msgid "OpenedNDaysAgo|Opened" +msgstr "ApertoNGiorniFa|Aperto" + +msgid "Options" +msgstr "Opzioni" + +msgid "Owner" +msgstr "Owner" + +msgid "Pipeline" +msgstr "Pipeline" + +msgid "Pipeline Health" +msgstr "Stato della Pipeline" + +msgid "Pipeline Schedule" +msgstr "Pianificazione Pipeline" + +msgid "Pipeline Schedules" +msgstr "Pianificazione multipla Pipeline" + +msgid "PipelineSchedules|Activated" +msgstr "PipelineSchedules|Attivata" + +msgid "PipelineSchedules|Active" +msgstr "PipelineSchedules|Attiva" + +msgid "PipelineSchedules|All" +msgstr "PipelineSchedules|Tutto" + +msgid "PipelineSchedules|Inactive" +msgstr "PipelineSchedules|Inattiva" + +msgid "PipelineSchedules|Next Run" +msgstr "PipelineSchedules|Prossima esecuzione" + +msgid "PipelineSchedules|None" +msgstr "PipelineSchedules|Nessuna" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "PipelineSchedules|Fornisci una breve descrizione per questa pipeline" + +msgid "PipelineSchedules|Take ownership" +msgstr "PipelineSchedules|Prendi possesso" + +msgid "PipelineSchedules|Target" +msgstr "PipelineSchedules|Target" + +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "PipelineSheduleIntervalPattern|Personalizzato" + +msgid "Pipeline|with stage" +msgstr "Pipeline|con stadio" + +msgid "Pipeline|with stages" +msgstr "Pipeline|con più stadi" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "Il Progetto '%{project_name}' in coda di eliminazione." + +msgid "Project '%{project_name}' was successfully created." +msgstr "Il Progetto '%{project_name}' è stato creato con successo." + +msgid "Project '%{project_name}' was successfully updated." +msgstr "Il Progetto '%{project_name}' è stato aggiornato con successo." + +msgid "Project '%{project_name}' will be deleted." +msgstr "Il Progetto '%{project_name}' verrà eliminato" + +msgid "Project access must be granted explicitly to each user." +msgstr "L'accesso al progetto dev'esser fornito esplicitamente ad ogni utente" + +msgid "Project export could not be deleted." +msgstr "L'esportazione del progetto non può essere eliminata." + +msgid "Project export has been deleted." +msgstr "L'esportazione del progetto è stata eliminata." + +msgid "" +"Project export link has expired. Please generate a new export from your " +"project settings." +msgstr "" +"Il link d'esportazione del progetto è scaduto. Genera una nuova esportazione " +"dalle impostazioni del tuo progetto." + +msgid "Project export started. A download link will be sent by email." +msgstr "" +"Esportazione del progetto iniziata. Un link di download sarà inviato via " +"email." + +msgid "Project home" +msgstr "Home di progetto" + +msgid "ProjectFeature|Disabled" +msgstr "ProjectFeature|Disabilitato" + +msgid "ProjectFeature|Everyone with access" +msgstr "ProjectFeature|Chiunque con accesso" + +msgid "ProjectFeature|Only team members" +msgstr "ProjectFeature|Solo i membri del team" + +msgid "ProjectFileTree|Name" +msgstr "ProjectFileTree|Nome" + +msgid "ProjectLastActivity|Never" +msgstr "ProjectLastActivity|Mai" + +msgid "ProjectLifecycle|Stage" +msgstr "ProgettoCicloVitale|Stadio" + +msgid "ProjectNetworkGraph|Graph" +msgstr "ProjectNetworkGraph|Grafico" + +msgid "Read more" +msgstr "Continua..." + +msgid "Readme" +msgstr "Leggimi" + +msgid "RefSwitcher|Branches" +msgstr "RefSwitcher|Branches" + +msgid "RefSwitcher|Tags" +msgstr "RefSwitcher|Tags" + +msgid "Related Commits" +msgstr "Commit correlati" + +msgid "Related Deployed Jobs" +msgstr "Attività di Rilascio Correlate" + +msgid "Related Issues" +msgstr "Issues Correlati" + +msgid "Related Jobs" +msgstr "Attività Correlate" + +msgid "Related Merge Requests" +msgstr "Richieste di Merge Correlate" + +msgid "Related Merged Requests" +msgstr "Richieste di Merge Completate Correlate" + +msgid "Remind later" +msgstr "Ricordamelo più tardi" + +msgid "Remove project" +msgstr "Rimuovi progetto" + +msgid "Request Access" +msgstr "Richiedi accesso" + +msgid "Revert this commit" +msgstr "Ripristina questo commit" + +msgid "Revert this merge request" +msgstr "Ripristina questa richiesta di merge" + +msgid "Save pipeline schedule" +msgstr "Salva pianificazione pipeline" + +msgid "Schedule a new pipeline" +msgstr "Pianifica una nuova Pipeline" + +msgid "Scheduling Pipelines" +msgstr "Pianificazione pipelines" + +msgid "Search branches and tags" +msgstr "Ricerca branches e tags" + +msgid "Select Archive Format" +msgstr "Seleziona formato d'archivio" + +msgid "Select a timezone" +msgstr "Seleziona una timezone" + +msgid "Select target branch" +msgstr "Seleziona una branch di destinazione" + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "" +"Imposta una password sul tuo account per eseguire pull o push tramite " +"%{protocol}" + +msgid "Set up CI" +msgstr "Configura CI" + +msgid "Set up Koding" +msgstr "Configura Koding" + +msgid "Set up auto deploy" +msgstr "Configura il rilascio automatico" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "SetPasswordToCloneLink|imposta una password" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "Visualizza %d evento" +msgstr[1] "Visualizza %d eventi" + +msgid "Source code" +msgstr "Codice Sorgente" + +msgid "StarProject|Star" +msgstr "StarProject|Star" + +msgid "Start a %{new_merge_request} with these changes" +msgstr "inizia una %{new_merge_request} con queste modifiche" + +msgid "Switch branch/tag" +msgstr "Cambia branch/tag" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "Tag" +msgstr[1] "Tags" + +msgid "Tags" +msgstr "Tags" + +msgid "Target Branch" +msgstr "Branch di destinazione" + +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." +msgstr "" +"Lo stadio di programmazione mostra il tempo trascorso dal primo commit alla " +"creazione di una richiesta di merge (MR). I dati saranno aggiunti una volta " +"che avrai creato la prima richiesta di merge." + +msgid "The collection of events added to the data gathered for that stage." +msgstr "L'insieme di eventi aggiunti ai dati raccolti per quello stadio." + +msgid "The fork relationship has been removed." +msgstr "La relazione del fork è stata rimossa" + +msgid "" +"The issue stage shows the time it takes from creating an issue to assigning " +"the issue to a milestone, or add the issue to a list on your Issue Board. " +"Begin creating issues to see data for this stage." +msgstr "" +"Questo stadio di issue mostra il tempo che ci vuole dal creare un issue " +"all'assegnarli una milestone, o ad aggiungere un issue alla tua board. Crea " +"un issue per vedere questo stadio." + +msgid "The phase of the development lifecycle." +msgstr "Il ciclo vitale della fase di sviluppo." + +msgid "" +"The pipelines schedule runs pipelines in the future, repeatedly, for " +"specific branches or tags. Those scheduled pipelines will inherit limited " +"project access based on their associated user." +msgstr "" +"Le pipelines pianificate vengono eseguite nel futuro, ripetitivamente, per " +"specifici tag o branch ed ereditano restrizioni di progetto basate " +"sull'utente ad esse associato." + +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "" +"Lo stadio di pianificazione mostra il tempo trascorso dal primo commit al " +"suo step precedente. Questo periodo sarà disponibile automaticamente nel " +"momento in cui farai il primo commit." + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." +msgstr "" +"Lo stadio di produzione mostra il tempo totale che trascorre tra la " +"creazione di un issue il suo rilascio (inteso come codice) in produzione. " +"Questo dato sarà disponibile automaticamente nel momento in cui avrai " +"completato l'intero processo ideale del ciclo di produzione" + +msgid "The project can be accessed by any logged in user." +msgstr "Qualunque utente autenticato può accedere a questo progetto." + +msgid "The project can be accessed without any authentication." +msgstr "" +"Chiunque può accedere a questo progetto (senza alcuna autenticazione)." + +msgid "The repository for this project does not exist." +msgstr "La repository di questo progetto non esiste." + +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." +msgstr "" +"Lo stadio di revisione mostra il tempo tra una richiesta di merge al suo " +"svolgimento effettivo. Questo dato sarà disponibile appena avrai completato " +"una MR (Merger Request)" + +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." +msgstr "" +"Lo stadio di pre-rilascio mostra il tempo che trascorre da una MR (Richiesta " +"di Merge) completata al suo rilascio in ambiente di produzione. Questa " +"informazione sarà disponibile dal tuo primo rilascio in produzione" + +msgid "" +"The testing stage shows the time GitLab CI takes to run every pipeline for " +"the related merge request. The data will automatically be added after your " +"first pipeline finishes running." +msgstr "" +"Lo stadio di test mostra il tempo che ogni Pipeline impiega per essere " +"eseguita in ogni Richiesta di Merge correlata. L'informazione sarà " +"disponibile automaticamente quando la tua prima Pipeline avrà finito d'esser " +"eseguita." + +msgid "The time taken by each data entry gathered by that stage." +msgstr "" +"Il tempo aggregato relativo eventi/data entry raccolto in quello stadio." + +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." +msgstr "" +"Il valore falsato nel mezzo di una serie di dati osservati. ES: tra 3,5,9 il " +"mediano è 5. Tra 3,5,7,8 il mediano è (5+7)/2 quindi 6." + +msgid "" +"This means you can not push code until you create an empty repository or " +"import existing one." +msgstr "" +"Questo significa che non è possibile effettuare push di codice fino a che " +"non crei una repository vuota o ne importi una esistente" + +msgid "Time before an issue gets scheduled" +msgstr "Il tempo che impiega un issue per esser pianificato" + +msgid "Time before an issue starts implementation" +msgstr "Il tempo che impiega un issue per esser implementato" + +msgid "Time between merge request creation and merge/close" +msgstr "Il tempo tra la creazione di una richiesta di merge ed il merge/close" + +msgid "Time until first merge request" +msgstr "Il tempo fino alla prima richiesta di merge" + +msgid "Timeago|%s days ago" +msgstr "Timeago|%s giorni fa" + +msgid "Timeago|%s days remaining" +msgstr "Timeago|%s giorni rimanenti" + +msgid "Timeago|%s hours remaining" +msgstr "Timeago|%s ore rimanenti" + +msgid "Timeago|%s minutes ago" +msgstr "Timeago|%s minuti fa" + +msgid "Timeago|%s minutes remaining" +msgstr "Timeago|%s minuti rimanenti" + +msgid "Timeago|%s months ago" +msgstr "Timeago|%s minuti fa" + +msgid "Timeago|%s months remaining" +msgstr "Timeago|%s mesi rimanenti" + +msgid "Timeago|%s seconds remaining" +msgstr "Timeago|%s secondi rimanenti" + +msgid "Timeago|%s weeks ago" +msgstr "Timeago|%s settimane fa" + +msgid "Timeago|%s weeks remaining" +msgstr "Timeago|%s settimane rimanenti" + +msgid "Timeago|%s years ago" +msgstr "Timeago|%s anni fa" + +msgid "Timeago|%s years remaining" +msgstr "Timeago|%s anni rimanenti" + +msgid "Timeago|1 day remaining" +msgstr "Timeago|1 giorno rimanente" + +msgid "Timeago|1 hour remaining" +msgstr "Timeago|1 ora rimanente" + +msgid "Timeago|1 minute remaining" +msgstr "Timeago|1 minuto rimanente" + +msgid "Timeago|1 month remaining" +msgstr "Timeago|1 mese rimanente" + +msgid "Timeago|1 week remaining" +msgstr "Timeago|1 settimana rimanente" + +msgid "Timeago|1 year remaining" +msgstr "Timeago|1 anno rimanente" + +msgid "Timeago|Past due" +msgstr "Timeago|Entro" + +msgid "Timeago|a day ago" +msgstr "Timeago|un giorno fa" + +msgid "Timeago|a month ago" +msgstr "Timeago|un mese fa" + +msgid "Timeago|a week ago" +msgstr "Timeago|una settimana fa" + +msgid "Timeago|a while" +msgstr "Timeago|poco fa" + +msgid "Timeago|a year ago" +msgstr "Timeago|un anno fa" + +msgid "Timeago|about %s hours ago" +msgstr "Timeago|circa %s ore fa" + +msgid "Timeago|about a minute ago" +msgstr "Timeago|circa un minuto fa" + +msgid "Timeago|about an hour ago" +msgstr "Timeago|circa un ora fa" + +msgid "Timeago|in %s days" +msgstr "Timeago|in %s giorni" + +msgid "Timeago|in %s hours" +msgstr "Timeago|in %s ore" + +msgid "Timeago|in %s minutes" +msgstr "Timeago|in %s minuti" + +msgid "Timeago|in %s months" +msgstr "Timeago|in %s mesi" + +msgid "Timeago|in %s seconds" +msgstr "Timeago|in %s secondi" + +msgid "Timeago|in %s weeks" +msgstr "Timeago|in %s settimane" + +msgid "Timeago|in %s years" +msgstr "Timeago|in %s anni" + +msgid "Timeago|in 1 day" +msgstr "Timeago|in 1 giorno" + +msgid "Timeago|in 1 hour" +msgstr "Timeago|in 1 ora" + +msgid "Timeago|in 1 minute" +msgstr "Timeago|in 1 minuto" + +msgid "Timeago|in 1 month" +msgstr "Timeago|in 1 mese" + +msgid "Timeago|in 1 week" +msgstr "Timeago|in 1 settimana" + +msgid "Timeago|in 1 year" +msgstr "Timeago|in 1 anno" + +msgid "Timeago|less than a minute ago" +msgstr "Timeago|meno di un minuto fa" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "Tempo|hr" +msgstr[1] "Tempo|hr" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "Tempo|min" +msgstr[1] "Tempo|mins" + +msgid "Time|s" +msgstr "Tempo|s" + +msgid "Total Time" +msgstr "Tempo Totale" + +msgid "Total test time for all commits/merges" +msgstr "Tempo totale di test per tutti i commits/merges" + +msgid "Unstar" +msgstr "Unstar" + +msgid "Upload New File" +msgstr "Carica un nuovo file" + +msgid "Upload file" +msgstr "Carica file" + +msgid "Use your global notification setting" +msgstr "Usa le tue impostazioni globali " + +msgid "VisibilityLevel|Internal" +msgstr "VisibilityLevel|Interno" + +msgid "VisibilityLevel|Private" +msgstr "VisibilityLevel|Privato" + +msgid "VisibilityLevel|Public" +msgstr "VisibilityLevel|Pubblico" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "" +"Vuoi visualizzare i dati? Richiedi l'accesso ad un amministratore, grazie." + +msgid "We don't have enough data to show this stage." +msgstr "Non ci sono sufficienti dati da mostrare su questo stadio" + +msgid "Withdraw Access Request" +msgstr "Ritira richiesta d'accesso" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" +"Stai per rimuovere %{project_name_with_namespace}.\n" +"I progetti rimossi NON POSSONO essere ripristinati\n" +"Sei assolutamente sicuro?" + +msgid "" +"You are going to remove the fork relationship to source project " +"%{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "" +"Stai per rimuovere la relazione con il progetto sorgente " +"%{forked_from_project}. Sei ASSOLUTAMENTE sicuro?" + +msgid "" +"You are going to transfer %{project_name_with_namespace} to another owner. " +"Are you ABSOLUTELY sure?" +msgstr "" +"Stai per trasferire %{project_name_with_namespace} ad un altro owner. Sei " +"ASSOLUTAMENTE sicuro?" + +msgid "You can only add files when you are on a branch" +msgstr "Puoi aggiungere files solo quando sei in una branch" + +msgid "You have reached your project limit" +msgstr "Hai raggiunto il tuo limite di progetto" + +msgid "You must sign in to star a project" +msgstr "Devi accedere per porre una star al progetto" + +msgid "You need permission." +msgstr "Necessiti del permesso." + +msgid "You will not get any notifications via email" +msgstr "Non riceverai alcuna notifica via email" + +msgid "You will only receive notifications for the events you choose" +msgstr "Riceverai notifiche solo per gli eventi che hai scelto" + +msgid "" +"You will only receive notifications for threads you have participated in" +msgstr "Riceverai notifiche solo per i threads a cui hai partecipato" + +msgid "You will receive notifications for any activity" +msgstr "Riceverai notifiche per ogni attività" + +msgid "" +"You will receive notifications only for comments in which you were " +"@mentioned" +msgstr "Riceverai notifiche solo per i commenti ai quale sei stato menzionato" + +msgid "" +"You won't be able to pull or push project code via %{protocol} until you " +"%{set_password_link} on your account" +msgstr "" +"Non sarai in grado di eseguire pull o push di codice tramite %{protocol} " +"fino a che %{set_password_link} nel tuo account." + +msgid "" +"You won't be able to pull or push project code via SSH until you " +"%{add_ssh_key_link} to your profile" +msgstr "" +"Non sarai in grado di effettuare push o pull tramite SSH fino a che " +"%{add_ssh_key_link} al tuo profilo" + +msgid "Your name" +msgstr "Il tuo nome" + +msgid "day" +msgid_plural "days" +msgstr[0] "giorno" +msgstr[1] "giorni" + +msgid "new merge request" +msgstr "Nuova richiesta di merge" + +msgid "notification emails" +msgstr "Notifiche via email" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "parent" +msgstr[1] "parents" + diff --git a/locale/it/gitlab.po.time_stamp b/locale/it/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d -- cgit v1.2.1 From 17ba052f5c9d7c390b350469d15ffc674a943b07 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 30 Jun 2017 15:23:46 +0800 Subject: Update wordings, allow only full path, add tests --- app/models/ci/pipeline.rb | 9 ++++---- app/models/project.rb | 14 +++++------- .../projects/pipelines_settings/_show.html.haml | 6 ++--- doc/api/projects.md | 6 ++--- doc/user/project/pipelines/settings.md | 2 +- spec/models/ci/pipeline_spec.rb | 26 ++++++---------------- spec/models/project_spec.rb | 24 ++++++++++++++++++++ 7 files changed, 49 insertions(+), 38 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 57bf5a8a4c5..12986e5781e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -343,10 +343,11 @@ module Ci 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') + if project.ci_config_file.blank? + '.gitlab-ci.yml' + else + project.ci_config_file + end end def environments diff --git a/app/models/project.rb b/app/models/project.rb index 507dffde18b..5374aca7701 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -187,7 +187,7 @@ class Project < ActiveRecord::Base validates :creator, presence: true, on: :create validates :description, length: { maximum: 2000 }, allow_blank: true validates :ci_config_file, - format: { without: /\.{2}/.freeze, + format: { without: /\.{2}/, message: 'cannot include directory traversal.' }, length: { maximum: 255 }, allow_blank: true @@ -222,7 +222,6 @@ 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 has_many :uploads, as: :model, dependent: :destroy @@ -527,6 +526,11 @@ class Project < ActiveRecord::Base import_data&.destroy end + def ci_config_file=(value) + # Strip all leading slashes so that //foo -> foo + super(value&.sub(%r{\A/+}, '')) + end + def import_url=(value) return super(value) unless Gitlab::UrlSanitizer.valid?(value) @@ -1484,10 +1488,4 @@ class Project < ActiveRecord::Base raise ex 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 2c2f0341e2a..4b3efd12e08 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -47,11 +47,11 @@ %hr .form-group - = f.label :ci_config_file, 'Custom CI Config File', class: 'label-light' + = 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. + The path to CI config file. Default to .gitlab-ci.yml + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-file'), target: '_blank' %hr .form-group diff --git a/doc/api/projects.md b/doc/api/projects.md index ea349ae8f68..7565f18e907 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -346,7 +346,7 @@ Parameters: | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | -| `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) | +| `ci_config_file` | boolean | no | The path to CI config file | ### Create project for user @@ -383,7 +383,7 @@ Parameters: | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | -| `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) | +| `ci_config_file` | boolean | no | The path to CI config file | ### Edit project @@ -418,7 +418,7 @@ Parameters: | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | -| `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) | +| `ci_config_file` | boolean | no | The path to CI config file | ### Fork project diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 702b3453a0e..7274fe816cc 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -27,7 +27,7 @@ The default value is 60 minutes. Decrease the time limit if you want to impose a hard limit on your jobs' running time or increase it otherwise. In any case, if the job surpasses the threshold, it is marked as failed. -## Custom CI Config File +## Custom CI config file > - [Introduced][ce-12509] in GitLab 9.4. diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 5ed02031708..8d4d87def5e 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -748,47 +748,35 @@ describe Ci::Pipeline, models: true do end end - describe 'yaml config file resolution' do - let(:project) { FactoryGirl.build(:project) } + describe '#ci_yaml_file_path' do + let(:project) { create(:empty_project) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } - it 'uses custom ci config file path when present' do + it 'returns the path from project' do allow(project).to receive(:ci_config_file) { 'custom/path' } - expect(pipeline.ci_yaml_file_path).to eq('custom/path/.gitlab-ci.yml') + expect(pipeline.ci_yaml_file_path).to eq('custom/path') end - it 'uses root when custom path is nil' do + it 'returns default 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 + it 'returns default 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 allow(project).to receive(:ci_config_file) { 'custom' } pipeline.ci_yaml_file expect(pipeline.yaml_errors) - .to eq('Failed to load CI/CD config file at custom/.gitlab-ci.yml') + .to eq('Failed to load CI/CD config file at custom') end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index fb39357659c..349f9c3d7eb 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -144,6 +144,8 @@ describe Project, models: true do it { is_expected.to validate_length_of(:description).is_at_most(2000) } it { is_expected.to validate_length_of(:ci_config_file).is_at_most(255) } + it { is_expected.to allow_value('').for(:ci_config_file) } + it { is_expected.not_to allow_value('test/../foo').for(:ci_config_file) } it { is_expected.to validate_presence_of(:creator) } @@ -1491,6 +1493,28 @@ describe Project, models: true do end end + describe '#ci_config_file=' do + let(:project) { create(:empty_project) } + + it 'sets nil' do + project.update!(ci_config_file: nil) + + expect(project.ci_config_file).to be_nil + end + + it 'sets a string' do + project.update!(ci_config_file: 'foo/.gitlab_ci.yml') + + expect(project.ci_config_file).to eq('foo/.gitlab_ci.yml') + end + + it 'sets a string but remove all leading slashes' do + project.update!(ci_config_file: '///foo//.gitlab_ci.yml') + + expect(project.ci_config_file).to eq('foo//.gitlab_ci.yml') + end + end + describe 'Project import job' do let(:project) { create(:empty_project, import_url: generate(:url)) } -- cgit v1.2.1 From afbc7520c296196d0f3f95d4a24a9e42c0e41f3c Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 30 Jun 2017 07:32:25 +0000 Subject: `AccessTokenValidationService` accepts `String` or `API::Scope` scopes. - There's no need to use `API::Scope` for scopes that don't have `if` conditions, such as in `lib/gitlab/auth.rb`. --- app/services/access_token_validation_service.rb | 9 ++++++++- lib/api/scope.rb | 2 +- lib/gitlab/auth.rb | 1 - spec/services/access_token_validation_service_spec.rb | 12 ++++++------ 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index bf5aef0055e..9c00ea789ec 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -37,7 +37,14 @@ class AccessTokenValidationService # small number of records involved. # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12300/#note_33689006 token_scopes = token.scopes.map(&:to_sym) - required_scopes.any? { |scope| scope.sufficient?(token_scopes, request) } + + required_scopes.any? do |scope| + if scope.respond_to?(:sufficient?) + scope.sufficient?(token_scopes, request) + else + API::Scope.new(scope).sufficient?(token_scopes, request) + end + end end end end diff --git a/lib/api/scope.rb b/lib/api/scope.rb index c23846d1e7d..d5165b2e482 100644 --- a/lib/api/scope.rb +++ b/lib/api/scope.rb @@ -11,7 +11,7 @@ module API # Are the `scopes` passed in sufficient to adequately authorize the passed # request for the scope represented by the current instance of this class? def sufficient?(scopes, request) - verify_if_condition(request) && scopes.include?(self.name) + scopes.include?(self.name) && verify_if_condition(request) end private diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 6d0d638ba14..ccb5d886bab 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -140,7 +140,6 @@ module Gitlab end def valid_scoped_token?(token, scopes) - scopes = scopes.map { |scope| API::Scope.new(scope) } AccessTokenValidationService.new(token).include_any_scope?(scopes) end diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index 660a05e0b6d..11225fad18a 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -6,28 +6,28 @@ describe AccessTokenValidationService, services: true do it "returns true if the required scope is present in the token's scopes" do token = double("token", scopes: [:api, :read_user]) - scopes = [API::Scope.new(:api)] + scopes = [:api] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if more than one of the required scopes is present in the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - scopes = [API::Scope.new(:api), API::Scope.new(:other_scope)] + scopes = [:api, :other_scope] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes is an exact match for the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) - scopes = [API::Scope.new(:api), API::Scope.new(:read_user), API::Scope.new(:other_scope)] + scopes = [:api, :read_user, :other_scope] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do token = double("token", scopes: [:api, :read_user]) - scopes = [API::Scope.new(:api), API::Scope.new(:read_user), API::Scope.new(:other_scope)] + scopes = [:api, :read_user, :other_scope] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end @@ -41,7 +41,7 @@ describe AccessTokenValidationService, services: true do it "returns false if there are no scopes in common between the required scopes and the token scopes" do token = double("token", scopes: [:api, :read_user]) - scopes = [API::Scope.new(:other_scope)] + scopes = [:other_scope] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(false) end @@ -56,7 +56,7 @@ describe AccessTokenValidationService, services: true do it "does not ignore scopes whose `if` condition is not set" do token = double("token", scopes: [:api, :read_user]) - scopes = [API::Scope.new(:api, if: ->(_) { false }), API::Scope.new(:read_user)] + scopes = [API::Scope.new(:api, if: ->(_) { false }), :read_user] expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end -- cgit v1.2.1 From 829aed07429ad312e7adf44c8330a87ce8144431 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 30 Jun 2017 15:36:20 +0800 Subject: Add changelog entry --- changelogs/unreleased/32815--Add-Custom-CI-Config-Path.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/32815--Add-Custom-CI-Config-Path.yml diff --git a/changelogs/unreleased/32815--Add-Custom-CI-Config-Path.yml b/changelogs/unreleased/32815--Add-Custom-CI-Config-Path.yml new file mode 100644 index 00000000000..eff41f62df4 --- /dev/null +++ b/changelogs/unreleased/32815--Add-Custom-CI-Config-Path.yml @@ -0,0 +1,4 @@ +--- +title: Allow customize CI config path Keith Pope +merge_request: 12509 +author: Keith Pope -- cgit v1.2.1 From 057c3c4e31df9dc8b1866b185dbf6d89e2751e3c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 30 Jun 2017 16:14:48 +0800 Subject: Introduce CI_CONFIG_PATH --- app/models/ci/pipeline.rb | 3 ++- doc/ci/variables/README.md | 1 + spec/models/ci/build_spec.rb | 11 +++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 12986e5781e..6905ab9ea23 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -391,7 +391,8 @@ module Ci def predefined_variables [ - { key: 'CI_PIPELINE_ID', value: id.to_s, public: true } + { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }, + { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true } ] end diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index d1f9881e51b..82c4ea38aa4 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -40,6 +40,7 @@ future GitLab releases.** | **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_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Default to `.gitlab-ci.yml` | | **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. | diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 488697f74eb..e5fd549f0d7 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1179,6 +1179,7 @@ describe Ci::Build, :models do { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, + { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true }, { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true }, { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false }, { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false } @@ -1469,6 +1470,16 @@ describe Ci::Build, :models do it { is_expected.to include(deployment_variable) } end + context 'when project has custom CI config path' do + let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: 'custom', public: true } } + + before do + project.update(ci_config_file: 'custom') + end + + it { is_expected.to include(ci_config_path) } + end + context 'returns variables in valid order' do let(:build_pre_var) { { key: 'build', value: 'value' } } let(:project_pre_var) { { key: 'project', value: 'value' } } -- cgit v1.2.1 From 571297e3f7527065673f1b7cf1f9b7d969178457 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 30 Jun 2017 09:05:00 +0000 Subject: Remove unnecessary pull command from codeclimate job --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5723b836c76..a3ce1de50c2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -474,8 +474,6 @@ codeclimate: services: - docker:dind script: - - docker pull stedolan/jq - - docker pull codeclimate/codeclimate - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json artifacts: -- cgit v1.2.1 From 42ccb5981a8425216f9d69372754c52510f0cade Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 20 Jun 2017 16:38:14 +0100 Subject: Only do complicated confidentiality checks when necessary When we are filtering by a single project, and the current user has access to see confidential issues on that project, we don't need to filter by confidentiality at all - just as if the user were an admin. The filter by confidentiality often picks a non-optimal query plan: for instance, AND-ing the results of all issues in the project (a relatively small set), and all issues in the states requested (a huge set), rather than just starting small and winnowing further. --- app/finders/issues_finder.rb | 42 +++++++---- app/views/layouts/nav/_project.html.haml | 4 +- spec/finders/issues_finder_spec.rb | 125 +++++++++++++++++++++++++++---- 3 files changed, 142 insertions(+), 29 deletions(-) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 3da5508aefd..3455a75b8bc 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -16,14 +16,30 @@ # sort: string # class IssuesFinder < IssuableFinder + CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER + def klass Issue end + def not_restricted_by_confidentiality + return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues? + return Issue.all if user_can_see_all_confidential_issues? + + Issue.where(' + issues.confidential IS NOT TRUE + OR (issues.confidential = TRUE + AND (issues.author_id = :user_id + OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id) + OR issues.project_id IN(:project_ids)))', + user_id: current_user.id, + project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id)) + end + private def init_collection - IssuesFinder.not_restricted_by_confidentiality(current_user) + not_restricted_by_confidentiality end def by_assignee(items) @@ -38,22 +54,20 @@ class IssuesFinder < IssuableFinder end end - def self.not_restricted_by_confidentiality(user) - return Issue.where('issues.confidential IS NOT TRUE') if user.blank? + def item_project_ids(items) + items&.reorder(nil)&.select(:project_id) + end - return Issue.all if user.full_private_access? + def user_can_see_all_confidential_issues? + return false unless current_user + return true if current_user.full_private_access? - Issue.where(' - issues.confidential IS NOT TRUE - OR (issues.confidential = TRUE - AND (issues.author_id = :user_id - OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id) - OR issues.project_id IN(:project_ids)))', - user_id: user.id, - project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) + project? && + project && + project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL end - def item_project_ids(items) - items&.reorder(nil)&.select(:project_id) + def user_cannot_see_confidential_issues? + current_user.blank? end end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 68024d782a6..a2c6e44425a 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -28,7 +28,7 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id, state: :opened).execute.count) - if project_nav_tab? :merge_requests - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] @@ -37,7 +37,7 @@ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id, state: :opened).execute.count) - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 8ace1fb5751..dfa15e859a4 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -295,22 +295,121 @@ describe IssuesFinder do end end - describe '.not_restricted_by_confidentiality' do - let(:authorized_user) { create(:user) } - let(:project) { create(:empty_project, namespace: authorized_user.namespace) } - let!(:public_issue) { create(:issue, project: project) } - let!(:confidential_issue) { create(:issue, project: project, confidential: true) } - - it 'returns non confidential issues for nil user' do - expect(described_class.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) - end + describe '#not_restricted_by_confidentiality' do + let(:guest) { create(:user) } + set(:authorized_user) { create(:user) } + set(:project) { create(:empty_project, namespace: authorized_user.namespace) } + set(:public_issue) { create(:issue, project: project) } + set(:confidential_issue) { create(:issue, project: project, confidential: true) } + + context 'when no project filter is given' do + let(:params) { {} } + + context 'for an anonymous user' do + subject { described_class.new(nil, params).not_restricted_by_confidentiality } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + end + + context 'for a user without project membership' do + subject { described_class.new(user, params).not_restricted_by_confidentiality } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + end + + context 'for a guest user' do + subject { described_class.new(guest, params).not_restricted_by_confidentiality } + + before do + project.add_guest(guest) + end + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + end + + context 'for a project member with access to view confidential issues' do + subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality } - it 'returns non confidential issues for user not authorized for the issues projects' do - expect(described_class.send(:not_restricted_by_confidentiality, user)).to include(public_issue) + it 'returns all issues' do + expect(subject).to include(public_issue, confidential_issue) + end + end end - it 'returns all issues for user authorized for the issues projects' do - expect(described_class.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) + context 'when searching within a specific project' do + let(:params) { { project_id: project.id } } + + context 'for an anonymous user' do + subject { described_class.new(nil, params).not_restricted_by_confidentiality } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + + it 'does not filter by confidentiality' do + expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for a user without project membership' do + subject { described_class.new(user, params).not_restricted_by_confidentiality } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + + it 'filters by confidentiality' do + expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for a guest user' do + subject { described_class.new(guest, params).not_restricted_by_confidentiality } + + before do + project.add_guest(guest) + end + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + + it 'filters by confidentiality' do + expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for a project member with access to view confidential issues' do + subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality } + + it 'returns all issues' do + expect(subject).to include(public_issue, confidential_issue) + end + + it 'does not filter by confidentiality' do + expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end end end end -- cgit v1.2.1 From 20bb678d91715817f3da04c7a1b73db84295accd Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 22 Jun 2017 20:58:20 +0100 Subject: Cache total issue / MR counts for project by user type This runs a slightly slower query to get the issue and MR counts in the navigation, but caches by user type (can see all / none confidential issues) for two minutes. --- app/finders/issues_finder.rb | 26 ++++++++--------- app/helpers/issuables_helper.rb | 29 ++++++++++++------- app/views/layouts/nav/_project.html.haml | 4 +-- spec/helpers/issuables_helper_spec.rb | 49 ++++++++++++++++++++++++++++---- 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 3455a75b8bc..328198c026a 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -36,6 +36,19 @@ class IssuesFinder < IssuableFinder project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id)) end + def user_can_see_all_confidential_issues? + return false unless current_user + return true if current_user.full_private_access? + + project? && + project && + project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL + end + + def user_cannot_see_confidential_issues? + current_user.blank? + end + private def init_collection @@ -57,17 +70,4 @@ class IssuesFinder < IssuableFinder def item_project_ids(items) items&.reorder(nil)&.select(:project_id) end - - def user_can_see_all_confidential_issues? - return false unless current_user - return true if current_user.full_private_access? - - project? && - project && - project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL - end - - def user_cannot_see_confidential_issues? - current_user.blank? - end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 3259a9c1933..d99a9bab12f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -165,11 +165,7 @@ module IssuablesHelper } state_title = titles[state] || state.to_s.humanize - - count = - Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do - issuables_count_for_state(issuable_type, state) - end + count = issuables_count_for_state(issuable_type, state) html = content_tag(:span, state_title) html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') @@ -255,22 +251,35 @@ module IssuablesHelper end end - def issuables_count_for_state(issuable_type, state) + def issuables_count_for_state(issuable_type, state, finder: nil) + finder ||= public_send("#{issuable_type}_finder") + cache_key = issuables_state_counter_cache_key(issuable_type, finder, state) + @counts ||= {} - @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state - @counts[issuable_type][state] + @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do + finder.count_by_state + end + + @counts[cache_key][state] end IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY - def issuables_state_counter_cache_key(issuable_type, state) + def issuables_state_counter_cache_key(issuable_type, finder, state) opts = params.with_indifferent_access opts[:state] = state opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) opts.delete_if { |_, value| value.blank? } - hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-')) + key_components = ['issuables_count', issuable_type, opts.sort] + + if issuable_type == :issues + key_components << finder.user_can_see_all_confidential_issues? + key_components << finder.user_cannot_see_confidential_issues? + end + + hexdigest(key_components.flatten.join('-')) end def issuable_templates(issuable) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a2c6e44425a..b095adcfe7e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -28,7 +28,7 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id, state: :opened).execute.count) + %span.badge.count.issue_counter= number_with_delimiter(issuables_count_for_state(:issues, :opened, finder: IssuesFinder.new(current_user, project_id: @project.id))) - if project_nav_tab? :merge_requests - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] @@ -37,7 +37,7 @@ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id, state: :opened).execute.count) + %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(issuables_count_for_state(:merge_requests, :opened, finder: MergeRequestsFinder.new(current_user, project_id: @project.id))) - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 15cb620199d..7dfda388de4 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -77,20 +77,58 @@ describe IssuablesHelper do }.with_indifferent_access end + let(:finder) { double(:finder, user_cannot_see_confidential_issues?: true, user_can_see_all_confidential_issues?: false) } + + before do + allow(helper).to receive(:issues_finder).and_return(finder) + allow(helper).to receive(:merge_requests_finder).and_return(finder) + end + it 'returns the cached value when called for the same issuable type & with the same params' do expect(helper).to receive(:params).twice.and_return(params) - expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) + expect(finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') - expect(helper).not_to receive(:issuables_count_for_state) + expect(finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') end + it 'takes confidential status into account when searching for issues' do + allow(helper).to receive(:params).and_return(params) + expect(finder).to receive(:count_by_state).and_return(opened: 42) + + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to include('42') + + expect(finder).to receive(:user_cannot_see_confidential_issues?).and_return(false) + expect(finder).to receive(:count_by_state).and_return(opened: 40) + + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to include('40') + + expect(finder).to receive(:user_can_see_all_confidential_issues?).and_return(true) + expect(finder).to receive(:count_by_state).and_return(opened: 45) + + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to include('45') + end + + it 'does not take confidential status into account when searching for merge requests' do + allow(helper).to receive(:params).and_return(params) + expect(finder).to receive(:count_by_state).and_return(opened: 42) + expect(finder).not_to receive(:user_cannot_see_confidential_issues?) + expect(finder).not_to receive(:user_can_see_all_confidential_issues?) + + expect(helper.issuables_state_counter_text(:merge_requests, :opened)) + .to include('42') + end + it 'does not take some keys into account in the cache key' do + expect(finder).to receive(:count_by_state).and_return(opened: 42) expect(helper).to receive(:params).and_return({ author_id: '11', state: 'foo', @@ -98,11 +136,11 @@ describe IssuablesHelper do utf8: 'foo', page: 'foo' }.with_indifferent_access) - expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') + expect(finder).not_to receive(:count_by_state) expect(helper).to receive(:params).and_return({ author_id: '11', state: 'bar', @@ -110,7 +148,6 @@ describe IssuablesHelper do utf8: 'bar', page: 'bar' }.with_indifferent_access) - expect(helper).not_to receive(:issuables_count_for_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') @@ -118,13 +155,13 @@ describe IssuablesHelper do it 'does not take params order into account in the cache key' do expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') - expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) + expect(finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') - expect(helper).not_to receive(:issuables_count_for_state) + expect(finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') -- cgit v1.2.1 From c400030d0f51c71f32990ab0ecfebfa245ed663e Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 23 Jun 2017 12:50:33 +0100 Subject: Don't count any confidential issues for non-project-members --- app/finders/issuable_finder.rb | 2 +- app/finders/issues_finder.rb | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 558f8b5e2e5..e8605f3d5b3 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -62,7 +62,7 @@ class IssuableFinder # grouping and counting within that query. # def count_by_state - count_params = params.merge(state: nil, sort: nil) + count_params = params.merge(state: nil, sort: nil, for_counting: true) labels_count = label_names.any? ? label_names.count : 1 finder = self.class.new(current_user, count_params) counts = Hash.new(0) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 328198c026a..b213a7aebfd 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -23,8 +23,8 @@ class IssuesFinder < IssuableFinder end def not_restricted_by_confidentiality - return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues? return Issue.all if user_can_see_all_confidential_issues? + return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues? Issue.where(' issues.confidential IS NOT TRUE @@ -37,16 +37,19 @@ class IssuesFinder < IssuableFinder end def user_can_see_all_confidential_issues? - return false unless current_user - return true if current_user.full_private_access? + return @user_can_see_all_confidential_issues = false if current_user.blank? + return @user_can_see_all_confidential_issues = true if current_user.full_private_access? - project? && + @user_can_see_all_confidential_issues = + project? && project && project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL end def user_cannot_see_confidential_issues? - current_user.blank? + return false if user_can_see_all_confidential_issues? + + current_user.blank? || params[:for_counting] end private -- cgit v1.2.1 From 8deece32478aaa83354fcfff7b5d6f3250d55844 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 26 Jun 2017 15:48:27 +0100 Subject: Add changelog entry for issue / MR tab counting optimisations --- changelogs/unreleased/speed-up-issue-counting-for-a-project.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/speed-up-issue-counting-for-a-project.yml diff --git a/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml new file mode 100644 index 00000000000..493ecbcb77a --- /dev/null +++ b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml @@ -0,0 +1,5 @@ +--- +title: Cache open issue and merge request counts for project tabs to speed up project + pages +merge_request: +author: -- cgit v1.2.1 From 0c6cdd07829668e04012219eb21cc60db8c1eabc Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 29 Jun 2017 12:43:56 +0100 Subject: Make finders responsible for counter cache keys --- app/finders/issuable_finder.rb | 14 +++++++++++ app/finders/issues_finder.rb | 23 +++++++++++++----- app/helpers/issuables_helper.rb | 21 +--------------- spec/finders/issues_finder_spec.rb | 18 +++++++------- spec/helpers/issuables_helper_spec.rb | 46 +++++++++++++++++------------------ 5 files changed, 63 insertions(+), 59 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index e8605f3d5b3..7bc2117f61e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -20,6 +20,7 @@ # class IssuableFinder NONE = '0'.freeze + IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze attr_accessor :current_user, :params @@ -86,6 +87,10 @@ class IssuableFinder execute.find_by!(*params) end + def state_counter_cache_key(state) + Digest::SHA1.hexdigest(state_counter_cache_key_components(state).flatten.join('-')) + end + def group return @group if defined?(@group) @@ -418,4 +423,13 @@ class IssuableFinder def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end + + def state_counter_cache_key_components(state) + opts = params.with_indifferent_access + opts[:state] = state + opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) + opts.delete_if { |_, value| value.blank? } + + ['issuables_count', klass.to_ability_name, opts.sort] + end end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index b213a7aebfd..d20f4475a03 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -22,7 +22,7 @@ class IssuesFinder < IssuableFinder Issue end - def not_restricted_by_confidentiality + def with_confidentiality_access_check return Issue.all if user_can_see_all_confidential_issues? return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues? @@ -36,7 +36,15 @@ class IssuesFinder < IssuableFinder project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id)) end + private + + def init_collection + with_confidentiality_access_check + end + def user_can_see_all_confidential_issues? + return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues) + return @user_can_see_all_confidential_issues = false if current_user.blank? return @user_can_see_all_confidential_issues = true if current_user.full_private_access? @@ -46,16 +54,19 @@ class IssuesFinder < IssuableFinder project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL end - def user_cannot_see_confidential_issues? + def user_cannot_see_confidential_issues?(for_counting: false) return false if user_can_see_all_confidential_issues? - current_user.blank? || params[:for_counting] + current_user.blank? || for_counting || params[:for_counting] end - private + def state_counter_cache_key_components(state) + extra_components = [ + user_can_see_all_confidential_issues?, + user_cannot_see_confidential_issues?(for_counting: true) + ] - def init_collection - not_restricted_by_confidentiality + super + extra_components end def by_assignee(items) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index d99a9bab12f..5385ada6dc4 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -253,7 +253,7 @@ module IssuablesHelper def issuables_count_for_state(issuable_type, state, finder: nil) finder ||= public_send("#{issuable_type}_finder") - cache_key = issuables_state_counter_cache_key(issuable_type, finder, state) + cache_key = finder.state_counter_cache_key(state) @counts ||= {} @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do @@ -263,25 +263,6 @@ module IssuablesHelper @counts[cache_key][state] end - IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze - private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY - - def issuables_state_counter_cache_key(issuable_type, finder, state) - opts = params.with_indifferent_access - opts[:state] = state - opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) - opts.delete_if { |_, value| value.blank? } - - key_components = ['issuables_count', issuable_type, opts.sort] - - if issuable_type == :issues - key_components << finder.user_can_see_all_confidential_issues? - key_components << finder.user_cannot_see_confidential_issues? - end - - hexdigest(key_components.flatten.join('-')) - end - def issuable_templates(issuable) @issuable_templates ||= case issuable diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index dfa15e859a4..4a52f0d5c58 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -295,7 +295,7 @@ describe IssuesFinder do end end - describe '#not_restricted_by_confidentiality' do + describe '#with_confidentiality_access_check' do let(:guest) { create(:user) } set(:authorized_user) { create(:user) } set(:project) { create(:empty_project, namespace: authorized_user.namespace) } @@ -306,7 +306,7 @@ describe IssuesFinder do let(:params) { {} } context 'for an anonymous user' do - subject { described_class.new(nil, params).not_restricted_by_confidentiality } + subject { described_class.new(nil, params).with_confidentiality_access_check } it 'returns only public issues' do expect(subject).to include(public_issue) @@ -315,7 +315,7 @@ describe IssuesFinder do end context 'for a user without project membership' do - subject { described_class.new(user, params).not_restricted_by_confidentiality } + subject { described_class.new(user, params).with_confidentiality_access_check } it 'returns only public issues' do expect(subject).to include(public_issue) @@ -324,7 +324,7 @@ describe IssuesFinder do end context 'for a guest user' do - subject { described_class.new(guest, params).not_restricted_by_confidentiality } + subject { described_class.new(guest, params).with_confidentiality_access_check } before do project.add_guest(guest) @@ -337,7 +337,7 @@ describe IssuesFinder do end context 'for a project member with access to view confidential issues' do - subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality } + subject { described_class.new(authorized_user, params).with_confidentiality_access_check } it 'returns all issues' do expect(subject).to include(public_issue, confidential_issue) @@ -349,7 +349,7 @@ describe IssuesFinder do let(:params) { { project_id: project.id } } context 'for an anonymous user' do - subject { described_class.new(nil, params).not_restricted_by_confidentiality } + subject { described_class.new(nil, params).with_confidentiality_access_check } it 'returns only public issues' do expect(subject).to include(public_issue) @@ -364,7 +364,7 @@ describe IssuesFinder do end context 'for a user without project membership' do - subject { described_class.new(user, params).not_restricted_by_confidentiality } + subject { described_class.new(user, params).with_confidentiality_access_check } it 'returns only public issues' do expect(subject).to include(public_issue) @@ -379,7 +379,7 @@ describe IssuesFinder do end context 'for a guest user' do - subject { described_class.new(guest, params).not_restricted_by_confidentiality } + subject { described_class.new(guest, params).with_confidentiality_access_check } before do project.add_guest(guest) @@ -398,7 +398,7 @@ describe IssuesFinder do end context 'for a project member with access to view confidential issues' do - subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality } + subject { described_class.new(authorized_user, params).with_confidentiality_access_check } it 'returns all issues' do expect(subject).to include(public_issue, confidential_issue) diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 7dfda388de4..d2e918ef014 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -77,59 +77,57 @@ describe IssuablesHelper do }.with_indifferent_access end - let(:finder) { double(:finder, user_cannot_see_confidential_issues?: true, user_can_see_all_confidential_issues?: false) } + let(:issues_finder) { IssuesFinder.new(nil, params) } + let(:merge_requests_finder) { MergeRequestsFinder.new(nil, params) } before do - allow(helper).to receive(:issues_finder).and_return(finder) - allow(helper).to receive(:merge_requests_finder).and_return(finder) + allow(helper).to receive(:issues_finder).and_return(issues_finder) + allow(helper).to receive(:merge_requests_finder).and_return(merge_requests_finder) end it 'returns the cached value when called for the same issuable type & with the same params' do - expect(helper).to receive(:params).twice.and_return(params) - expect(finder).to receive(:count_by_state).and_return(opened: 42) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') - expect(finder).not_to receive(:count_by_state) + expect(issues_finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') end it 'takes confidential status into account when searching for issues' do - allow(helper).to receive(:params).and_return(params) - expect(finder).to receive(:count_by_state).and_return(opened: 42) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to include('42') - expect(finder).to receive(:user_cannot_see_confidential_issues?).and_return(false) - expect(finder).to receive(:count_by_state).and_return(opened: 40) + expect(issues_finder).to receive(:user_cannot_see_confidential_issues?).twice.and_return(false) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 40) expect(helper.issuables_state_counter_text(:issues, :opened)) .to include('40') - expect(finder).to receive(:user_can_see_all_confidential_issues?).and_return(true) - expect(finder).to receive(:count_by_state).and_return(opened: 45) + expect(issues_finder).to receive(:user_can_see_all_confidential_issues?).and_return(true) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 45) expect(helper.issuables_state_counter_text(:issues, :opened)) .to include('45') end it 'does not take confidential status into account when searching for merge requests' do - allow(helper).to receive(:params).and_return(params) - expect(finder).to receive(:count_by_state).and_return(opened: 42) - expect(finder).not_to receive(:user_cannot_see_confidential_issues?) - expect(finder).not_to receive(:user_can_see_all_confidential_issues?) + expect(merge_requests_finder).to receive(:count_by_state).and_return(opened: 42) + expect(merge_requests_finder).not_to receive(:user_cannot_see_confidential_issues?) + expect(merge_requests_finder).not_to receive(:user_can_see_all_confidential_issues?) expect(helper.issuables_state_counter_text(:merge_requests, :opened)) .to include('42') end it 'does not take some keys into account in the cache key' do - expect(finder).to receive(:count_by_state).and_return(opened: 42) - expect(helper).to receive(:params).and_return({ + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) + expect(issues_finder).to receive(:params).and_return({ author_id: '11', state: 'foo', sort: 'foo', @@ -140,8 +138,8 @@ describe IssuablesHelper do expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') - expect(finder).not_to receive(:count_by_state) - expect(helper).to receive(:params).and_return({ + expect(issues_finder).not_to receive(:count_by_state) + expect(issues_finder).to receive(:params).and_return({ author_id: '11', state: 'bar', sort: 'bar', @@ -154,14 +152,14 @@ describe IssuablesHelper do end it 'does not take params order into account in the cache key' do - expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') - expect(finder).to receive(:count_by_state).and_return(opened: 42) + expect(issues_finder).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') - expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') - expect(finder).not_to receive(:count_by_state) + expect(issues_finder).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') + expect(issues_finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') -- cgit v1.2.1 From cb30edfae5c3557686463ca22eca7ef572c3ac33 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 29 Jun 2017 17:15:49 +0100 Subject: Clarify counter caching for users without project access --- app/finders/issues_finder.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index d20f4475a03..18f60f9a2b6 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -54,6 +54,21 @@ class IssuesFinder < IssuableFinder project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL end + # Anonymous users can't see any confidential issues. + # + # Users without access to see _all_ confidential issues (as in + # `user_can_see_all_confidential_issues?`) are more complicated, because they + # can see confidential issues where: + # 1. They are an assignee. + # 2. The are an author. + # + # That's fine for most cases, but if we're just counting, we need to cache + # effectively. If we cached this accurately, we'd have a cache key for every + # authenticated user without sufficient access to the project. Instead, when + # we are counting, we treat them as if they can't see any confidential issues. + # + # This does mean the counts may be wrong for those users, but avoids an + # explosion in cache keys. def user_cannot_see_confidential_issues?(for_counting: false) return false if user_can_see_all_confidential_issues? -- cgit v1.2.1 From 49b747725ef5932d34ed83e028f77b2d370fd76a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 30 Jun 2017 09:42:17 +0000 Subject: Only verifies top position after the request has finished to account for errors --- app/assets/javascripts/build.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 9974e135022..60103155ce0 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -85,9 +85,8 @@ window.Build = (function () { if (!this.hasBeenScrolled) { this.scrollToBottom(); } - }); - - this.verifyTopPosition(); + }) + .then(() => this.verifyTopPosition()); } Build.prototype.canScroll = function () { @@ -176,7 +175,7 @@ window.Build = (function () { } if ($flashError.length) { - topPostion += $flashError.outerHeight(); + topPostion += $flashError.outerHeight() + prependTopDefault; } this.$buildTrace.css({ @@ -234,7 +233,8 @@ window.Build = (function () { if (!this.hasBeenScrolled) { this.scrollToBottom(); } - }); + }) + .then(() => this.verifyTopPosition()); }, 4000); } else { this.$buildRefreshAnimation.remove(); -- cgit v1.2.1 From 4bd17d4f7427b2f5f950e601d1a21042b30f69b1 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 30 Jun 2017 11:55:27 +0100 Subject: Make issuables_count_for_state public This doesn't need to be public in CE, but EE uses it as such. --- app/helpers/issuables_helper.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 5385ada6dc4..05177e58c5a 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -233,6 +233,18 @@ module IssuablesHelper } end + def issuables_count_for_state(issuable_type, state, finder: nil) + finder ||= public_send("#{issuable_type}_finder") + cache_key = finder.state_counter_cache_key(state) + + @counts ||= {} + @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do + finder.count_by_state + end + + @counts[cache_key][state] + end + private def sidebar_gutter_collapsed? @@ -251,18 +263,6 @@ module IssuablesHelper end end - def issuables_count_for_state(issuable_type, state, finder: nil) - finder ||= public_send("#{issuable_type}_finder") - cache_key = finder.state_counter_cache_key(state) - - @counts ||= {} - @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do - finder.count_by_state - end - - @counts[cache_key][state] - end - def issuable_templates(issuable) @issuable_templates ||= case issuable -- cgit v1.2.1 From 5565e5ee401ed18cd8b082709d0757f97fa75b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Fri, 30 Jun 2017 18:56:38 +0800 Subject: fix the format of the translation string --- locale/it/gitlab.po | 273 ++++++++++++++++++++++++++-------------------------- 1 file changed, 137 insertions(+), 136 deletions(-) diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po index 19848b767de..777e4c0ece3 100644 --- a/locale/it/gitlab.po +++ b/locale/it/gitlab.po @@ -1,3 +1,4 @@ +# Huang Tao , 2017. #zanata # Paolo Falomo , 2017. #zanata msgid "" msgstr "" @@ -74,7 +75,7 @@ msgid "Browse files" msgstr "Guarda i files" msgid "ByAuthor|by" -msgstr "PerAutore|per" +msgstr "per" msgid "CI configuration" msgstr "Configurazione CI (Integrazione Continua)" @@ -83,16 +84,16 @@ msgid "Cancel" msgstr "Cancella" msgid "ChangeTypeActionLabel|Pick into branch" -msgstr "CambiaEtichettaDelTipoDiAzione|Preleva nella branch" +msgstr "Preleva nella branch" msgid "ChangeTypeActionLabel|Revert in branch" -msgstr "CambiaEtichettaDelTipoDiAzione|Ripristina nella branch" +msgstr "Ripristina nella branch" msgid "ChangeTypeAction|Cherry-pick" -msgstr "CambiaTipoDiAzione|Cherry-pick" +msgstr "Cherry-pick" msgid "ChangeTypeAction|Revert" -msgstr "CambiaTipoDiAzione|Ripristina" +msgstr "Ripristina" msgid "Changelog" msgstr "Changelog" @@ -107,58 +108,58 @@ msgid "Cherry-pick this merge request" msgstr "Cherry-pick questa richiesta di merge" msgid "CiStatusLabel|canceled" -msgstr "CiStatusLabel|cancellato" +msgstr "cancellato" msgid "CiStatusLabel|created" -msgstr "CiStatusLabel|creato" +msgstr "creato" msgid "CiStatusLabel|failed" -msgstr "CiStatusLabel|fallito" +msgstr "fallito" msgid "CiStatusLabel|manual action" -msgstr "CiStatusLabel|azione manuale" +msgstr "azione manuale" msgid "CiStatusLabel|passed" -msgstr "CiStatusLabel|superata" +msgstr "superata" msgid "CiStatusLabel|passed with warnings" -msgstr "CiStatusLabel|superata con avvisi" +msgstr "superata con avvisi" msgid "CiStatusLabel|pending" -msgstr "CiStatusLabel|in coda" +msgstr "in coda" msgid "CiStatusLabel|skipped" -msgstr "CiStatusLabel|saltata" +msgstr "saltata" msgid "CiStatusLabel|waiting for manual action" -msgstr "CiStatusLabel|in attesa di azione manuale" +msgstr "in attesa di azione manuale" msgid "CiStatusText|blocked" -msgstr "CiStatusText|bloccata" +msgstr "bloccata" msgid "CiStatusText|canceled" -msgstr "CiStatusText|cancellata" +msgstr "cancellata" msgid "CiStatusText|created" -msgstr "CiStatusText|creata" +msgstr "creata" msgid "CiStatusText|failed" -msgstr "CiStatusText|fallita" +msgstr "fallita" msgid "CiStatusText|manual" -msgstr "CiStatusText|manuale" +msgstr "manuale" msgid "CiStatusText|passed" -msgstr "CiStatusText|superata" +msgstr "superata" msgid "CiStatusText|pending" -msgstr "CiStatusText|in coda" +msgstr "in coda" msgid "CiStatusText|skipped" -msgstr "CiStatusText|saltata" +msgstr "saltata" msgid "CiStatus|running" -msgstr "CiStatus|in corso" +msgstr "in corso" msgid "Commit" msgid_plural "Commits" @@ -169,16 +170,16 @@ msgid "Commit message" msgstr "Messaggio del commit" msgid "CommitBoxTitle|Commit" -msgstr "CommitBoxTitle|Commit" +msgstr "Commit" msgid "CommitMessage|Add %{file_name}" -msgstr "CommitMessage|Aggiungi %{file_name}" +msgstr "Aggiungi %{file_name}" msgid "Commits" msgstr "Commits" msgid "Commits|History" -msgstr "Commits|Cronologia" +msgstr "Cronologia" msgid "Committed by" msgstr "Committato da " @@ -214,10 +215,10 @@ msgid "Create new..." msgstr "Crea nuovo..." msgid "CreateNewFork|Fork" -msgstr "CreateNewFork|Fork" +msgstr "Fork" msgid "CreateTag|Tag" -msgstr "CreateTag|Tag" +msgstr "Tag" msgid "Cron Timezone" msgstr "Cron Timezone" @@ -248,25 +249,25 @@ msgstr "" "ed il rilascio in produzione del tuo progetto" msgid "CycleAnalyticsStage|Code" -msgstr "StadioAnalisiCiclica|Codice" +msgstr "Codice" msgid "CycleAnalyticsStage|Issue" -msgstr "StadioAnalisiCiclica|Issue" +msgstr "Issue" msgid "CycleAnalyticsStage|Plan" -msgstr "StadioAnalisiCiclica|Pianificazione" +msgstr "Pianificazione" msgid "CycleAnalyticsStage|Production" -msgstr "StadioAnalisiCiclica|Produzione" +msgstr "Produzione" msgid "CycleAnalyticsStage|Review" -msgstr "StadioAnalisiCiclica|Revisione" +msgstr "Revisione" msgid "CycleAnalyticsStage|Staging" -msgstr "StadioAnalisiCiclica|Pre-rilascio" +msgstr "Pre-rilascio" msgid "CycleAnalyticsStage|Test" -msgstr "StadioAnalisiCiclica|Test" +msgstr "Test" msgid "Define a custom pattern with cron syntax" msgstr "Definisci un patter personalizzato mediante la sintassi cron" @@ -304,16 +305,16 @@ msgid "Download zip" msgstr "Scarica zip" msgid "DownloadArtifacts|Download" -msgstr "DownloadArtifacts|Scarica" +msgstr "Scarica" msgid "DownloadCommit|Email Patches" -msgstr "DownloadCommit|Email Patches" +msgstr "Email Patches" msgid "DownloadCommit|Plain Diff" -msgstr "DownloadCommit|Differenze" +msgstr "Differenze" msgid "DownloadSource|Download" -msgstr "DownloadSource|Scarica" +msgstr "Scarica" msgid "Edit" msgstr "Modifica" @@ -346,10 +347,10 @@ msgid "Find file" msgstr "Trova file" msgid "FirstPushedBy|First" -msgstr "PrimoPushDel|Primo" +msgstr "Primo" msgid "FirstPushedBy|pushed by" -msgstr "PrimoPushDel|Push di" +msgstr "Push di" msgid "Fork" msgid_plural "Forks" @@ -357,7 +358,7 @@ msgstr[0] "Fork" msgstr[1] "Forks" msgid "ForkedFromProjectPath|Forked from" -msgstr "ForkedFromProjectPath|Fork da" +msgstr "Fork da" msgid "From issue creation until deploy to production" msgstr "Dalla creazione di un issue fino al rilascio in produzione" @@ -371,7 +372,7 @@ msgid "Go to your fork" msgstr "Vai il tuo fork" msgid "GoToYourFork|Fork" -msgstr "GoToYourFork|Fork" +msgstr "Fork" msgid "Home" msgstr "Home" @@ -389,10 +390,10 @@ msgid "Introducing Cycle Analytics" msgstr "Introduzione delle Analisi Cicliche" msgid "LFSStatus|Disabled" -msgstr "LFSStatus|Disabilitato" +msgstr "Disabilitato" msgid "LFSStatus|Enabled" -msgstr "LFSStatus|Abilitato" +msgstr "Abilitato" msgid "Last %d day" msgid_plural "Last %d days" @@ -412,7 +413,7 @@ msgid "Learn more in the" msgstr "Leggi di più su" msgid "Learn more in the|pipeline schedules documentation" -msgstr "Leggi di più su|documentazione sulla pianificazione delle pipelines" +msgstr "documentazione sulla pianificazione delle pipelines" msgid "Leave group" msgstr "Abbandona il gruppo" @@ -429,7 +430,7 @@ msgid "Median" msgstr "Mediano" msgid "MissingSSHKeyWarningLink|add an SSH key" -msgstr "MissingSSHKeyWarningLink|aggiungi una chiave SSH" +msgstr "aggiungi una chiave SSH" msgid "New Issue" msgid_plural "New Issues" @@ -479,58 +480,58 @@ msgid "Notification events" msgstr "Notifica eventi" msgid "NotificationEvent|Close issue" -msgstr "NotificationEvent|Chiudi issue" +msgstr "Chiudi issue" msgid "NotificationEvent|Close merge request" -msgstr "NotificationEvent|Chiudi richiesta di merge" +msgstr "Chiudi richiesta di merge" msgid "NotificationEvent|Failed pipeline" -msgstr "NotificationEvent|Pipeline fallita" +msgstr "Pipeline fallita" msgid "NotificationEvent|Merge merge request" -msgstr "NotificationEvent|Completa la richiesta di merge" +msgstr "Completa la richiesta di merge" msgid "NotificationEvent|New issue" -msgstr "NotificationEvent|Nuovo issue" +msgstr "Nuovo issue" msgid "NotificationEvent|New merge request" -msgstr "NotificationEvent|Nuova richiesta di merge" +msgstr "Nuova richiesta di merge" msgid "NotificationEvent|New note" -msgstr "NotificationEvent|Nuova nota" +msgstr "Nuova nota" msgid "NotificationEvent|Reassign issue" -msgstr "NotificationEvent|Riassegna issue" +msgstr "Riassegna issue" msgid "NotificationEvent|Reassign merge request" -msgstr "NotificationEvent|Riassegna richiesta di Merge" +msgstr "Riassegna richiesta di Merge" msgid "NotificationEvent|Reopen issue" -msgstr "NotificationEvent|Riapri issue" +msgstr "Riapri issue" msgid "NotificationEvent|Successful pipeline" -msgstr "NotificationEvent|Pipeline Completata" +msgstr "Pipeline Completata" msgid "NotificationLevel|Custom" -msgstr "NotificationLevel|Personalizzato" +msgstr "Personalizzato" msgid "NotificationLevel|Disabled" -msgstr "NotificationLevel|Disabilitato" +msgstr "Disabilitato" msgid "NotificationLevel|Global" -msgstr "NotificationLevel|Globale" +msgstr "Globale" msgid "NotificationLevel|On mention" -msgstr "NotificationLevel|Se menzionato" +msgstr "Se menzionato" msgid "NotificationLevel|Participate" -msgstr "NotificationLevel|Partecipa" +msgstr "Partecipa" msgid "NotificationLevel|Watch" -msgstr "NotificationLevel|Osserva" +msgstr "Osserva" msgid "OfSearchInADropdown|Filter" -msgstr "OfSearchInADropdown|Filtra" +msgstr "Filtra" msgid "OpenedNDaysAgo|Opened" msgstr "ApertoNGiorniFa|Aperto" @@ -554,40 +555,40 @@ msgid "Pipeline Schedules" msgstr "Pianificazione multipla Pipeline" msgid "PipelineSchedules|Activated" -msgstr "PipelineSchedules|Attivata" +msgstr "Attivata" msgid "PipelineSchedules|Active" -msgstr "PipelineSchedules|Attiva" +msgstr "Attiva" msgid "PipelineSchedules|All" -msgstr "PipelineSchedules|Tutto" +msgstr "Tutto" msgid "PipelineSchedules|Inactive" -msgstr "PipelineSchedules|Inattiva" +msgstr "Inattiva" msgid "PipelineSchedules|Next Run" -msgstr "PipelineSchedules|Prossima esecuzione" +msgstr "Prossima esecuzione" msgid "PipelineSchedules|None" -msgstr "PipelineSchedules|Nessuna" +msgstr "Nessuna" msgid "PipelineSchedules|Provide a short description for this pipeline" -msgstr "PipelineSchedules|Fornisci una breve descrizione per questa pipeline" +msgstr "Fornisci una breve descrizione per questa pipeline" msgid "PipelineSchedules|Take ownership" -msgstr "PipelineSchedules|Prendi possesso" +msgstr "Prendi possesso" msgid "PipelineSchedules|Target" -msgstr "PipelineSchedules|Target" +msgstr "Target" msgid "PipelineSheduleIntervalPattern|Custom" -msgstr "PipelineSheduleIntervalPattern|Personalizzato" +msgstr "Personalizzato" msgid "Pipeline|with stage" -msgstr "Pipeline|con stadio" +msgstr "con stadio" msgid "Pipeline|with stages" -msgstr "Pipeline|con più stadi" +msgstr "con più stadi" msgid "Project '%{project_name}' queued for deletion." msgstr "Il Progetto '%{project_name}' in coda di eliminazione." @@ -626,25 +627,25 @@ msgid "Project home" msgstr "Home di progetto" msgid "ProjectFeature|Disabled" -msgstr "ProjectFeature|Disabilitato" +msgstr "Disabilitato" msgid "ProjectFeature|Everyone with access" -msgstr "ProjectFeature|Chiunque con accesso" +msgstr "Chiunque con accesso" msgid "ProjectFeature|Only team members" -msgstr "ProjectFeature|Solo i membri del team" +msgstr "Solo i membri del team" msgid "ProjectFileTree|Name" -msgstr "ProjectFileTree|Nome" +msgstr "Nome" msgid "ProjectLastActivity|Never" -msgstr "ProjectLastActivity|Mai" +msgstr "Mai" msgid "ProjectLifecycle|Stage" msgstr "ProgettoCicloVitale|Stadio" msgid "ProjectNetworkGraph|Graph" -msgstr "ProjectNetworkGraph|Grafico" +msgstr "Grafico" msgid "Read more" msgstr "Continua..." @@ -653,10 +654,10 @@ msgid "Readme" msgstr "Leggimi" msgid "RefSwitcher|Branches" -msgstr "RefSwitcher|Branches" +msgstr "Branches" msgid "RefSwitcher|Tags" -msgstr "RefSwitcher|Tags" +msgstr "Tags" msgid "Related Commits" msgstr "Commit correlati" @@ -727,7 +728,7 @@ msgid "Set up auto deploy" msgstr "Configura il rilascio automatico" msgid "SetPasswordToCloneLink|set a password" -msgstr "SetPasswordToCloneLink|imposta una password" +msgstr "imposta una password" msgid "Showing %d event" msgid_plural "Showing %d events" @@ -738,7 +739,7 @@ msgid "Source code" msgstr "Codice Sorgente" msgid "StarProject|Star" -msgstr "StarProject|Star" +msgstr "Star" msgid "Start a %{new_merge_request} with these changes" msgstr "inizia una %{new_merge_request} con queste modifiche" @@ -882,140 +883,140 @@ msgid "Time until first merge request" msgstr "Il tempo fino alla prima richiesta di merge" msgid "Timeago|%s days ago" -msgstr "Timeago|%s giorni fa" +msgstr "%s giorni fa" msgid "Timeago|%s days remaining" -msgstr "Timeago|%s giorni rimanenti" +msgstr "%s giorni rimanenti" msgid "Timeago|%s hours remaining" -msgstr "Timeago|%s ore rimanenti" +msgstr "%s ore rimanenti" msgid "Timeago|%s minutes ago" -msgstr "Timeago|%s minuti fa" +msgstr "%s minuti fa" msgid "Timeago|%s minutes remaining" -msgstr "Timeago|%s minuti rimanenti" +msgstr "%s minuti rimanenti" msgid "Timeago|%s months ago" -msgstr "Timeago|%s minuti fa" +msgstr "%s minuti fa" msgid "Timeago|%s months remaining" -msgstr "Timeago|%s mesi rimanenti" +msgstr "%s mesi rimanenti" msgid "Timeago|%s seconds remaining" -msgstr "Timeago|%s secondi rimanenti" +msgstr "%s secondi rimanenti" msgid "Timeago|%s weeks ago" -msgstr "Timeago|%s settimane fa" +msgstr "%s settimane fa" msgid "Timeago|%s weeks remaining" -msgstr "Timeago|%s settimane rimanenti" +msgstr "%s settimane rimanenti" msgid "Timeago|%s years ago" -msgstr "Timeago|%s anni fa" +msgstr "%s anni fa" msgid "Timeago|%s years remaining" -msgstr "Timeago|%s anni rimanenti" +msgstr "%s anni rimanenti" msgid "Timeago|1 day remaining" -msgstr "Timeago|1 giorno rimanente" +msgstr "1 giorno rimanente" msgid "Timeago|1 hour remaining" -msgstr "Timeago|1 ora rimanente" +msgstr "1 ora rimanente" msgid "Timeago|1 minute remaining" -msgstr "Timeago|1 minuto rimanente" +msgstr "1 minuto rimanente" msgid "Timeago|1 month remaining" -msgstr "Timeago|1 mese rimanente" +msgstr "1 mese rimanente" msgid "Timeago|1 week remaining" -msgstr "Timeago|1 settimana rimanente" +msgstr "1 settimana rimanente" msgid "Timeago|1 year remaining" -msgstr "Timeago|1 anno rimanente" +msgstr "1 anno rimanente" msgid "Timeago|Past due" -msgstr "Timeago|Entro" +msgstr "Entro" msgid "Timeago|a day ago" -msgstr "Timeago|un giorno fa" +msgstr "un giorno fa" msgid "Timeago|a month ago" -msgstr "Timeago|un mese fa" +msgstr "un mese fa" msgid "Timeago|a week ago" -msgstr "Timeago|una settimana fa" +msgstr "una settimana fa" msgid "Timeago|a while" -msgstr "Timeago|poco fa" +msgstr "poco fa" msgid "Timeago|a year ago" -msgstr "Timeago|un anno fa" +msgstr "un anno fa" msgid "Timeago|about %s hours ago" -msgstr "Timeago|circa %s ore fa" +msgstr "circa %s ore fa" msgid "Timeago|about a minute ago" -msgstr "Timeago|circa un minuto fa" +msgstr "circa un minuto fa" msgid "Timeago|about an hour ago" -msgstr "Timeago|circa un ora fa" +msgstr "circa un ora fa" msgid "Timeago|in %s days" -msgstr "Timeago|in %s giorni" +msgstr "in %s giorni" msgid "Timeago|in %s hours" -msgstr "Timeago|in %s ore" +msgstr "in %s ore" msgid "Timeago|in %s minutes" -msgstr "Timeago|in %s minuti" +msgstr "in %s minuti" msgid "Timeago|in %s months" -msgstr "Timeago|in %s mesi" +msgstr "in %s mesi" msgid "Timeago|in %s seconds" -msgstr "Timeago|in %s secondi" +msgstr "in %s secondi" msgid "Timeago|in %s weeks" -msgstr "Timeago|in %s settimane" +msgstr "in %s settimane" msgid "Timeago|in %s years" -msgstr "Timeago|in %s anni" +msgstr "in %s anni" msgid "Timeago|in 1 day" -msgstr "Timeago|in 1 giorno" +msgstr "in 1 giorno" msgid "Timeago|in 1 hour" -msgstr "Timeago|in 1 ora" +msgstr "in 1 ora" msgid "Timeago|in 1 minute" -msgstr "Timeago|in 1 minuto" +msgstr "in 1 minuto" msgid "Timeago|in 1 month" -msgstr "Timeago|in 1 mese" +msgstr "in 1 mese" msgid "Timeago|in 1 week" -msgstr "Timeago|in 1 settimana" +msgstr "in 1 settimana" msgid "Timeago|in 1 year" -msgstr "Timeago|in 1 anno" +msgstr "in 1 anno" msgid "Timeago|less than a minute ago" -msgstr "Timeago|meno di un minuto fa" +msgstr "meno di un minuto fa" msgid "Time|hr" msgid_plural "Time|hrs" -msgstr[0] "Tempo|hr" -msgstr[1] "Tempo|hr" +msgstr[0] "hr" +msgstr[1] "hr" msgid "Time|min" msgid_plural "Time|mins" -msgstr[0] "Tempo|min" -msgstr[1] "Tempo|mins" +msgstr[0] "min" +msgstr[1] "mins" msgid "Time|s" -msgstr "Tempo|s" +msgstr "s" msgid "Total Time" msgstr "Tempo Totale" @@ -1036,13 +1037,13 @@ msgid "Use your global notification setting" msgstr "Usa le tue impostazioni globali " msgid "VisibilityLevel|Internal" -msgstr "VisibilityLevel|Interno" +msgstr "Interno" msgid "VisibilityLevel|Private" -msgstr "VisibilityLevel|Privato" +msgstr "Privato" msgid "VisibilityLevel|Public" -msgstr "VisibilityLevel|Pubblico" +msgstr "Pubblico" msgid "Want to see the data? Please ask an administrator for access." msgstr "" -- cgit v1.2.1 From 7648f113814a78ffde802172197ba2b0074ec753 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 28 Jun 2017 17:33:48 +0200 Subject: Remove unnecessary contexts --- spec/lib/gitlab/git/repository_spec.rb | 174 ++++++++++++------------------ spec/lib/gitlab/gitaly_client/ref_spec.rb | 4 - spec/models/environment_spec.rb | 27 ++--- 3 files changed, 74 insertions(+), 131 deletions(-) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 0cd458bf933..464cb41a842 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -5,6 +5,11 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } + after do + # Prevent cached stubs (gRPC connection objects) from poisoning tests. + Gitlab::GitalyClient.clear_stubs! + end + describe "Respond to" do subject { repository } @@ -30,31 +35,21 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(repository.root_ref.encoding).to eq(Encoding.find('UTF-8')) end - context 'with gitaly enabled' do - before do - stub_gitaly - end - - after do - Gitlab::GitalyClient.clear_stubs! - end - - it 'gets the branch name from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) - repository.root_ref - end + it 'gets the branch name from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + repository.root_ref + end - it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) - .and_raise(GRPC::NotFound) - expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository) - end + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + .and_raise(GRPC::NotFound) + expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository) + end - it 'wraps GRPC exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) - .and_raise(GRPC::Unknown) - expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError) - end + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + .and_raise(GRPC::Unknown) + expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError) end end @@ -135,31 +130,21 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to include("master") } it { is_expected.not_to include("branch-from-space") } - context 'with gitaly enabled' do - before do - stub_gitaly - end - - after do - Gitlab::GitalyClient.clear_stubs! - end - - it 'gets the branch names from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) - subject - end + it 'gets the branch names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + subject + end - it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) - .and_raise(GRPC::NotFound) - expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) - end + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + .and_raise(GRPC::NotFound) + expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) + end - it 'wraps GRPC other exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) - .and_raise(GRPC::Unknown) - expect { subject }.to raise_error(Gitlab::Git::CommandError) - end + it 'wraps GRPC other exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + .and_raise(GRPC::Unknown) + expect { subject }.to raise_error(Gitlab::Git::CommandError) end end @@ -183,31 +168,21 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to include("v1.0.0") } it { is_expected.not_to include("v5.0.0") } - context 'with gitaly enabled' do - before do - stub_gitaly - end - - after do - Gitlab::GitalyClient.clear_stubs! - end - - it 'gets the tag names from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) - subject - end + it 'gets the tag names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + subject + end - it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) - .and_raise(GRPC::NotFound) - expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) - end + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + .and_raise(GRPC::NotFound) + expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) + end - it 'wraps GRPC exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) - .and_raise(GRPC::Unknown) - expect { subject }.to raise_error(Gitlab::Git::CommandError) - end + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + .and_raise(GRPC::Unknown) + expect { subject }.to raise_error(Gitlab::Git::CommandError) end end @@ -1281,42 +1256,32 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) end - context 'with gitaly enabled' do - before do - stub_gitaly - end - - after do - Gitlab::GitalyClient.clear_stubs! - end - - it 'returns a Branch with UTF-8 fields' do - branches = @repo.local_branches.to_a - expect(branches.size).to be > 0 - utf_8 = Encoding.find('utf-8') - branches.each do |branch| - expect(branch.name.encoding).to eq(utf_8) - expect(branch.target.encoding).to eq(utf_8) unless branch.target.nil? - end + it 'returns a Branch with UTF-8 fields' do + branches = @repo.local_branches.to_a + expect(branches.size).to be > 0 + utf_8 = Encoding.find('utf-8') + branches.each do |branch| + expect(branch.name.encoding).to eq(utf_8) + expect(branch.target.encoding).to eq(utf_8) unless branch.target.nil? end + end - it 'gets the branches from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) - .and_return([]) - @repo.local_branches - end + it 'gets the branches from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) + .and_return([]) + @repo.local_branches + end - it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) - .and_raise(GRPC::NotFound) - expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository) - end + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) + .and_raise(GRPC::NotFound) + expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository) + end - it 'wraps GRPC exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) - .and_raise(GRPC::Unknown) - expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError) - end + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) + .and_raise(GRPC::Unknown) + expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError) end end @@ -1395,11 +1360,4 @@ describe Gitlab::Git::Repository, seed_helper: true do sha = Rugged::Commit.create(repo, options) repo.lookup(sha) end - - def stub_gitaly - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) - - stub = double(:stub) - allow(Gitaly::Ref::Stub).to receive(:new).and_return(stub) - end end diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb index 8ad39a02b93..986ae348652 100644 --- a/spec/lib/gitlab/gitaly_client/ref_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -6,10 +6,6 @@ describe Gitlab::GitalyClient::Ref do let(:relative_path) { project.path_with_namespace + '.git' } let(:client) { described_class.new(project.repository) } - before do - allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) - end - after do # When we say `expect_any_instance_of(Gitaly::Ref::Stub)` a double is created, # and because GitalyClient shares stubs these will get passed from example to diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index b0635c6a90a..0a2cd8c2957 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -120,28 +120,17 @@ describe Environment, models: true do let(:head_commit) { project.commit } let(:commit) { project.commit.parent } - context 'Gitaly find_ref_name feature disabled' do - it 'returns deployment id for the environment' do - expect(environment.first_deployment_for(commit)).to eq deployment1 - end + it 'returns deployment id for the environment' do + expect(environment.first_deployment_for(commit)).to eq deployment1 + end - it 'return nil when no deployment is found' do - expect(environment.first_deployment_for(head_commit)).to eq nil - end + it 'return nil when no deployment is found' do + expect(environment.first_deployment_for(head_commit)).to eq nil end - # TODO: Uncomment when feature is reenabled - # context 'Gitaly find_ref_name feature enabled' do - # before do - # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true) - # end - # - # it 'calls GitalyClient' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name) - # - # environment.first_deployment_for(commit) - # end - # end + it 'returns a UTF-8 ref' do + expect(environment.first_deployment_for(commit).ref).to be_utf8 + end end describe '#environment_type' do -- cgit v1.2.1 From 8a62f304ef541b93ac47dab3b69b645f2b65537a Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 28 Jun 2017 17:44:37 +0200 Subject: Add a UTF-8 encoding matcher --- spec/lib/gitlab/git/blame_spec.rb | 3 +++ spec/lib/gitlab/git/branch_spec.rb | 2 +- spec/lib/gitlab/git/diff_spec.rb | 2 +- spec/lib/gitlab/git/repository_spec.rb | 11 +++++------ spec/support/matchers/be_utf8.rb | 9 +++++++++ 5 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 spec/support/matchers/be_utf8.rb diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 8b041ac69b1..66c016d14b3 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -20,6 +20,7 @@ describe Gitlab::Git::Blame, seed_helper: true do expect(data.size).to eq(95) expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) expect(data.first[:line]).to eq("# Contribute to GitLab") + expect(data.first[:line]).to be_utf8 end end @@ -40,6 +41,7 @@ describe Gitlab::Git::Blame, seed_helper: true do expect(data.size).to eq(1) expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) expect(data.first[:line]).to eq("Ä ü") + expect(data.first[:line]).to be_utf8 end end @@ -61,6 +63,7 @@ describe Gitlab::Git::Blame, seed_helper: true do expect(data.size).to eq(1) expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) expect(data.first[:line]).to eq(" ") + expect(data.first[:line]).to be_utf8 end end end diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 9dba4397e79..d1d7ed1d02a 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -48,7 +48,7 @@ describe Gitlab::Git::Branch, seed_helper: true do expect(Gitlab::Git::Commit).to receive(:decorate) .with(hash_including(attributes)).and_call_original - expect(branch.dereferenced_target.message.encoding).to be(Encoding::UTF_8) + expect(branch.dereferenced_target.message).to be_utf8 end end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index d50ccb0df30..d97e85364c2 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -180,7 +180,7 @@ EOT let(:raw_patch) { @raw_diff_hash[:diff].encode(Encoding::ASCII_8BIT) } it 'encodes diff patch to UTF-8' do - expect(diff.diff.encoding).to eq(Encoding::UTF_8) + expect(diff.diff).to be_utf8 end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 464cb41a842..9e924c2541b 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'returns UTF-8' do - expect(repository.root_ref.encoding).to eq(Encoding.find('UTF-8')) + expect(repository.root_ref).to be_utf8 end it 'gets the branch name from GitalyClient' do @@ -124,7 +124,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'returns UTF-8' do - expect(subject.first.encoding).to eq(Encoding.find('UTF-8')) + expect(subject.first).to be_utf8 end it { is_expected.to include("master") } @@ -158,7 +158,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'returns UTF-8' do - expect(subject.first.encoding).to eq(Encoding.find('UTF-8')) + expect(subject.first).to be_utf8 end describe '#last' do @@ -1259,10 +1259,9 @@ describe Gitlab::Git::Repository, seed_helper: true do it 'returns a Branch with UTF-8 fields' do branches = @repo.local_branches.to_a expect(branches.size).to be > 0 - utf_8 = Encoding.find('utf-8') branches.each do |branch| - expect(branch.name.encoding).to eq(utf_8) - expect(branch.target.encoding).to eq(utf_8) unless branch.target.nil? + expect(branch.name).to be_utf8 + expect(branch.target).to be_utf8 unless branch.target.nil? end end diff --git a/spec/support/matchers/be_utf8.rb b/spec/support/matchers/be_utf8.rb new file mode 100644 index 00000000000..ea806352422 --- /dev/null +++ b/spec/support/matchers/be_utf8.rb @@ -0,0 +1,9 @@ +RSpec::Matchers.define :be_utf8 do |_| + match do |actual| + actual.is_a?(String) && actual.encoding == Encoding.find('UTF-8') + end + + description do + "be a String with encoding UTF-8" + end +end -- cgit v1.2.1 From 9da3076944146444cb864d5db066a766c76b1935 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Fri, 30 Jun 2017 14:47:53 +0200 Subject: Improve support for external issue references --- .../concerns/mentionable/reference_regexes.rb | 2 +- app/models/external_issue.rb | 5 ---- app/models/project.rb | 4 +-- .../project_services/issue_tracker_service.rb | 5 +++- app/models/project_services/jira_service.rb | 2 +- .../adam-external-issue-references-spike.yml | 4 +++ doc/integration/external-issue-tracker.md | 3 ++ doc/user/project/integrations/bugzilla.md | 11 ++++++++ doc/user/project/integrations/redmine.md | 11 ++++++++ lib/banzai/filter/abstract_reference_filter.rb | 15 +--------- .../filter/external_issue_reference_filter.rb | 4 ++- lib/banzai/filter/issue_reference_filter.rb | 32 +-------------------- lib/banzai/reference_parser/issue_parser.rb | 3 -- spec/factories/projects.rb | 2 +- .../filter/external_issue_reference_filter_spec.rb | 4 +-- .../banzai/filter/issue_reference_filter_spec.rb | 25 ---------------- spec/lib/banzai/pipeline/gfm_pipeline_spec.rb | 33 ++++++++++++++++++++++ .../banzai/reference_parser/issue_parser_spec.rb | 10 ------- spec/models/project_services/jira_service_spec.rb | 6 ++-- .../project_services/redmine_service_spec.rb | 4 +-- spec/services/git_push_service_spec.rb | 12 -------- .../issue_tracker_service_shared_example.rb | 8 +++--- 22 files changed, 87 insertions(+), 118 deletions(-) create mode 100644 changelogs/unreleased/adam-external-issue-references-spike.yml create mode 100644 spec/lib/banzai/pipeline/gfm_pipeline_spec.rb diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb index 1848230ec7e..2d86a70c395 100644 --- a/app/models/concerns/mentionable/reference_regexes.rb +++ b/app/models/concerns/mentionable/reference_regexes.rb @@ -14,7 +14,7 @@ module Mentionable end EXTERNAL_PATTERN = begin - issue_pattern = ExternalIssue.reference_pattern + issue_pattern = IssueTrackerService.reference_pattern link_patterns = URI.regexp(%w(http https)) reference_pattern(link_patterns, issue_pattern) end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index e63f89a9f85..0bf18e529f0 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -38,11 +38,6 @@ class ExternalIssue @project.id end - # Pattern used to extract `JIRA-123` issue references from text - def self.reference_pattern - @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} - end - def to_reference(_from_project = nil, full: nil) id end diff --git a/app/models/project.rb b/app/models/project.rb index a75c5209955..8140ed3763f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -727,8 +727,8 @@ class Project < ActiveRecord::Base end end - def issue_reference_pattern - issues_tracker.reference_pattern + def external_issue_reference_pattern + external_issue_tracker.class.reference_pattern end def default_issues_tracker? diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index ff138b9066d..fcc7c4bec06 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -5,7 +5,10 @@ class IssueTrackerService < Service # Pattern used to extract links from comments # Override this method on services that uses different patterns - def reference_pattern + # This pattern does not support cross-project references + # The other code assumes that this pattern is a superset of all + # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN + def self.reference_pattern @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?\d+)} end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2450fb43212..00328892b4a 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -18,7 +18,7 @@ class JiraService < IssueTrackerService end # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 - def reference_pattern + def self.reference_pattern @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} end diff --git a/changelogs/unreleased/adam-external-issue-references-spike.yml b/changelogs/unreleased/adam-external-issue-references-spike.yml new file mode 100644 index 00000000000..aeec6688425 --- /dev/null +++ b/changelogs/unreleased/adam-external-issue-references-spike.yml @@ -0,0 +1,4 @@ +--- +title: Improve support for external issue references +merge_request: 12485 +author: diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 265c891cf83..2dd9b33273c 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -8,6 +8,9 @@ you to do the following: issue index of the external tracker - clicking **New issue** on the project dashboard creates a new issue on the external tracker +- you can reference these external issues inside GitLab interface + (merge requests, commits, comments) and they will be automatically converted + into links ## Configuration diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md index 0b219e84478..6a040516231 100644 --- a/doc/user/project/integrations/bugzilla.md +++ b/doc/user/project/integrations/bugzilla.md @@ -16,3 +16,14 @@ Once you have configured and enabled Bugzilla: - the **Issues** link on the GitLab project pages takes you to the appropriate Bugzilla product page - clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue + +## Referencing issues in Bugzilla + +Issues in Bugzilla can be referenced in two alternative ways: +1. `#` where `` is a number (example `#143`) +2. `-` where `` starts with a capital letter which is + then followed by capital letters, numbers or underscores, and `` is + a number (example `API_32-143`). + +Please note that `` part is ignored and links always point to the +address specified in `issues_url`. diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index 89c0312d3c2..8026f1f57bc 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -21,3 +21,14 @@ Once you have configured and enabled Redmine: As an example, below is a configuration for a project named gitlab-ci. ![Redmine configuration](img/redmine_configuration.png) + +## Referencing issues in Redmine + +Issues in Redmine can be referenced in two alternative ways: +1. `#` where `` is a number (example `#143`) +2. `-` where `` starts with a capital letter which is + then followed by capital letters, numbers or underscores, and `` is + a number (example `API_32-143`). + +Please note that `` part is ignored and links always point to the +address specified in `issues_url`. diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 8bc2dd18bda..7a262dd025c 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -216,12 +216,7 @@ module Banzai @references_per_project ||= begin refs = Hash.new { |hash, key| hash[key] = Set.new } - regex = - if uses_reference_pattern? - Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern) - else - object_class.link_reference_pattern - end + regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern) nodes.each do |node| node.to_html.scan(regex) do @@ -323,14 +318,6 @@ module Banzai value end end - - # There might be special cases like filters - # that should ignore reference pattern - # eg: IssueReferenceFilter when using a external issues tracker - # In those cases this method should be overridden on the filter subclass - def uses_reference_pattern? - true - end end end end diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index dce4de3ceaf..53a229256a5 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -3,6 +3,8 @@ module Banzai # HTML filter that replaces external issue tracker references with links. # References are ignored if the project doesn't use an external issue # tracker. + # + # This filter does not support cross-project references. class ExternalIssueReferenceFilter < ReferenceFilter self.reference_type = :external_issue @@ -87,7 +89,7 @@ module Banzai end def issue_reference_pattern - external_issues_cached(:issue_reference_pattern) + external_issues_cached(:external_issue_reference_pattern) end private diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 044d18ff824..ba1a5ac84b3 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -15,10 +15,6 @@ module Banzai Issue end - def uses_reference_pattern? - context[:project].default_issues_tracker? - end - def find_object(project, iid) issues_per_project[project][iid] end @@ -38,13 +34,7 @@ module Banzai projects_per_reference.each do |path, project| issue_ids = references_per_project[path] - - issues = - if project.default_issues_tracker? - project.issues.where(iid: issue_ids.to_a) - else - issue_ids.map { |id| ExternalIssue.new(id, project) } - end + issues = project.issues.where(iid: issue_ids.to_a) issues.each do |issue| hash[project][issue.iid.to_i] = issue @@ -55,26 +45,6 @@ module Banzai end end - def object_link_title(object) - if object.is_a?(ExternalIssue) - "Issue in #{object.project.external_issue_tracker.title}" - else - super - end - end - - def data_attributes_for(text, project, object, link: false) - if object.is_a?(ExternalIssue) - data_attribute( - project: project.id, - external_issue: object.id, - reference_type: ExternalIssueReferenceFilter.reference_type - ) - else - super - end - end - def projects_relation_for_paths(paths) super(paths).includes(:gitlab_issue_tracker_service) end diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index 9fd4bd68d43..a65bbe23958 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -4,9 +4,6 @@ module Banzai self.reference_type = :issue def nodes_visible_to_user(user, nodes) - # It is not possible to check access rights for external issue trackers - return nodes if project && project.external_issue_tracker - issues = issues_for_nodes(nodes) readable_issues = Ability diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index aef1c17a239..1bb2db11e7f 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -220,7 +220,7 @@ FactoryGirl.define do active: true, properties: { 'project_url' => 'http://redmine/projects/project_name_in_redmine', - 'issues_url' => "http://redmine/#{project.id}/project_name_in_redmine/:id", + 'issues_url' => 'http://redmine/projects/project_name_in_redmine/issues/:id', 'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new' } ) diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index a4bb043f8f1..b7d82c36ddd 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -88,12 +88,12 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do it 'queries the collection on the first call' do expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original - expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original + expect_any_instance_of(Project).to receive(:external_issue_reference_pattern).once.and_call_original not_cached = reference_filter.call("look for #{reference}", { project: project }) expect_any_instance_of(Project).not_to receive(:default_issues_tracker?) - expect_any_instance_of(Project).not_to receive(:issue_reference_pattern) + expect_any_instance_of(Project).not_to receive(:external_issue_reference_pattern) cached = reference_filter.call("look for #{reference}", { project: project }) diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index e5c1deb338b..a79d365d6c5 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -39,13 +39,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do let(:reference) { "##{issue.iid}" } - it 'ignores valid references when using non-default tracker' do - allow(project).to receive(:default_issues_tracker?).and_return(false) - - exp = act = "Issue #{reference}" - expect(reference_filter(act).to_html).to eq exp - end - it 'links to a valid reference' do doc = reference_filter("Fixed #{reference}") @@ -340,24 +333,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do .to eq({ project => { issue.iid => issue } }) end end - - context 'using an external issue tracker' do - it 'returns a Hash containing the issues per project' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(project).to receive(:default_issues_tracker?).and_return(false) - - expect(filter).to receive(:projects_per_reference) - .and_return({ project.path_with_namespace => project }) - - expect(filter).to receive(:references_per_project) - .and_return({ project.path_with_namespace => Set.new([1]) }) - - expect(filter.issues_per_project[project][1]) - .to be_an_instance_of(ExternalIssue) - end - end end describe '.references_in' do diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb new file mode 100644 index 00000000000..2b8c76f2bb8 --- /dev/null +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +describe Banzai::Pipeline::GfmPipeline do + describe 'integration between parsing regular and external issue references' do + let(:project) { create(:redmine_project, :public) } + + it 'allows to use shorthand external reference syntax for Redmine' do + markdown = '#12' + + result = described_class.call(markdown, project: project)[:output] + link = result.css('a').first + + expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12' + end + + it 'parses cross-project references to regular issues' do + other_project = create(:empty_project, :public) + issue = create(:issue, project: other_project) + markdown = issue.to_reference(project, full: true) + + result = described_class.call(markdown, project: project)[:output] + link = result.css('a').first + + expect(link['href']).to eq( + Gitlab::Routing.url_helpers.namespace_project_issue_path( + other_project.namespace, + other_project, + issue + ) + ) + end + end +end diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 58e1a0c1bc1..acdd23f81f3 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -39,16 +39,6 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end end - - context 'when the project uses an external issue tracker' do - it 'returns all nodes' do - link = double(:link) - - expect(project).to receive(:external_issue_tracker).and_return(true) - - expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) - end - end end describe '#referenced_by' do diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index c86f56c55eb..4a1de76f099 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -64,12 +64,12 @@ describe JiraService, models: true do end end - describe '#reference_pattern' do + describe '.reference_pattern' do it_behaves_like 'allows project key on reference pattern' it 'does not allow # on the code' do - expect(subject.reference_pattern.match('#123')).to be_nil - expect(subject.reference_pattern.match('1#23#12')).to be_nil + expect(described_class.reference_pattern.match('#123')).to be_nil + expect(described_class.reference_pattern.match('1#23#12')).to be_nil end end diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb index 6631d9040b1..441b3f896ca 100644 --- a/spec/models/project_services/redmine_service_spec.rb +++ b/spec/models/project_services/redmine_service_spec.rb @@ -31,11 +31,11 @@ describe RedmineService, models: true do end end - describe '#reference_pattern' do + describe '.reference_pattern' do it_behaves_like 'allows project key on reference pattern' it 'does allow # on the reference' do - expect(subject.reference_pattern.match('#123')[:issue]).to eq('123') + expect(described_class.reference_pattern.match('#123')[:issue]).to eq('123') end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index ca827fc0f39..8e8816870e1 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -401,18 +401,6 @@ describe GitPushService, services: true do expect(SystemNoteService).not_to receive(:cross_reference) execute_service(project, commit_author, @oldrev, @newrev, @ref ) end - - it "doesn't close issues when external issue tracker is in use" do - allow_any_instance_of(Project).to receive(:default_issues_tracker?) - .and_return(false) - external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern) - allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker) - - # The push still shouldn't create cross-reference notes. - expect do - execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' ) - end.not_to change { Note.where(project_id: project.id, system: true).count } - end end context "to non-default branches" do diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb index e70b3963d9d..a6ab03cb808 100644 --- a/spec/support/issue_tracker_service_shared_example.rb +++ b/spec/support/issue_tracker_service_shared_example.rb @@ -8,15 +8,15 @@ end RSpec.shared_examples 'allows project key on reference pattern' do |url_attr| it 'allows underscores in the project name' do - expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end it 'allows numbers in the project name' do - expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' + expect(described_class.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' end it 'requires the project name to begin with A-Z' do - expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil - expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(described_class.reference_pattern.match('3EXT_EXT-1234')).to eq nil + expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end end -- cgit v1.2.1 From adc00877adff3a3b351cf4310948ec71b6a161dc Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 29 Jun 2017 14:31:50 +0200 Subject: Allow developers to have custom rspec output settings --- .gitignore | 1 + .rspec | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .rspec diff --git a/.gitignore b/.gitignore index e529e33530a..0d6194dd1e5 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ eslint-report.html /.gitlab_workhorse_secret /webpack-report/ /locale/**/LC_MESSAGES +/.rspec diff --git a/.rspec b/.rspec deleted file mode 100644 index 35f4d7441e0..00000000000 --- a/.rspec +++ /dev/null @@ -1,2 +0,0 @@ ---color ---format Fuubar -- cgit v1.2.1 From b3498d88c1dd8e1c0d6880383148a3d818fd1145 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 30 Jun 2017 15:03:35 +0200 Subject: Use Gitaly 0.14.0 --- GITALY_SERVER_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 54d1a4f2a4a..a803cc227fe 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.13.0 +0.14.0 -- cgit v1.2.1 From 3c88a7869b87693ba8c3fb9814d39437dd569a31 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 29 Jun 2017 07:43:41 +0000 Subject: Implement review comments for !12445 from @godfat and @rymai. - Use `GlobalPolicy` to authorize the users that a non-authenticated user can fetch from `/api/v4/users`. We allow access if the `Gitlab::VisibilityLevel::PUBLIC` visibility level is not restricted. - Further, as before, `/api/v4/users` is only accessible to unauthenticated users if the `username` parameter is passed. - Turn off `authenticate!` for the `/api/v4/users` endpoint by matching on the actual route + method, rather than the description. - Change the type of `current_user` check in `UsersFinder` to be more compatible with EE. --- app/finders/users_finder.rb | 11 ++++------- app/policies/base_policy.rb | 6 ++++++ app/policies/global_policy.rb | 3 ++- app/policies/user_policy.rb | 6 ------ lib/api/helpers.rb | 4 ++-- lib/api/users.rb | 26 +++++++++++--------------- spec/requests/api/users_spec.rb | 2 +- 7 files changed, 26 insertions(+), 32 deletions(-) diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 0534317df8f..07deceb827b 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -27,11 +27,8 @@ class UsersFinder users = by_search(users) users = by_blocked(users) users = by_active(users) - - if current_user - users = by_external_identity(users) - users = by_external(users) - end + users = by_external_identity(users) + users = by_external(users) users end @@ -63,13 +60,13 @@ class UsersFinder end def by_external_identity(users) - return users unless current_user.admin? && params[:extern_uid] && params[:provider] + return users unless current_user&.admin? && params[:extern_uid] && params[:provider] users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) end def by_external(users) - return users = users.where.not(external: true) unless current_user.admin? + return users = users.where.not(external: true) unless current_user&.admin? return users unless params[:external] users.external diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 623424c63e0..261a2e780c5 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -1,4 +1,6 @@ class BasePolicy + include Gitlab::CurrentSettings + class RuleSet attr_reader :can_set, :cannot_set def initialize(can_set, cannot_set) @@ -124,4 +126,8 @@ class BasePolicy yield @rule_set end + + def restricted_public_level? + current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) + end end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 2683aaad981..e9be43a5037 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -1,9 +1,10 @@ class GlobalPolicy < BasePolicy def rules + can! :read_users_list unless restricted_public_level? + return unless @user can! :create_group if @user.can_create_group - can! :read_users_list unless @user.blocked? || @user.internal? can! :log_in unless @user.access_locked? diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 229846e368c..265c56aba53 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,6 +1,4 @@ class UserPolicy < BasePolicy - include Gitlab::CurrentSettings - def rules can! :read_user if @user || !restricted_public_level? @@ -12,8 +10,4 @@ class UserPolicy < BasePolicy cannot! :destroy_user if @subject.ghost? end end - - def restricted_public_level? - current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) - end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 1322afaa64f..a3aec8889d7 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -410,8 +410,8 @@ module API # Does the current route match the route identified by # `description`? - def route_matches_description?(description) - options.dig(:route_options, :description) == description + def request_matches_route?(method, route) + request.request_method == method && request.path == route end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index 34619c90d8b..18ce58299e7 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -4,7 +4,7 @@ module API before do allow_access_with_scope :read_user if request.get? - authenticate! unless route_matches_description?("Get the list of users") + authenticate! unless request_matches_route?('GET', '/api/v4/users') end resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do @@ -55,22 +55,18 @@ module API users = UsersFinder.new(current_user, params).execute - authorized = - if current_user - can?(current_user, :read_users_list) - else - # When `current_user` is not present, require that the `username` - # parameter is passed, to prevent an unauthenticated user from accessing - # a list of all the users on the GitLab instance. `UsersFinder` performs - # an exact match on the `username` parameter, so we are guaranteed to - # get either 0 or 1 `users` here. - params[:username].present? && - users.all? { |user| can?(current_user, :read_user, user) } - end + authorized = can?(current_user, :read_users_list) + + # When `current_user` is not present, require that the `username` + # parameter is passed, to prevent an unauthenticated user from accessing + # a list of all the users on the GitLab instance. `UsersFinder` performs + # an exact match on the `username` parameter, so we are guaranteed to + # get either 0 or 1 `users` here. + authorized &&= params[:username].present? if current_user.blank? - render_api_error!("Not authorized.", 403) unless authorized + forbidden!("Not authorized to access /api/v4/users") unless authorized - entity = current_user.try(:admin?) ? Entities::UserWithAdmin : Entities::UserBasic + entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic present paginate(users), with: entity end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 01541901330..bf7ed2d3ad6 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -34,7 +34,7 @@ describe API::Users do it "returns authorization error when the `username` parameter refers to an inaccessible user" do user = create(:user) - expect(Ability).to receive(:allowed?).with(nil, :read_user, user).and_return(false) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) get api("/users"), username: user.username -- cgit v1.2.1 From 2d52a628867c4c718413d0b93835f89b084d1693 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 30 Jun 2017 13:20:09 +0000 Subject: Resolve "More actions dropdown hidden by end of diff" --- app/assets/stylesheets/pages/diff.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 631649b363f..55011e8a21b 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -20,8 +20,6 @@ } .diff-content { - overflow: auto; - overflow-y: hidden; background: $white-light; color: $gl-text-color; border-radius: 0 0 3px 3px; -- cgit v1.2.1 From 1a449b24a22283aeffa08603d2cff00db10d7fe5 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 30 Jun 2017 14:35:31 +0100 Subject: Update CHANGELOG.md for 9.3.3 [ci skip] --- CHANGELOG.md | 9 +++++++++ changelogs/unreleased/dm-dependency-linker-newlines.yml | 5 ----- changelogs/unreleased/fix-34417.yml | 4 ---- changelogs/unreleased/fix-head-pipeline-for-commit-status.yml | 4 ---- changelogs/unreleased/highest-return-on-diff-investment.yml | 4 ---- changelogs/unreleased/issue-boards-closed-list-all.yml | 4 ---- changelogs/unreleased/issue-form-multiple-line-markdown.yml | 4 ---- 7 files changed, 9 insertions(+), 25 deletions(-) delete mode 100644 changelogs/unreleased/dm-dependency-linker-newlines.yml delete mode 100644 changelogs/unreleased/fix-34417.yml delete mode 100644 changelogs/unreleased/fix-head-pipeline-for-commit-status.yml delete mode 100644 changelogs/unreleased/highest-return-on-diff-investment.yml delete mode 100644 changelogs/unreleased/issue-boards-closed-list-all.yml delete mode 100644 changelogs/unreleased/issue-form-multiple-line-markdown.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index f372cbf91e8..7591559da22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.3.3 (2017-06-30) + +- Fix head pipeline stored in merge request for external pipelines. !12478 +- Bring back branches badge to main project page. !12548 +- Fix diff of requirements.txt file by not matching newlines as part of package names. +- Perform housekeeping only when an import of a fresh project is completed. +- Fixed issue boards closed list not showing all closed issues. +- Fixed multi-line markdown tooltip buttons in issue edit form. + ## 9.3.2 (2017-06-27) - API: Fix optional arugments for POST :id/variables. !12474 diff --git a/changelogs/unreleased/dm-dependency-linker-newlines.yml b/changelogs/unreleased/dm-dependency-linker-newlines.yml deleted file mode 100644 index 5631095fcb7..00000000000 --- a/changelogs/unreleased/dm-dependency-linker-newlines.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix diff of requirements.txt file by not matching newlines as part of package - names -merge_request: -author: diff --git a/changelogs/unreleased/fix-34417.yml b/changelogs/unreleased/fix-34417.yml deleted file mode 100644 index 5f012ad0c81..00000000000 --- a/changelogs/unreleased/fix-34417.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Perform housekeeping only when an import of a fresh project is completed -merge_request: -author: diff --git a/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml b/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml deleted file mode 100644 index f12e7b53790..00000000000 --- a/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix head pipeline stored in merge request for external pipelines -merge_request: 12478 -author: diff --git a/changelogs/unreleased/highest-return-on-diff-investment.yml b/changelogs/unreleased/highest-return-on-diff-investment.yml deleted file mode 100644 index c8be1e0ff8f..00000000000 --- a/changelogs/unreleased/highest-return-on-diff-investment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bring back branches badge to main project page -merge_request: 12548 -author: diff --git a/changelogs/unreleased/issue-boards-closed-list-all.yml b/changelogs/unreleased/issue-boards-closed-list-all.yml deleted file mode 100644 index 7643864150d..00000000000 --- a/changelogs/unreleased/issue-boards-closed-list-all.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards closed list not showing all closed issues -merge_request: -author: diff --git a/changelogs/unreleased/issue-form-multiple-line-markdown.yml b/changelogs/unreleased/issue-form-multiple-line-markdown.yml deleted file mode 100644 index 23128f346bc..00000000000 --- a/changelogs/unreleased/issue-form-multiple-line-markdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed multi-line markdown tooltip buttons in issue edit form -merge_request: -author: -- cgit v1.2.1 From 728be6af7c46bf57f2d4a3dd2f1d5dd9ec23bd4b Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Sun, 18 Jun 2017 22:30:58 -0400 Subject: Make setSidebarHeight more efficient with SidebarHeightManager. --- .../javascripts/issuable_bulk_update_sidebar.js | 26 ++--------------- app/assets/javascripts/right_sidebar.js | 25 ++-------------- app/assets/javascripts/sidebar_height_manager.js | 33 ++++++++++++++++++++++ 3 files changed, 38 insertions(+), 46 deletions(-) create mode 100644 app/assets/javascripts/sidebar_height_manager.js diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index a8856120c5e..4f376599ba9 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -5,6 +5,7 @@ /* global SubscriptionSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import SidebarHeightManager from './sidebar_height_manager'; const HIDDEN_CLASS = 'hidden'; const DISABLED_CONTENT_CLASS = 'disabled-content'; @@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar { return navbarHeight + layoutNavHeight + subNavScroll; } - initSidebar() { - if (!this.navHeight) { - this.navHeight = this.getNavHeight(); - } - - if (!this.sidebarInitialized) { - $(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this)); - $(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this)); - this.sidebarInitialized = true; - } - } - setupBulkUpdateActions() { IssuableBulkUpdateActions.setOriginalDropdownData(); } @@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar { this.toggleCheckboxDisplay(enable); if (enable) { - this.initSidebar(); + SidebarHeightManager.init(); } } @@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar { this.$bulkEditSubmitBtn.enable(); } } - // loosely based on method of the same name in right_sidebar.js - setSidebarHeight() { - const currentScrollDepth = window.pageYOffset || 0; - const diff = this.navHeight - currentScrollDepth; - - if (diff > 0) { - this.$sidebar.outerHeight(window.innerHeight - diff); - } else { - this.$sidebar.outerHeight('100%'); - } - } static getCheckedIssueIds() { const $checkedIssues = $('.selected_issue:checked'); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b5cd01044a3..d8f1fe10b26 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */ import Cookies from 'js-cookie'; +import SidebarHeightManager from './sidebar_height_manager'; (function() { this.Sidebar = (function() { @@ -8,12 +9,6 @@ import Cookies from 'js-cookie'; this.toggleTodo = this.toggleTodo.bind(this); this.sidebar = $('aside'); - this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); - this.$navGitlab = $('.navbar-gitlab'); - this.$layoutNav = $('.layout-nav'); - this.$subScroll = $('.sub-nav-scroll'); - this.$rightSidebar = $('.js-right-sidebar'); - this.removeListeners(); this.addEventListeners(); } @@ -27,16 +22,14 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.addEventListeners = function() { + SidebarHeightManager.init(); const $document = $(document); - const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); - const slowerThrottledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 200); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); - $(window).on('resize', () => throttledSetSidebarHeight()); - $document.on('scroll', () => slowerThrottledSetSidebarHeight()); + $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); @@ -214,18 +207,6 @@ import Cookies from 'js-cookie'; } }; - Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0); - const diff = $navHeight - $(window).scrollTop(); - if (diff > 0) { - this.$rightSidebar.outerHeight($(window).height() - diff); - this.$sidebarInner.height('100%'); - } else { - this.$rightSidebar.outerHeight('100%'); - this.$sidebarInner.height(''); - } - }; - Sidebar.prototype.isOpen = function() { return this.sidebar.is('.right-sidebar-expanded'); }; diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js new file mode 100644 index 00000000000..022415f22b2 --- /dev/null +++ b/app/assets/javascripts/sidebar_height_manager.js @@ -0,0 +1,33 @@ +export default { + init() { + if (!this.initialized) { + this.$window = $(window); + this.$rightSidebar = $('.js-right-sidebar'); + this.$navHeight = $('.navbar-gitlab').outerHeight() + + $('.layout-nav').outerHeight() + + $('.sub-nav-scroll').outerHeight(); + + const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20); + const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200); + + this.$window.on('scroll', throttledSetSidebarHeight); + this.$window.on('resize', debouncedSetSidebarHeight); + this.initialized = true; + } + }, + + setSidebarHeight() { + const currentScrollDepth = window.pageYOffset || 0; + const diff = this.$navHeight - currentScrollDepth; + + if (diff > 0) { + const newSidebarHeight = window.innerHeight - diff; + this.$rightSidebar.outerHeight(newSidebarHeight); + this.sidebarHeightIsCustom = true; + } else if (this.sidebarHeightIsCustom) { + this.$rightSidebar.outerHeight('100%'); + this.sidebarHeightIsCustom = false; + } + }, +}; + -- cgit v1.2.1 From 9e3ef082be2595921319279ec095c2765a66e9e9 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 30 Jun 2017 14:10:09 +0000 Subject: Remove placeholder note when award emoji slash command is applied --- app/assets/javascripts/notes.js | 4 ++++ spec/javascripts/notes_spec.js | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 46d77b31ffd..194d1730f3d 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -337,6 +337,10 @@ export default class Notes { if (!noteEntity.valid) { if (noteEntity.errors.commands_only) { + if (noteEntity.commands_changes && + Object.keys(noteEntity.commands_changes).length > 0) { + $notesList.find('.system-note.being-posted').remove(); + } this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); this.refresh(); } diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 5ece4ed080b..2c096ed08a8 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -523,6 +523,51 @@ import '~/notes'; }); }); + describe('postComment with Slash commands', () => { + const sampleComment = '/assign @root\n/award :100:'; + const note = { + commands_changes: { + assignee_id: 1, + emoji_award: '100' + }, + errors: { + commands_only: ['Commands applied'] + }, + valid: false + }; + let $form; + let $notesContainer; + + beforeEach(() => { + this.notes = new Notes('', []); + window.gon.current_username = 'root'; + window.gon.current_user_fullname = 'Administrator'; + gl.awardsHandler = { + addAwardToEmojiBar: () => {}, + scrollToAwards: () => {} + }; + gl.GfmAutoComplete = { + dataSources: { + commands: '/root/test-project/autocomplete_sources/commands' + } + }; + $form = $('form.js-main-target-form'); + $notesContainer = $('ul.main-notes-list'); + $form.find('textarea.js-note-text').val(sampleComment); + }); + + it('should remove slash command placeholder when comment with slash commands is done posting', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough(); + $('.js-comment-button').click(); + + expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown + deferred.resolve(note); + expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed + }); + }); + describe('update comment with script tags', () => { const sampleComment = ''; const updatedComment = ''; -- cgit v1.2.1 From 9c8075c4b95f090fc6f00c897f6bf097d29ee8bf Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 29 Jun 2017 23:26:23 -0700 Subject: Make Project#ensure_repository force create a repo In Geo, Project#ensure_repository is used to make sure that a Git repo is available to be fetched on a secondary. If a project were a fork, this directory would never be created. Closes gitlab-org/gitlab-ee#2800 --- app/models/project.rb | 20 ++++++++++---------- changelogs/unreleased/sh-allow-force-repo-create.yml | 4 ++++ spec/models/project_spec.rb | 13 +++++++++++++ 3 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/sh-allow-force-repo-create.yml diff --git a/app/models/project.rb b/app/models/project.rb index a75c5209955..21f4a18ec4a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1073,21 +1073,21 @@ class Project < ActiveRecord::Base merge_requests.where(source_project_id: self.id) end - def create_repository + def create_repository(force = false) # Forked import is handled asynchronously - unless forked? - if gitlab_shell.add_repository(repository_storage_path, path_with_namespace) - repository.after_create - true - else - errors.add(:base, 'Failed to create repository via gitlab-shell') - false - end + return if forked? && !force + + if gitlab_shell.add_repository(repository_storage_path, path_with_namespace) + repository.after_create + true + else + errors.add(:base, 'Failed to create repository via gitlab-shell') + false end end def ensure_repository - create_repository unless repository_exists? + create_repository(true) unless repository_exists? end def repository_exists? diff --git a/changelogs/unreleased/sh-allow-force-repo-create.yml b/changelogs/unreleased/sh-allow-force-repo-create.yml new file mode 100644 index 00000000000..2a65ba807bb --- /dev/null +++ b/changelogs/unreleased/sh-allow-force-repo-create.yml @@ -0,0 +1,4 @@ +--- +title: Make Project#ensure_repository force create a repo +merge_request: +author: diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1390848ff4a..0eeaf68a02a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1357,6 +1357,19 @@ describe Project, models: true do project.ensure_repository end + + it 'creates the repository if it is a fork' do + expect(project).to receive(:forked?).and_return(true) + + allow(project).to receive(:repository_exists?) + .and_return(false) + + expect(shell).to receive(:add_repository) + .with(project.repository_storage_path, project.path_with_namespace) + .and_return(true) + + project.ensure_repository + end end describe '#user_can_push_to_empty_repo?' do -- cgit v1.2.1 From 4e38985b1cc159c4e1582ab34c29d7ec151aef35 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 30 Jun 2017 15:44:21 +0100 Subject: Fix typo in IssuesFinder comment [ci skip] --- app/finders/issues_finder.rb | 2 +- changelogs/unreleased/speed-up-issue-counting-for-a-project.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 18f60f9a2b6..85230ff1293 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -60,7 +60,7 @@ class IssuesFinder < IssuableFinder # `user_can_see_all_confidential_issues?`) are more complicated, because they # can see confidential issues where: # 1. They are an assignee. - # 2. The are an author. + # 2. They are an author. # # That's fine for most cases, but if we're just counting, we need to cache # effectively. If we cached this accurately, we'd have a cache key for every diff --git a/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml index 493ecbcb77a..6bf03d9a382 100644 --- a/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml +++ b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml @@ -1,5 +1,5 @@ --- title: Cache open issue and merge request counts for project tabs to speed up project pages -merge_request: +merge_request: 12457 author: -- cgit v1.2.1 From 7541362fe7b8983f1c6ce511dc9b7c727857dfdd Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 30 Jun 2017 15:48:42 +0000 Subject: Automatically hide sidebar on smaller screens --- app/assets/stylesheets/new_sidebar.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index be4cc02b3ea..17f23f7fce3 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -49,6 +49,7 @@ $new-sidebar-width: 220px; position: fixed; z-index: 400; width: $new-sidebar-width; + transition: width $sidebar-transition-duration; top: 50px; bottom: 0; left: 0; @@ -62,6 +63,8 @@ $new-sidebar-width: 220px; } li { + white-space: nowrap; + a { display: block; padding: 12px 14px; @@ -72,6 +75,10 @@ $new-sidebar-width: 220px; color: $gl-text-color; text-decoration: none; } + + @media (max-width: $screen-xs-max) { + width: 0; + } } .sidebar-sub-level-items { -- cgit v1.2.1 From 1b0c6ffd512d0bee24964936da02e1b10d6a5a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 29 Jun 2017 18:03:17 +0200 Subject: Disable RSpec/BeforeAfterAll and enable RSpec/ImplicitExpect cops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .rubocop.yml | 10 ++++++++++ .rubocop_todo.yml | 10 ---------- spec/models/project_group_link_spec.rb | 12 ++++++------ spec/models/project_services/external_wiki_service_spec.rb | 4 ++-- spec/models/project_spec.rb | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 32ec60f540b..9785e7626f9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -965,6 +965,10 @@ RSpec/AnyInstance: RSpec/BeEql: Enabled: true +# We don't enforce this as we use this technique in a few places. +RSpec/BeforeAfterAll: + Enabled: false + # Check that the first argument to the top level describe is the tested class or # module. RSpec/DescribeClass: @@ -1024,6 +1028,12 @@ RSpec/FilePath: RSpec/Focus: Enabled: true +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: is_expected, should +RSpec/ImplicitExpect: + Enabled: true + EnforcedStyle: is_expected + # Checks for the usage of instance variables. RSpec/InstanceVariable: Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 5ab4692dd60..2ec558e274f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,10 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 54 -RSpec/BeforeAfterAll: - Enabled: false - # Offense count: 233 RSpec/EmptyLineAfterFinalLet: Enabled: false @@ -24,12 +20,6 @@ RSpec/EmptyLineAfterSubject: RSpec/HookArgument: Enabled: false -# Offense count: 12 -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: is_expected, should -RSpec/ImplicitExpect: - Enabled: false - # Offense count: 11 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: it_behaves_like, it_should_behave_like diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index 4161b9158b1..d68d8b719cd 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe ProjectGroupLink do describe "Associations" do - it { should belong_to(:group) } - it { should belong_to(:project) } + it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:project) } end describe "Validation" do @@ -12,10 +12,10 @@ describe ProjectGroupLink do let(:project) { create(:project, group: group) } let!(:project_group_link) { create(:project_group_link, project: project) } - it { should validate_presence_of(:project_id) } - it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) } - it { should validate_presence_of(:group) } - it { should validate_presence_of(:group_access) } + it { is_expected.to validate_presence_of(:project_id) } + it { is_expected.to validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) } + it { is_expected.to validate_presence_of(:group) } + it { is_expected.to validate_presence_of(:group_access) } it "doesn't allow a project to be shared with the group it is in" do project_group_link.group = group diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb index 291fc645a1c..ef10df9e092 100644 --- a/spec/models/project_services/external_wiki_service_spec.rb +++ b/spec/models/project_services/external_wiki_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe ExternalWikiService, models: true do include ExternalWikiHelper describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe 'Validations' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1390848ff4a..f9b702c54aa 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -823,13 +823,13 @@ describe Project, models: true do let(:avatar_path) { "/#{project.full_path}/avatar" } - it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } + it { is_expected.to eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } end context 'when git repo is empty' do let(:project) { create(:empty_project) } - it { should eq nil } + it { is_expected.to eq nil } end end -- cgit v1.2.1 From 2c68019b551ed1264a2dce835844fc4638f35119 Mon Sep 17 00:00:00 2001 From: Joe Marty Date: Fri, 30 Jun 2017 15:57:38 +0000 Subject: Fix curl example paths (missing the 'files' segment) --- doc/api/repository_files.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 18ceb8f779e..1fc577561a0 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -61,7 +61,7 @@ POST /projects/:id/repository/files/:file_path ``` ```bash -curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' +curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' ``` Example response: @@ -90,7 +90,7 @@ PUT /projects/:id/repository/files/:file_path ``` ```bash -curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' +curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' ``` Example response: @@ -129,7 +129,7 @@ DELETE /projects/:id/repository/files/:file_path ``` ```bash -curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' +curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' ``` Example response: -- cgit v1.2.1 From 912613c41be3790b004f65935e8380aea9e5895f Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 30 Jun 2017 09:00:07 -0700 Subject: Reduce 28 test runs to 4 14 to 2, but these shared examples are used twice. This was already done in another context further down the file. --- .../gitlab/health_checks/fs_shards_check_spec.rb | 36 ++++++++++++---------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index fbacbc4a338..b333e162909 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -103,30 +103,34 @@ describe Gitlab::HealthChecks::FsShardsCheck do example.run_with_retry retry: times_to_try end - it { is_expected.to all(have_attributes(labels: { shard: :default })) } - - it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) } - - it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) } + it 'provides metrics' do + expect(subject).to all(have_attributes(labels: { shard: :default })) + expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) + + expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) + end end context 'storage points to directory that has both read and write rights' do before do FileUtils.chmod_R(0755, tmp_dir) end - it { is_expected.to all(have_attributes(labels: { shard: :default })) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) } + it 'provides metrics' do + expect(subject).to all(have_attributes(labels: { shard: :default })) - it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) } + expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) + + expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) + end end end end -- cgit v1.2.1 From 4c20ee71a979121d88f90e475825fc03a01e3468 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Fri, 30 Jun 2017 12:20:21 -0400 Subject: Restore timeago translations in renderTimeago. --- app/assets/javascripts/lib/utils/datetime_utility.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 034a1ec2054..1d1763c3963 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -116,7 +116,7 @@ window.dateFormat = dateFormat; const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); // timeago.js sets timeouts internally for each timeago value to be updated in real time - gl.utils.getTimeago().render(timeagoEls); + gl.utils.getTimeago().render(timeagoEls, lang); }; w.gl.utils.getDayDifference = function(a, b) { -- cgit v1.2.1 From ec396fd93ab800e8b962f7b8bc6095f8ab754d93 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 30 Jun 2017 16:52:11 +0000 Subject: New navigation breadcrumbs --- app/assets/javascripts/group_name.js | 23 ++-- app/assets/stylesheets/new_nav.scss | 124 +++++++++++++++++++++ app/helpers/groups_helper.rb | 21 +++- app/helpers/projects_helper.rb | 12 +- app/views/dashboard/activity.html.haml | 1 + app/views/dashboard/groups/index.html.haml | 1 + app/views/dashboard/milestones/index.html.haml | 1 + app/views/dashboard/projects/index.html.haml | 2 + app/views/dashboard/snippets/index.html.haml | 1 + app/views/layouts/_page.html.haml | 2 + app/views/layouts/header/_default.html.haml | 2 +- app/views/layouts/header/_new.html.haml | 2 - app/views/layouts/nav/_breadcrumbs.html.haml | 19 ++++ app/views/projects/boards/_show.html.haml | 4 + app/views/projects/issues/_nav_btns.html.haml | 11 ++ app/views/projects/issues/index.html.haml | 19 +--- .../projects/merge_requests/_nav_btns.html.haml | 5 + app/views/projects/merge_requests/index.html.haml | 16 ++- spec/helpers/groups_helper_spec.rb | 2 +- 19 files changed, 232 insertions(+), 36 deletions(-) create mode 100644 app/views/layouts/nav/_breadcrumbs.html.haml create mode 100644 app/views/projects/issues/_nav_btns.html.haml create mode 100644 app/views/projects/merge_requests/_nav_btns.html.haml diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js index 462d792b8d5..37c6765d942 100644 --- a/app/assets/javascripts/group_name.js +++ b/app/assets/javascripts/group_name.js @@ -1,13 +1,13 @@ - +import Cookies from 'js-cookie'; import _ from 'underscore'; export default class GroupName { constructor() { - this.titleContainer = document.querySelector('.title-container'); - this.title = document.querySelector('.title'); + this.titleContainer = document.querySelector('.js-title-container'); + this.title = this.titleContainer.querySelector('.title'); this.titleWidth = this.title.offsetWidth; - this.groupTitle = document.querySelector('.group-title'); - this.groups = document.querySelectorAll('.group-path'); + this.groupTitle = this.titleContainer.querySelector('.group-title'); + this.groups = this.titleContainer.querySelectorAll('.group-path'); this.toggle = null; this.isHidden = false; this.init(); @@ -33,11 +33,20 @@ export default class GroupName { createToggle() { this.toggle = document.createElement('button'); + this.toggle.setAttribute('type', 'button'); this.toggle.className = 'text-expander group-name-toggle'; this.toggle.setAttribute('aria-label', 'Toggle full path'); - this.toggle.innerHTML = '...'; + if (Cookies.get('new_nav') === 'true') { + this.toggle.innerHTML = ''; + } else { + this.toggle.innerHTML = '...'; + } this.toggle.addEventListener('click', this.toggleGroups.bind(this)); - this.titleContainer.insertBefore(this.toggle, this.title); + if (Cookies.get('new_nav') === 'true') { + this.title.insertBefore(this.toggle, this.groupTitle); + } else { + this.titleContainer.insertBefore(this.toggle, this.title); + } this.toggleGroups(); } diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 3ce5b4fd073..bfb7a0c7e25 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -264,3 +264,127 @@ header.navbar-gitlab-new { } } } + +.breadcrumbs { + display: flex; + min-height: 60px; + padding-top: $gl-padding-top; + padding-bottom: $gl-padding-top; + color: $gl-text-color; + border-bottom: 1px solid $border-color; + + .dropdown-toggle-caret { + position: relative; + top: -1px; + padding: 0 5px; + color: rgba($black, .65); + font-size: 10px; + line-height: 1; + background: none; + border: 0; + + &:focus { + outline: 0; + } + } +} + +.breadcrumbs-container { + display: flex; + width: 100%; + position: relative; + + .dropdown-menu-projects { + margin-top: -$gl-padding; + margin-left: $gl-padding; + } +} + +.breadcrumbs-links { + flex: 1; + align-self: center; + color: $black-transparent; + + a { + color: rgba($black, .65); + + &:not(:first-child), + &.group-path { + margin-left: 4px; + } + + &:not(:last-of-type), + &.group-path { + margin-right: 3px; + } + } + + .title { + white-space: nowrap; + + > a { + &:last-of-type { + font-weight: 600; + } + } + } + + .avatar-tile { + margin-right: 5px; + border: 1px solid $border-color; + border-radius: 50%; + vertical-align: sub; + + &.identicon { + float: left; + width: 16px; + height: 16px; + margin-top: 2px; + font-size: 10px; + } + } + + .text-expander { + margin-left: 4px; + margin-right: 4px; + + > i { + position: relative; + top: 1px; + } + } +} + +.breadcrumbs-extra { + flex: 0 0 auto; + margin-left: auto; +} + +.breadcrumbs-sub-title { + margin: 2px 0 0; + font-size: 16px; + font-weight: normal; + + ul { + margin: 0; + } + + li { + display: inline-block; + + &:not(:last-child) { + &::after { + content: "/"; + margin: 0 2px 0 5px; + } + } + + &:last-child a { + font-weight: 600; + } + } + + a { + color: $gl-text-color; + } +} diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index eb45241615f..af0b3e9c5bc 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -16,11 +16,12 @@ module GroupsHelper full_title = '' group.ancestors.reverse.each do |parent| - full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') + full_title += group_title_link(parent, hidable: true) + full_title += ' / '.html_safe end - full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') + full_title += group_title_link(group) full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name content_tag :span, class: 'group-title' do @@ -56,4 +57,20 @@ module GroupsHelper def group_issues(group) IssuesFinder.new(current_user, group_id: group.id).execute end + + private + + def group_title_link(group, hidable: false) + link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do + output = + if show_new_nav? + image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16) + else + "" + end + + output << simple_sanitize(group.name) + output.html_safe + end + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c04b1419a19..53d95c2de94 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -58,7 +58,17 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } + project_link = link_to project_path(project), { class: "project-item-select-holder" } do + output = + if show_new_nav? + project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16) + else + "" + end + + output << simple_sanitize(project.name) + output.html_safe + end if current_user project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml index f893c3e1675..ad35d05c29a 100644 --- a/app/views/dashboard/activity.html.haml +++ b/app/views/dashboard/activity.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - @no_container = true = content_for :meta_tags do diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index f9b45a539a1..1cea8182733 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - page_title "Groups" - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 664ec618b79..ef1467c4d78 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - page_title 'Milestones' - header_title 'Milestones', dashboard_milestones_path diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 5e63a61e21b..7ac6cf06fb9 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- @hide_top_links = true +- @breadcrumb_title = "Projects" = content_for :meta_tags do = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 85cbe0bf0e6..e86b1ab3116 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - page_title "Snippets" - header_title "Snippets", dashboard_snippets_path diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 62a76a1b00e..1a9f5401a78 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -14,6 +14,8 @@ = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message + - if show_new_nav? + = render "layouts/nav/breadcrumbs" %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } .content{ id: "content-body" } = yield diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index f056c0af968..8cbc3f6105f 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -17,7 +17,7 @@ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo - .title-container + .title-container.js-title-container %h1.title{ class: ('initializing' if @has_group_title) }= title .navbar-collapse.collapse diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml index c0833c64911..5859f689dd1 100644 --- a/app/views/layouts/header/_new.html.haml +++ b/app/views/layouts/header/_new.html.haml @@ -83,8 +83,6 @@ = icon('ellipsis-v', class: 'js-navbar-toggle-right') = icon('times', class: 'js-navbar-toggle-left', style: 'display: none;') - = yield :header_content - = render 'shared/outdated_browser' - if @project && !@project.empty_repo? diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml new file mode 100644 index 00000000000..5f1641f4300 --- /dev/null +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -0,0 +1,19 @@ +- breadcrumb_title = @breadcrumb_title || controller.controller_name.humanize +- hide_top_links = @hide_top_links || false + +%nav.breadcrumbs{ role: "navigation" } + .breadcrumbs-container{ class: container_class } + .breadcrumbs-links.js-title-container + - unless hide_top_links + .title + = link_to "GitLab", root_path + \/ + = header_title + %h2.breadcrumbs-sub-title + %ul.list-unstyled + - if content_for?(:sub_title_before) + = yield :sub_title_before + %li= link_to breadcrumb_title, request.path + - if content_for?(:breadcrumbs_extra) + .breadcrumbs-extra.hidden-xs= yield :breadcrumbs_extra + = yield :header_content diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 6684ecfce81..3622720a8b7 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -2,6 +2,10 @@ - @content_class = "issue-boards-content" - page_title "Boards" +- if show_new_nav? + - content_for :sub_title_before do + %li= link_to "Issues", namespace_project_issues_path(@project.namespace, @project) + - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml new file mode 100644 index 00000000000..698959ec74f --- /dev/null +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -0,0 +1,11 @@ += link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do + = icon('rss') +- if @can_bulk_update + = button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" += link_to "New issue", new_namespace_project_issue_path(@project.namespace, + @project, + issue: { assignee_id: issues_finder.assignee.try(:id), + milestone_id: issues_finder.milestones.first.try(:id) }), + class: "btn btn-new", + title: "New issue", + id: "new_issue_link" diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7183794ce72..89ac5ff7128 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -13,23 +13,16 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") +- if show_new_nav? + - content_for :breadcrumbs_extra do + = render "projects/issues/nav_btns" + - if project_issues(@project).exists? %div{ class: (container_class) } .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls - = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do - = icon('rss') - - if @can_bulk_update - = button_tag "Edit Issues", class: "btn btn-default js-bulk-update-toggle" - = link_to new_namespace_project_issue_path(@project.namespace, - @project, - issue: { assignee_id: issues_finder.assignee.try(:id), - milestone_id: issues_finder.milestones.first.try(:id) }), - class: "btn btn-new", - title: "New issue", - id: "new_issue_link" do - New issue + .nav-controls{ class: ("visible-xs" if show_new_nav?) } + = render "projects/issues/nav_btns" = render 'shared/issuable/search_bar', type: :issues - if @can_bulk_update diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml new file mode 100644 index 00000000000..e92f2712347 --- /dev/null +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -0,0 +1,5 @@ +- if @can_bulk_update + = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" +- if merge_project + = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do + New merge request diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 1e30cc09894..6fe44ba3c3d 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,5 +1,7 @@ - @no_container = true - @can_bulk_update = can?(current_user, :admin_merge_request, @project) +- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) +- new_merge_request_path = namespace_project_new_merge_request_path(merge_project.namespace, merge_project) if merge_project - page_title "Merge Requests" - unless @project.default_issues_tracker? @@ -10,22 +12,18 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' +- if show_new_nav? + - content_for :breadcrumbs_extra do + = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'projects/last_push' -- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) -- new_merge_request_path = namespace_project_new_merge_request_path(merge_project.namespace, merge_project) if merge_project - - if @project.merge_requests.exists? %div{ class: container_class } .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls - - if @can_bulk_update - = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" - - if merge_project - = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do - New merge request + .nav-controls{ class: ("visible-xs" if show_new_nav?) } + = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 8da22dc78fa..e3f9d9db9eb 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -91,7 +91,7 @@ describe GroupsHelper do let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } it 'outputs the groups in the correct order' do - expect(group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) + expect(helper.group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) end end end -- cgit v1.2.1 From 8fd0887fe4ab1c8ccee2fe023af2b6b2afd22ecc Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Fri, 30 Jun 2017 09:52:59 -0700 Subject: Add issuable-list class to shared mr/issue lists to fix new responsive layout --- app/views/shared/_issues.html.haml | 2 +- app/views/shared/_merge_requests.html.haml | 2 +- changelogs/unreleased/fix-assigned-issuable-lists.yml | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/fix-assigned-issuable-lists.yml diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 3a49227961f..49555b6ff4e 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,6 +1,6 @@ - if @issues.to_a.any? .panel.panel-default.panel-small.panel-without-border - %ul.content-list.issues-list + %ul.content-list.issues-list.issuable-list = render partial: 'projects/issues/issue', collection: @issues = paginate @issues, theme: "gitlab" - else diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index eecbb32e90e..0517896cfbd 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,6 +1,6 @@ - if @merge_requests.to_a.any? .panel.panel-default.panel-small.panel-without-border - %ul.content-list.mr-list + %ul.content-list.mr-list.issuable-list = render partial: 'projects/merge_requests/merge_request', collection: @merge_requests = paginate @merge_requests, theme: "gitlab" diff --git a/changelogs/unreleased/fix-assigned-issuable-lists.yml b/changelogs/unreleased/fix-assigned-issuable-lists.yml new file mode 100644 index 00000000000..fc2cd18ddb6 --- /dev/null +++ b/changelogs/unreleased/fix-assigned-issuable-lists.yml @@ -0,0 +1,5 @@ +--- +title: Add issuable-list class to shared mr/issue lists to fix new responsive layout + design +merge_request: +author: -- cgit v1.2.1 From cf996b446457318e6f73cd64ced5f4f7bf30d68d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Sat, 1 Jul 2017 02:08:28 +0800 Subject: We don't need to disable transaction in this case --- db/migrate/20160804142904_add_ci_config_file_to_project.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/db/migrate/20160804142904_add_ci_config_file_to_project.rb b/db/migrate/20160804142904_add_ci_config_file_to_project.rb index 4b9860c5f74..674a22ae8f3 100644 --- a/db/migrate/20160804142904_add_ci_config_file_to_project.rb +++ b/db/migrate/20160804142904_add_ci_config_file_to_project.rb @@ -1,13 +1,6 @@ -# 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 -- cgit v1.2.1 From 27730abe3a7b26c44a71b1d12134223186d25d5b Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Fri, 30 Jun 2017 11:54:23 -0700 Subject: Add GitLab Runner Helm Chart documenation for cucstom certificates This outlines how to provide the custom ssl certificate to the runner for accessing GitLab in the case that GitLab is using a custom/self-signed certificate. --- doc/install/kubernetes/gitlab_runner_chart.md | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index b8bc0795f2e..515b2841d08 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -54,6 +54,13 @@ gitlabURL: http://gitlab.your-domain.com/ ## runnerRegistrationToken: "" +## Set the certsSecretName in order to pass custom certficates for GitLab Runner to use +## Provide resource name for a Kubernetes Secret Object in the same namespace, +## this is used to populate the /etc/gitlab-runner/certs directory +## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates +## +#certsSecretName: + ## Configure the maximum number of concurrent jobs ## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section ## @@ -135,6 +142,52 @@ runners: privileged: true ``` +### Providing a custom certificate for accessing GitLab + +You can provide a [Kubernetes Secret](https://kubernetes.io/docs/concepts/configuration/secret/) +to the GitLab Runner Helm Chart, which will be used to populate the container's +`/etc/gitlab-runner/certs` directory. + +Each key name in the Secret will be used as a filename in the directory, with the +file content being the value associated with the key. + +More information on how GitLab Runner uses these certificates can be found in the +[Runner Documentation](https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates). + + - The key/file name used should be in the format `.crt`. For example: `gitlab.your-domain.com.crt`. + - Any intermediate certificates need to be concatenated to your server certificate in the same file. + - The hostname used should be the one the certificate is registered for. + +The GitLab Runner Helm Chart does not create a secret for you. In order to create +the secret, you can prepare your certificate on you local machine, and then run +the `kubectl create secret` command from the directory with the certificate + +```bash +kubectl + --namespace + create secret generic + --from-file= +``` + +- `` is the Kubernetes namespace where you want to install the GitLab Runner. +- `` is the Kubernetes Secret resource name. For example: `gitlab-domain-cert` +- `` is the filename for the certificate in your current directory that will be imported into the secret + +You then need to provide the secret's name to the GitLab Runner chart. + +Add the following to your `values.yaml` + +```yaml +## Set the certsSecretName in order to pass custom certficates for GitLab Runner to use +## Provide resource name for a Kubernetes Secret Object in the same namespace, +## this is used to populate the /etc/gitlab-runner/certs directory +## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates +## +certsSecretName: +``` + +- `` is the Kubernetes Secret resource name. For example: `gitlab-domain-cert` + ## Installing GitLab Runner using the Helm Chart Once you [have configured](#configuration) GitLab Runner in your `values.yml` file, -- cgit v1.2.1 From 3900b2f3783490ec0d5ab7dbbe946b14bdc3b975 Mon Sep 17 00:00:00 2001 From: Horacio Bertorello Date: Thu, 29 Jun 2017 02:23:38 -0300 Subject: Hide archived project labels from group issue tracker --- app/finders/labels_finder.rb | 7 ++++++- .../hb-hide-archived-labels-from-group-issue-tracker.yml | 4 ++++ spec/finders/labels_finder_spec.rb | 6 +++--- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 042d792dada..ce432ddbfe6 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -83,7 +83,12 @@ class LabelsFinder < UnionFinder def projects return @projects if defined?(@projects) - @projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute + @projects = if skip_authorization + Project.all + else + ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute + end + @projects = @projects.in_namespace(params[:group_id]) if group? @projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.reorder(nil) diff --git a/changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml b/changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml new file mode 100644 index 00000000000..3b465d84126 --- /dev/null +++ b/changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml @@ -0,0 +1,4 @@ +--- +title: Hide archived project labels from group issue tracker +merge_request: 12547 +author: Horacio Bertorello diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index 1724cdba830..95d96354b77 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -49,12 +49,12 @@ describe LabelsFinder do end context 'filtering by group_id' do - it 'returns labels available for any project within the group' do + it 'returns labels available for any non-archived project within the group' do group_1.add_developer(user) - + project_1.archive! finder = described_class.new(user, group_id: group_1.id) - expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1, project_label_5] + expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5] end end -- cgit v1.2.1 From dcdf2a8bc58e5a9750f3a4dfdb4b1795863b3853 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 28 Jun 2017 21:30:38 +0200 Subject: Make entrypoint and command keys to be array of strings --- lib/gitlab/ci/config/entry/image.rb | 2 +- lib/gitlab/ci/config/entry/service.rb | 4 ++-- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 25 +++++++++++++++++-------- spec/lib/gitlab/ci/config/entry/image_spec.rb | 4 ++-- spec/lib/gitlab/ci/config/entry/service_spec.rb | 6 +++--- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index 897dcff8012..6555c589173 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -15,7 +15,7 @@ module Gitlab validates :config, allowed_keys: ALLOWED_KEYS validates :name, type: String, presence: true - validates :entrypoint, type: String, allow_nil: true + validates :entrypoint, array_of_strings: true, allow_nil: true end def hash? diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb index b52faf48b58..3e2ebcff31a 100644 --- a/lib/gitlab/ci/config/entry/service.rb +++ b/lib/gitlab/ci/config/entry/service.rb @@ -15,8 +15,8 @@ module Gitlab validates :config, allowed_keys: ALLOWED_KEYS validates :name, type: String, presence: true - validates :entrypoint, type: String, allow_nil: true - validates :command, type: String, allow_nil: true + validates :entrypoint, array_of_strings: true, allow_nil: true + validates :command, array_of_strings: true, allow_nil: true validates :alias, type: String, allow_nil: true end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index af0e7855a9b..e02317adbad 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -598,8 +598,10 @@ module Ci describe "Image and service handling" do context "when extended docker configuration is used" do it "returns image and service when defined" do - config = YAML.dump({ image: { name: "ruby:2.1" }, - services: ["mysql", { name: "docker:dind", alias: "docker" }], + config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: ["mysql", { name: "docker:dind", alias: "docker", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }], before_script: ["pwd"], rspec: { script: "rspec" } }) @@ -614,8 +616,10 @@ module Ci coverage_regex: nil, tag_list: [], options: { - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }, { name: "docker:dind", alias: "docker" }] + image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "mysql" }, + { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }] }, allow_failure: false, when: "on_success", @@ -628,8 +632,11 @@ module Ci config = YAML.dump({ image: "ruby:2.1", services: ["mysql"], before_script: ["pwd"], - rspec: { image: { name: "ruby:2.5" }, - services: [{ name: "postgresql", alias: "db-pg" }, "docker:dind"], script: "rspec" } }) + rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, "docker:dind"], + script: "rspec" } }) config_processor = GitlabCiYamlProcessor.new(config, path) @@ -642,8 +649,10 @@ module Ci coverage_regex: nil, tag_list: [], options: { - image: { name: "ruby:2.5" }, - services: [{ name: "postgresql", alias: "db-pg" }, { name: "docker:dind" }] + image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"]}, + { name: "docker:dind" }] }, allow_failure: false, when: "on_success", diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index bca22e39500..d8800fb675b 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Ci::Config::Entry::Image do end context 'when configuration is a hash' do - let(:config) { { name: 'ruby:2.2', entrypoint: '/bin/sh' } } + let(:config) { { name: 'ruby:2.2', entrypoint: ['/bin/sh', 'run'] } } describe '#value' do it 'returns image hash' do @@ -66,7 +66,7 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#entrypoint' do it "returns image's entrypoint" do - expect(entry.entrypoint).to eq '/bin/sh' + expect(entry.entrypoint).to eq ['/bin/sh', 'run'] end end end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 7202fe525e4..24b7086c34c 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Entry::Service do context 'when configuration is a hash' do let(:config) do - { name: 'postgresql:9.5', alias: 'db', command: 'cmd', entrypoint: '/bin/sh' } + { name: 'postgresql:9.5', alias: 'db', command: ['cmd', 'run'], entrypoint: ['/bin/sh', 'run'] } end describe '#valid?' do @@ -72,13 +72,13 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#command' do it "returns service's command" do - expect(entry.command).to eq 'cmd' + expect(entry.command).to eq ['cmd', 'run'] end end describe '#entrypoint' do it "returns service's entrypoint" do - expect(entry.entrypoint).to eq '/bin/sh' + expect(entry.entrypoint).to eq ['/bin/sh', 'run'] end end end -- cgit v1.2.1 From 16ff3229cbb71e05f9cea3a16b481a8120dfcb7e Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Fri, 30 Jun 2017 13:30:19 +0200 Subject: Fix rubocop offenses --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- spec/lib/gitlab/ci/config/entry/image_spec.rb | 4 ++-- spec/lib/gitlab/ci/config/entry/service_spec.rb | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index e02317adbad..482f03aa0cc 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -651,7 +651,7 @@ module Ci options: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"]}, + command: ["/usr/local/bin/init", "run"] }, { name: "docker:dind" }] }, allow_failure: false, diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index d8800fb675b..1a4d9ed5517 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Ci::Config::Entry::Image do end context 'when configuration is a hash' do - let(:config) { { name: 'ruby:2.2', entrypoint: ['/bin/sh', 'run'] } } + let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run) } } describe '#value' do it 'returns image hash' do @@ -66,7 +66,7 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#entrypoint' do it "returns image's entrypoint" do - expect(entry.entrypoint).to eq ['/bin/sh', 'run'] + expect(entry.entrypoint).to eq %w(/bin/sh run) end end end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 24b7086c34c..9ebf947a751 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Entry::Service do context 'when configuration is a hash' do let(:config) do - { name: 'postgresql:9.5', alias: 'db', command: ['cmd', 'run'], entrypoint: ['/bin/sh', 'run'] } + { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) } end describe '#valid?' do @@ -72,13 +72,13 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#command' do it "returns service's command" do - expect(entry.command).to eq ['cmd', 'run'] + expect(entry.command).to eq %w(cmd run) end end describe '#entrypoint' do it "returns service's entrypoint" do - expect(entry.entrypoint).to eq ['/bin/sh', 'run'] + expect(entry.entrypoint).to eq %w(/bin/sh run) end end end -- cgit v1.2.1 From da18378fa7c52d5f08494024e7dda90701534d6a Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Thu, 29 Jun 2017 18:41:09 -0400 Subject: Set force_remove_source_branch default to false. --- app/views/shared/issuable/form/_merge_params.html.haml | 3 +-- ...885-unintentionally-removing-branch-when-merging-merge-request.yml | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index bfa91629e1e..8f6509a8ce8 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -11,8 +11,7 @@ .col-sm-10.col-sm-offset-2 - if issuable.can_remove_source_branch?(current_user) .checkbox - - initial_checkbox_value = issuable.merge_params.key?('force_remove_source_branch') ? issuable.force_remove_source_branch? : true = label_tag 'merge_request[force_remove_source_branch]' do = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil - = check_box_tag 'merge_request[force_remove_source_branch]', '1', initial_checkbox_value + = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? Remove source branch when merge request is accepted. diff --git a/changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml b/changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml new file mode 100644 index 00000000000..313aeab91b5 --- /dev/null +++ b/changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml @@ -0,0 +1,4 @@ +--- +title: Set default for Remove source branch to false. +merge_request: !12576 +author: -- cgit v1.2.1 From 0bd4754aa1cbab94a717bba4e3cb841052223b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Mon, 3 Jul 2017 10:33:41 +0800 Subject: fix the format of the translation string --- locale/it/gitlab.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po index 777e4c0ece3..e3e2af142f5 100644 --- a/locale/it/gitlab.po +++ b/locale/it/gitlab.po @@ -8,9 +8,9 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2017-06-29 09:40-0400\n" +"PO-Revision-Date: 2017-07-02 10:32-0400\n" "Last-Translator: Paolo Falomo \n" -"Language-Team: Italian (https://translate.zanata.org/project/view/GitLab)\n" +"Language-Team: Italian\n" "Language: it\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" @@ -534,7 +534,7 @@ msgid "OfSearchInADropdown|Filter" msgstr "Filtra" msgid "OpenedNDaysAgo|Opened" -msgstr "ApertoNGiorniFa|Aperto" +msgstr "Aperto" msgid "Options" msgstr "Opzioni" @@ -642,7 +642,7 @@ msgid "ProjectLastActivity|Never" msgstr "Mai" msgid "ProjectLifecycle|Stage" -msgstr "ProgettoCicloVitale|Stadio" +msgstr "Stadio" msgid "ProjectNetworkGraph|Graph" msgstr "Grafico" -- cgit v1.2.1 From 96e986327c4dad9248f9013f191119ffafe4a6d8 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 3 Jul 2017 05:14:00 +0000 Subject: Implement review comments for !12445 from @jneen. - Fix duplicate `prevent` declaration - Add spec for `GlobalPolicy` --- app/policies/global_policy.rb | 1 - spec/policies/global_policy_spec.rb | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 spec/policies/global_policy_spec.rb diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 7767d3cccd5..55eefa76d3f 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -18,7 +18,6 @@ class GlobalPolicy < BasePolicy prevent :receive_notifications prevent :use_quick_actions prevent :create_group - prevent :log_in end rule { default }.policy do diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb new file mode 100644 index 00000000000..bb0fa0c0e9c --- /dev/null +++ b/spec/policies/global_policy_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe GlobalPolicy, models: true do + let(:current_user) { create(:user) } + let(:user) { create(:user) } + + subject { GlobalPolicy.new(current_user, [user]) } + + describe "reading the list of users" do + context "for a logged in user" do + it { is_expected.to be_allowed(:read_users_list) } + end + + context "for an anonymous user" do + let(:current_user) { nil } + + context "when the public level is restricted" do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it { is_expected.not_to be_allowed(:read_users_list) } + end + + context "when the public level is not restricted" do + before do + stub_application_setting(restricted_visibility_levels: []) + end + + it { is_expected.to be_allowed(:read_users_list) } + end + end + end +end -- cgit v1.2.1 From d8ab0d609da979bf255660fb36d1a976c149b344 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 3 Jul 2017 14:17:43 +0800 Subject: Give project to the dummy pipeline --- spec/support/cycle_analytics_helpers.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 6e1eb5c678d..c0a5491a430 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -74,7 +74,9 @@ module CycleAnalyticsHelpers def dummy_pipeline @dummy_pipeline ||= - Ci::Pipeline.new(sha: project.repository.commit('master').sha) + Ci::Pipeline.new( + sha: project.repository.commit('master').sha, + project: project) end def new_dummy_job(environment) -- cgit v1.2.1 From c7661f04d02316d48856c0b9693887228dc054d0 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 28 Jun 2017 14:36:04 +0200 Subject: Strings ready for translation; Pipeline charts Earlier, this was part of another MR, but that got split. Didn't pick that commit, as there were many merge conflicts. Vim macros seemed faster. --- app/views/projects/pipelines/charts.html.haml | 4 +- app/views/projects/pipelines/charts/_overall.haml | 16 ++-- .../projects/pipelines/charts/_pipeline_times.haml | 2 +- .../projects/pipelines/charts/_pipelines.haml | 12 +-- locale/en/gitlab.po | 97 ++++++++++++++++++++-- locale/gitlab.pot | 61 +++++++++++++- 6 files changed, 166 insertions(+), 26 deletions(-) diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index 8ffddfe6154..78002e8cd64 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- page_title "Charts", "Pipelines" +- page_title _("Charts"), _("Pipelines") - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('graphs') @@ -8,7 +8,7 @@ %div{ class: container_class } .sub-header-block .oneline - A collection of graphs for Continuous Integration + = _("A collection of graphs regarding Continuous Integration") #charts.ci-charts .row diff --git a/app/views/projects/pipelines/charts/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml index 93083397d5b..66786c7ff59 100644 --- a/app/views/projects/pipelines/charts/_overall.haml +++ b/app/views/projects/pipelines/charts/_overall.haml @@ -1,15 +1,15 @@ -%h4 Overall stats +%h4= s_("PipelineCharts|Overall statistics") %ul %li - Total: - %strong= pluralize @counts[:total], 'pipeline' + = s_("PipelineCharts|Total:") + %strong= n_("1 pipeline", "%d pipelines", @counts[:total]) % @counts[:total] %li - Successful: - %strong= pluralize @counts[:success], 'pipeline' + = s_("PipelineCharts|Successful:") + %strong= n_("1 pipeline", "%d pipelines", @counts[:success]) % @counts[:success] %li - Failed: - %strong= pluralize @counts[:failed], 'pipeline' + = s_("PipelineCharts|Failed:") + %strong= n_("1 pipeline", "%d pipelines", @counts[:failed]) % @counts[:failed] %li - Success ratio: + = s_("PipelineCharts|Success ratio:") %strong #{success_ratio(@counts)}% diff --git a/app/views/projects/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml index aee7c5492aa..1292f580a81 100644 --- a/app/views/projects/pipelines/charts/_pipeline_times.haml +++ b/app/views/projects/pipelines/charts/_pipeline_times.haml @@ -1,6 +1,6 @@ %div %p.light - Commit duration in minutes for last 30 commits + = _("Commit duration in minutes for last 30 commits") %canvas#build_timesChart{ height: 200 } diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml index b6f453b9736..be884448087 100644 --- a/app/views/projects/pipelines/charts/_pipelines.haml +++ b/app/views/projects/pipelines/charts/_pipelines.haml @@ -1,29 +1,29 @@ -%h4 Pipelines charts +%h4= _("Pipelines charts") %p   %span.cgreen = icon("circle") - success + = s_("Pipeline|success")   %span.cgray = icon("circle") - all + = s_("Pipeline|all") .prepend-top-default %p.light - Jobs for last week + = _("Jobs for last week") (#{date_from_to(Date.today - 7.days, Date.today)}) %canvas#weekChart{ height: 200 } .prepend-top-default %p.light - Jobs for last month + = _("Jobs for last month") (#{date_from_to(Date.today - 30.days, Date.today)}) %canvas#monthChart{ height: 200 } .prepend-top-default %p.light - Jobs for last year + = _("Jobs for last year") %canvas#yearChart.padded{ height: 250 } - [:week, :month, :year].each do |scope| diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index afb8fb3176f..bda3fc09e85 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -17,9 +17,27 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "\n" +msgid "%d additional commit has been omitted to prevent performance issues." +msgid_plural "%d additional commits have been omitted to prevent performance issues." +msgstr[0] "" +msgstr[1] "" + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "" +msgstr[1] "" + msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "" +msgid "1 pipeline" +msgid_plural "%d pipelines" +msgstr[0] "" +msgstr[1] "" + +msgid "A collection of graphs regarding Continuous Integration" +msgstr "" + msgid "About auto deploy" msgstr "" @@ -61,9 +79,24 @@ msgstr[1] "" msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgstr "" +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "" + msgid "Branches" msgstr "" +msgid "Browse Directory" +msgstr "" + +msgid "Browse File" +msgstr "" + +msgid "Browse Files" +msgstr "" + msgid "Browse files" msgstr "" @@ -159,6 +192,9 @@ msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit duration in minutes for last 30 commits" +msgstr "" + msgid "Commit message" msgstr "" @@ -171,6 +207,9 @@ msgstr "" msgid "Commits" msgstr "" +msgid "Commits feed" +msgstr "" + msgid "Commits|History" msgstr "" @@ -195,6 +234,9 @@ msgstr "" msgid "Create New Directory" msgstr "" +msgid "Create a personal access token on your account to pull or push via %{protocol}." +msgstr "" + msgid "Create directory" msgstr "" @@ -213,6 +255,9 @@ msgstr "" msgid "CreateTag|Tag" msgstr "" +msgid "CreateTokenToCloneLink|create a personal access token" +msgstr "" + msgid "Cron Timezone" msgstr "" @@ -323,6 +368,9 @@ msgstr "" msgid "Files" msgstr "" +msgid "Filter by commit message" +msgstr "" + msgid "Find by path" msgstr "" @@ -370,6 +418,15 @@ msgstr "" msgid "Introducing Cycle Analytics" msgstr "" +msgid "Jobs for last month" +msgstr "" + +msgid "Jobs for last week" +msgstr "" + +msgid "Jobs for last year" +msgstr "" + msgid "LFSStatus|Disabled" msgstr "" @@ -535,6 +592,21 @@ msgstr "" msgid "Pipeline Schedules" msgstr "" +msgid "PipelineCharts|Failed:" +msgstr "" + +msgid "PipelineCharts|Overall statistics" +msgstr "" + +msgid "PipelineCharts|Success ratio:" +msgstr "" + +msgid "PipelineCharts|Successful:" +msgstr "" + +msgid "PipelineCharts|Total:" +msgstr "" + msgid "PipelineSchedules|Activated" msgstr "" @@ -565,6 +637,18 @@ msgstr "" msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" +msgid "Pipelines" +msgstr "" + +msgid "Pipelines charts" +msgstr "" + +msgid "Pipeline|all" +msgstr "" + +msgid "Pipeline|success" +msgstr "" + msgid "Pipeline|with stage" msgstr "" @@ -688,7 +772,7 @@ msgstr "" msgid "Select target branch" msgstr "" -msgid "Set a password on your account to pull or push via %{protocol}" +msgid "Set a password on your account to pull or push via %{protocol}." msgstr "" msgid "Set up CI" @@ -714,10 +798,7 @@ msgstr "" msgid "StarProject|Star" msgstr "" -msgid "Start a %{new_merge_request} with these changes" -msgstr "" - -msgid "Start a new merge request with these changes" +msgid "Start a %{new_merge_request} with these changes" msgstr "" msgid "Switch branch/tag" @@ -948,9 +1029,15 @@ msgstr "" msgid "Upload file" msgstr "" +msgid "UploadLink|click to upload" +msgstr "" + msgid "Use your global notification setting" msgstr "" +msgid "View open merge request" +msgstr "" + msgid "VisibilityLevel|Internal" msgstr "" diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 07f9efeb495..9f1caeddaa7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-06-19 15:50-0500\n" -"PO-Revision-Date: 2017-06-19 15:50-0500\n" +"POT-Creation-Date: 2017-06-28 13:32+0200\n" +"PO-Revision-Date: 2017-06-28 13:32+0200\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -msgid "%d additional commit have been omitted to prevent performance issues." +msgid "%d additional commit has been omitted to prevent performance issues." msgid_plural "%d additional commits have been omitted to prevent performance issues." msgstr[0] "" msgstr[1] "" @@ -31,6 +31,14 @@ msgstr[1] "" msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "" +msgid "1 pipeline" +msgid_plural "%d pipelines" +msgstr[0] "" +msgstr[1] "" + +msgid "A collection of graphs regarding Continuous Integration" +msgstr "" + msgid "About auto deploy" msgstr "" @@ -185,6 +193,9 @@ msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit duration in minutes for last 30 commits" +msgstr "" + msgid "Commit message" msgstr "" @@ -224,6 +235,9 @@ msgstr "" msgid "Create New Directory" msgstr "" +msgid "Create a personal access token on your account to pull or push via %{protocol}." +msgstr "" + msgid "Create directory" msgstr "" @@ -242,6 +256,9 @@ msgstr "" msgid "CreateTag|Tag" msgstr "" +msgid "CreateTokenToCloneLink|create a personal access token" +msgstr "" + msgid "Cron Timezone" msgstr "" @@ -402,6 +419,15 @@ msgstr "" msgid "Introducing Cycle Analytics" msgstr "" +msgid "Jobs for last month" +msgstr "" + +msgid "Jobs for last week" +msgstr "" + +msgid "Jobs for last year" +msgstr "" + msgid "LFSStatus|Disabled" msgstr "" @@ -567,6 +593,21 @@ msgstr "" msgid "Pipeline Schedules" msgstr "" +msgid "PipelineCharts|Failed:" +msgstr "" + +msgid "PipelineCharts|Overall statistics" +msgstr "" + +msgid "PipelineCharts|Success ratio:" +msgstr "" + +msgid "PipelineCharts|Successful:" +msgstr "" + +msgid "PipelineCharts|Total:" +msgstr "" + msgid "PipelineSchedules|Activated" msgstr "" @@ -597,6 +638,18 @@ msgstr "" msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" +msgid "Pipelines" +msgstr "" + +msgid "Pipelines charts" +msgstr "" + +msgid "Pipeline|all" +msgstr "" + +msgid "Pipeline|success" +msgstr "" + msgid "Pipeline|with stage" msgstr "" @@ -720,7 +773,7 @@ msgstr "" msgid "Select target branch" msgstr "" -msgid "Set a password on your account to pull or push via %{protocol}" +msgid "Set a password on your account to pull or push via %{protocol}." msgstr "" msgid "Set up CI" -- cgit v1.2.1 From a080aa13a86cd0bfc11c728d9999deeafaa0a5d0 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 3 Jul 2017 07:44:58 +0000 Subject: Resolve "Issue Board -> "Remove from board" button when viewing an issue gives js error and fails" --- .../javascripts/boards/components/board_sidebar.js | 5 ++++- .../boards/components/sidebar/remove_issue.js | 3 +-- app/views/projects/boards/components/_sidebar.html.haml | 3 ++- ...on-when-viewing-an-issue-gives-js-error-and-fails.yml | 4 ++++ spec/features/boards/sidebar_spec.rb | 16 ++++++++++++++++ 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/34097-issue-board-remove-from-board-button-when-viewing-an-issue-gives-js-error-and-fails.yml diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index c7afd4ead6b..590b7be36e3 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ }, milestoneTitle() { return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; - } + }, + canRemove() { + return !this.list.preset; + }, }, watch: { detail: { diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 5597f128b80..6a900d4abd0 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ }, template: `
    + class="block list">