From 62ef67acc3a8d260aa3e641b350aaecf8d60f1aa Mon Sep 17 00:00:00 2001 From: Robin Bobbitt Date: Fri, 4 Aug 2017 09:17:20 -0400 Subject: Hide read_registry scope when registry is disabled on instance --- app/models/personal_access_token.rb | 2 +- ...-read-registry-scope-when-registry-disabled.yml | 4 +++ lib/gitlab/auth.rb | 2 +- spec/lib/gitlab/auth_spec.rb | 38 ++++++++++++++++++---- spec/models/personal_access_token_spec.rb | 35 +++++++++++++++++--- spec/requests/jwt_controller_spec.rb | 4 +++ .../api/scopes/read_user_shared_examples.rb | 4 +++ spec/support/stub_gitlab_calls.rb | 4 ++- 8 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 654be927ed8..ec0ebe4d353 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base protected def validate_scopes - unless scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) } + unless revoked || scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) } errors.add :scopes, "can only contain available scopes" end end diff --git a/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml b/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml new file mode 100644 index 00000000000..22ac9b9073f --- /dev/null +++ b/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml @@ -0,0 +1,4 @@ +--- +title: Hide read_registry scope when registry is disabled on instance +merge_request: 13314 +author: Robin Bobbitt diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 7d3aa532750..0a5afeb5202 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -2,7 +2,7 @@ module Gitlab module Auth MissingPersonalTokenError = Class.new(StandardError) - REGISTRY_SCOPES = [:read_registry].freeze + REGISTRY_SCOPES = Gitlab.config.registry.enabled ? [:read_registry].freeze : [].freeze # Scopes used for GitLab API access API_SCOPES = [:api, :read_user].freeze diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 4a498e79c87..0d65c7883b0 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -17,11 +17,31 @@ describe Gitlab::Auth do end it 'OPTIONAL_SCOPES contains all non-default scopes' do + stub_container_registry_config(enabled: true) + expect(subject::OPTIONAL_SCOPES).to eq %i[read_user read_registry openid] end - it 'REGISTRY_SCOPES contains all registry related scopes' do - expect(subject::REGISTRY_SCOPES).to eq %i[read_registry] + context 'REGISTRY_SCOPES' do + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end + + it 'is empty' do + expect(subject::REGISTRY_SCOPES).to eq [] + end + end + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'contains all registry related scopes' do + expect(subject::REGISTRY_SCOPES).to eq %i[read_registry] + end + end end end @@ -147,11 +167,17 @@ describe Gitlab::Auth do expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities)) end - it 'succeeds for personal access tokens with the `read_registry` scope' do - personal_access_token = create(:personal_access_token, scopes: ['read_registry']) + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'succeeds for personal access tokens with the `read_registry` scope' do + personal_access_token = create(:personal_access_token, scopes: ['read_registry']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + end end it 'succeeds if it is an impersonation token' do diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index b2f2a3ce914..01440b15674 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -41,7 +41,7 @@ describe PersonalAccessToken do it 'revokes the token' do active_personal_access_token.revoke! - expect(active_personal_access_token.revoked?).to be true + expect(active_personal_access_token).to be_revoked end end @@ -61,10 +61,37 @@ describe PersonalAccessToken do expect(personal_access_token).to be_valid end - it "allows creating a token with read_registry scope" do - personal_access_token.scopes = [:read_registry] + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end - expect(personal_access_token).to be_valid + it "rejects creating a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + expect(personal_access_token).not_to be_valid + expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes" + end + + it "allows revoking a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + personal_access_token.revoke! + + expect(personal_access_token).to be_revoked + end + end + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it "allows creating a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + expect(personal_access_token).to be_valid + end end it "rejects creating a token with unavailable scopes" do diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 8d79ea3dd40..41bf43a9bce 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -49,6 +49,10 @@ describe JwtController do let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) } let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } + before do + stub_container_registry_config(enabled: true) + end + subject! { get '/jwt/auth', parameters, headers } it 'authenticates correctly' do diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb index 3bd589d64b9..57e28e040d7 100644 --- a/spec/support/api/scopes/read_user_shared_examples.rb +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -23,6 +23,10 @@ shared_examples_for 'allows the "read_user" scope' do context 'when the requesting token does not have any required scope' do let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) } + before do + stub_container_registry_config(enabled: true) + end + it 'returns a "401" response' do get api_call.call(path, user, personal_access_token: token) diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 78a2ff73746..9695f35bd25 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -26,9 +26,11 @@ module StubGitlabCalls end def stub_container_registry_config(registry_settings) - allow(Gitlab.config.registry).to receive_messages(registry_settings) allow(Auth::ContainerRegistryAuthenticationService) .to receive(:full_access_token).and_return('token') + + allow(Gitlab.config.registry).to receive_messages(registry_settings) + load 'lib/gitlab/auth.rb' end def stub_container_registry_tags(repository: :any, tags:) -- cgit v1.2.1 From 88568317a8829a50a22c1151b9580c0d8fe60c1f Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Mon, 4 Sep 2017 15:52:00 -0500 Subject: Add GitLab-Pages version to Admin Dashboard --- app/views/admin/dashboard/index.html.haml | 5 +++++ .../unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml | 5 +++++ lib/gitlab/pages.rb | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml create mode 100644 lib/gitlab/pages.rb diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 069f8f89e0b..703f4165128 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -111,6 +111,11 @@ GitLab API %span.pull-right = API::API::version + - if Gitlab.config.pages.enabled + %p + GitLab Pages + %span.pull-right + = Gitlab::Pages::VERSION %p Git %span.pull-right diff --git a/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml b/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml new file mode 100644 index 00000000000..680ef0cef92 --- /dev/null +++ b/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Add GitLab-Pages version to Admin Dashboard +merge_request: 14040 +author: @travismiller +type: added diff --git a/lib/gitlab/pages.rb b/lib/gitlab/pages.rb new file mode 100644 index 00000000000..981ef8faa9a --- /dev/null +++ b/lib/gitlab/pages.rb @@ -0,0 +1,5 @@ +module Gitlab + module Pages + VERSION = File.read(Rails.root.join("GITLAB_PAGES_VERSION")).strip.freeze + end +end -- cgit v1.2.1 From 5c6f40ab6e825e8d31e3bfc362e9bcce80a14dba Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 8 Sep 2017 19:38:02 +0200 Subject: Add usage ping for Auto DevOps Fixes gitlab-org/gitlab-ce#37648 --- changelogs/unreleased/zj-usage-data-auto-devops.yml | 5 +++++ lib/gitlab/usage_data.rb | 2 ++ spec/lib/gitlab/usage_data_spec.rb | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 changelogs/unreleased/zj-usage-data-auto-devops.yml diff --git a/changelogs/unreleased/zj-usage-data-auto-devops.yml b/changelogs/unreleased/zj-usage-data-auto-devops.yml new file mode 100644 index 00000000000..9b5ec894042 --- /dev/null +++ b/changelogs/unreleased/zj-usage-data-auto-devops.yml @@ -0,0 +1,5 @@ +--- +title: Add usage data for Auto DevOps +merge_request: +author: +type: other diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 3cf26625108..47b0be5835f 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -22,6 +22,8 @@ module Gitlab ci_builds: ::Ci::Build.count, ci_internal_pipelines: ::Ci::Pipeline.internal.count, ci_external_pipelines: ::Ci::Pipeline.external.count, + ci_pipeline_config_auto_devops: ::Ci::Pipeline.auto_devops_source.count, + ci_pipeline_config_repository: ::Ci::Pipeline.repository_source.count, ci_runners: ::Ci::Runner.count, ci_triggers: ::Ci::Trigger.count, ci_pipeline_schedules: ::Ci::PipelineSchedule.count, diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 68429d792f2..daca5c73415 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -40,6 +40,8 @@ describe Gitlab::UsageData do ci_builds ci_internal_pipelines ci_external_pipelines + ci_pipeline_config_auto_devops + ci_pipeline_config_repository ci_runners ci_triggers ci_pipeline_schedules -- cgit v1.2.1 From fcab8b4343ba6ce7c95f7ab51de1fdbd5b078d43 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Fri, 8 Sep 2017 10:57:32 +0200 Subject: update installation and update instructions for 10.0 --- doc/install/installation.md | 4 +- doc/update/8.17-to-9.0.md | 2 +- doc/update/9.0-to-9.1.md | 2 +- doc/update/9.1-to-9.2.md | 2 +- doc/update/9.2-to-9.3.md | 2 +- doc/update/9.3-to-9.4.md | 2 +- doc/update/9.4-to-9.5.md | 2 +- doc/update/9.5-to-10.0.md | 356 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 364 insertions(+), 8 deletions(-) create mode 100644 doc/update/9.5-to-10.0.md diff --git a/doc/install/installation.md b/doc/install/installation.md index 66eb7675896..200cd94f43c 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -299,9 +299,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-5-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-0-stable gitlab -**Note:** You can change `9-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `10-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 2abc57da1a0..baab217b6b7 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -236,7 +236,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md index 3fd1d023d2a..6f1870a1366 100644 --- a/doc/update/9.0-to-9.1.md +++ b/doc/update/9.0-to-9.1.md @@ -236,7 +236,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-1-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md index 5f7a616cc7d..ce72b313031 100644 --- a/doc/update/9.1-to-9.2.md +++ b/doc/update/9.1-to-9.2.md @@ -194,7 +194,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-1-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-2-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md index 9d0b0da7edb..779ced0cf75 100644 --- a/doc/update/9.2-to-9.3.md +++ b/doc/update/9.2-to-9.3.md @@ -230,7 +230,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-2-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-3-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md index 9ee01bc9c51..78d8a6c7de5 100644 --- a/doc/update/9.3-to-9.4.md +++ b/doc/update/9.3-to-9.4.md @@ -243,7 +243,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-3-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md index 1b5a15589af..a7255142ef5 100644 --- a/doc/update/9.4-to-9.5.md +++ b/doc/update/9.4-to-9.5.md @@ -252,7 +252,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-5-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.5-to-10.0.md b/doc/update/9.5-to-10.0.md new file mode 100644 index 00000000000..8581e6511f2 --- /dev/null +++ b/doc/update/9.5-to-10.0.md @@ -0,0 +1,356 @@ +# From 9.5 to 10.0 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz +echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +cd ruby-2.3.3 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + + + + +Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage +JavaScript dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 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 + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-0-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-0-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$( Date: Mon, 11 Sep 2017 09:25:54 +0000 Subject: Delete duplicated lines. --- app/models/issue.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index 8c7d492e605..cd5056aae5e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -30,9 +30,6 @@ class Issue < ActiveRecord::Base has_many :issue_assignees has_many :assignees, class_name: "User", through: :issue_assignees - has_many :issue_assignees - has_many :assignees, class_name: "User", through: :issue_assignees - validates :project, presence: true scope :in_projects, ->(project_ids) { where(project_id: project_ids) } -- cgit v1.2.1 From 1e1c075d30e263abc13c13eb6d996b4c7ba2905a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 11 Sep 2017 14:05:13 +0200 Subject: Reset primary keys after swapping events tables This is required as otherwise newly created events will start with the wrong ID. --- .../20170830131015_swap_event_migration_tables.rb | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/db/migrate/20170830131015_swap_event_migration_tables.rb b/db/migrate/20170830131015_swap_event_migration_tables.rb index 5128d1b2fe7..a256de4a8af 100644 --- a/db/migrate/20170830131015_swap_event_migration_tables.rb +++ b/db/migrate/20170830131015_swap_event_migration_tables.rb @@ -7,6 +7,10 @@ class SwapEventMigrationTables < ActiveRecord::Migration # Set this constant to true if this migration requires downtime. DOWNTIME = false + class Event < ActiveRecord::Base + self.table_name = 'events' + end + def up rename_tables end @@ -19,5 +23,25 @@ class SwapEventMigrationTables < ActiveRecord::Migration rename_table :events, :events_old rename_table :events_for_migration, :events rename_table :events_old, :events_for_migration + + # Once swapped we need to reset the primary key of the new "events" table to + # make sure that data created starts with the right value. This isn't + # necessary for events_for_migration since we replicate existing primary key + # values to it. + if Gitlab::Database.postgresql? + reset_primary_key_for_postgresql + else + reset_primary_key_for_mysql + end + end + + def reset_primary_key_for_postgresql + reset_pk_sequence!(Event.table_name) + end + + def reset_primary_key_for_mysql + amount = Event.pluck('COALESCE(MAX(id), 1)').first + + execute "ALTER TABLE #{Event.table_name} AUTO_INCREMENT = #{amount}" end end -- cgit v1.2.1 From 615ba0071a8bcd3da93f6c27e45128af865cc7bb Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 11 Sep 2017 16:05:14 +0200 Subject: Eager load namespace owners for project dashboards This solves an N+1 query problem where we'd run multiple queries when getting the namespace owners of the displayed projects. --- app/controllers/dashboard/projects_controller.rb | 2 +- changelogs/unreleased/dashboards-projects-controller.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/dashboards-projects-controller.yml diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index f71ab702e71..cd94a36a6e7 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -48,7 +48,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ProjectsFinder .new(params: finder_params, current_user: current_user) .execute - .includes(:route, :creator, namespace: :route) + .includes(:route, :creator, namespace: [:route, :owner]) end def load_events diff --git a/changelogs/unreleased/dashboards-projects-controller.yml b/changelogs/unreleased/dashboards-projects-controller.yml new file mode 100644 index 00000000000..8b350f70a80 --- /dev/null +++ b/changelogs/unreleased/dashboards-projects-controller.yml @@ -0,0 +1,5 @@ +--- +title: Eager load namespace owners for project dashboards +merge_request: +author: +type: other -- cgit v1.2.1 From d70c3bbc149177037490c404a42896d8b486fbe8 Mon Sep 17 00:00:00 2001 From: "micael.bergeron" Date: Fri, 8 Sep 2017 09:30:35 -0400 Subject: make diff file header html safe when file is renamed --- app/views/projects/diffs/_file_header.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 73c316472e3..466a628b746 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -19,10 +19,10 @@ - if diff_file.renamed_file? - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } } - = old_path + != old_path → %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } - = new_path + != new_path - else %strong.file-title-name.has-tooltip{ data: { title: diff_file.file_path, container: 'body' } } = diff_file.file_path -- cgit v1.2.1 From a91101d0ac19e33b0f26acaa4c2f1207789641fb Mon Sep 17 00:00:00 2001 From: "micael.bergeron" Date: Mon, 11 Sep 2017 11:17:59 -0400 Subject: rework the html_safe not to use haml's auto escaping add feature test for inline diff in file header --- app/views/projects/diffs/_file_header.html.haml | 6 +++--- ...-have-escaped-html-for-the-inline-diff-in-the-header.yml | 5 +++++ spec/features/projects/diffs/diff_show_spec.rb | 13 +++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 466a628b746..d5b9f328098 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -17,12 +17,12 @@ = blob_icon diff_file.b_mode, diff_file.file_path - if diff_file.renamed_file? - - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) + - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path).map(&:html_safe) %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } } - != old_path + = old_path → %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } - != new_path + = new_path - else %strong.file-title-name.has-tooltip{ data: { title: diff_file.file_path, container: 'body' } } = diff_file.file_path diff --git a/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml b/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml new file mode 100644 index 00000000000..8c328eb0950 --- /dev/null +++ b/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml @@ -0,0 +1,5 @@ +--- +title: Fix the diff file header from being html escaped for renamed files. +merge_request: 14121 +author: +type: fixed diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb index bc102895aaf..a6f52c9ef58 100644 --- a/spec/features/projects/diffs/diff_show_spec.rb +++ b/spec/features/projects/diffs/diff_show_spec.rb @@ -108,6 +108,19 @@ feature 'Diff file viewer', :js do end end + context 'renamed file' do + before do + visit_commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') + end + + it 'shows the filename with diff highlight' do + within('.file-header-content') do + expect(page).to have_css('.idiff.left.right.deletion') + expect(page).to have_content('files/js/commit.coffee') + end + end + end + context 'binary file that appears to be text in the first 1024 bytes' do before do # The file we're visiting is smaller than 10 KB and we want it collapsed -- cgit v1.2.1 From d96b0eac0303278d4f215770533d09a2aec7955b Mon Sep 17 00:00:00 2001 From: Alex Lossent Date: Mon, 21 Aug 2017 11:51:45 +0200 Subject: Allow to use same periods for housekeeping tasks This enables skipping a lesser housekeeping task (incremental or full repack) by consistently scheduling a higher task (respectively full repack or gc) with the same period. Cf. #34981 --- app/models/application_setting.rb | 4 ++-- .../13711-allow-same-period-housekeeping.yml | 6 ++++++ spec/models/application_setting_spec.rb | 22 ++++++++++++++++++---- .../services/projects/housekeeping_service_spec.rb | 2 +- 4 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/13711-allow-same-period-housekeeping.yml diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3568e72e463..2155bf5223e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -137,11 +137,11 @@ class ApplicationSetting < ActiveRecord::Base validates :housekeeping_full_repack_period, presence: true, - numericality: { only_integer: true, greater_than: :housekeeping_incremental_repack_period } + numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_incremental_repack_period } validates :housekeeping_gc_period, presence: true, - numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period } + numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_full_repack_period } validates :terminal_max_session_time, presence: true, diff --git a/changelogs/unreleased/13711-allow-same-period-housekeeping.yml b/changelogs/unreleased/13711-allow-same-period-housekeeping.yml new file mode 100644 index 00000000000..6749e22cf6a --- /dev/null +++ b/changelogs/unreleased/13711-allow-same-period-housekeeping.yml @@ -0,0 +1,6 @@ +--- +title: Allow to use same periods for different housekeeping tasks (effectively + skipping the lesser task) +merge_request: 13711 +author: @cernvcs +type: added diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index c7a9eabdf06..78cacf9ff5d 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -167,19 +167,33 @@ describe ApplicationSetting do context 'housekeeping settings' do it { is_expected.not_to allow_value(0).for(:housekeeping_incremental_repack_period) } - it 'wants the full repack period to be longer than the incremental repack period' do + it 'wants the full repack period to be at least the incremental repack period' do subject.housekeeping_incremental_repack_period = 2 subject.housekeeping_full_repack_period = 1 expect(subject).not_to be_valid end - it 'wants the gc period to be longer than the full repack period' do - subject.housekeeping_full_repack_period = 2 - subject.housekeeping_gc_period = 1 + it 'wants the gc period to be at least the full repack period' do + subject.housekeeping_full_repack_period = 100 + subject.housekeeping_gc_period = 90 expect(subject).not_to be_valid end + + it 'allows the same period for incremental repack and full repack, effectively skipping incremental repack' do + subject.housekeeping_incremental_repack_period = 2 + subject.housekeeping_full_repack_period = 2 + + expect(subject).to be_valid + end + + it 'allows the same period for full repack and gc, effectively skipping full repack' do + subject.housekeeping_full_repack_period = 100 + subject.housekeeping_gc_period = 100 + + expect(subject).to be_valid + end end end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 437c009e7fa..b7b5de07380 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -75,7 +75,7 @@ describe Projects::HousekeepingService do end end - it 'uses all three kinds of housekeeping we offer' do + it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid) allow(subject).to receive(:lease_key).and_return(:the_lease_key) -- cgit v1.2.1 From 9b177bb7c94df6c7d3868235f75939a41acf8718 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Mon, 11 Sep 2017 15:44:42 +0000 Subject: Revert "Merge branch 'revert-f2421b2b' into 'master'" This reverts merge request !14148 --- app/assets/stylesheets/framework.scss | 1 + app/assets/stylesheets/framework/gitlab-theme.scss | 265 +++++++++++++++++++++ app/assets/stylesheets/framework/header.scss | 1 - app/assets/stylesheets/framework/variables.scss | 39 +++ app/assets/stylesheets/new_nav.scss | 119 +++------ app/assets/stylesheets/new_sidebar.scss | 7 - .../stylesheets/pages/profiles/preferences.scss | 64 +++++ app/controllers/admin/users_controller.rb | 1 + app/controllers/profiles/preferences_controller.rb | 3 +- app/helpers/preferences_helper.rb | 4 + app/models/user.rb | 1 + app/views/layouts/application.html.haml | 2 +- app/views/profiles/preferences/show.html.haml | 20 ++ app/views/profiles/preferences/update.js.erb | 4 + ...d-option-to-change-navigation-color-palette.yml | 5 + config/gitlab.yml.example | 7 + config/initializers/1_settings.rb | 1 + db/migrate/20170816234252_add_theme_id_to_users.rb | 10 + db/schema.rb | 1 + doc/api/keys.md | 1 + doc/api/session.md | 1 + doc/api/users.md | 5 + lib/api/entities.rb | 2 +- lib/gitlab/themes.rb | 84 +++++++ .../profiles/preferences_controller_spec.rb | 6 +- .../api/schemas/public_api/v4/user/login.json | 1 + spec/helpers/preferences_helper_spec.rb | 30 ++- .../migrate_events_to_push_event_payloads_spec.rb | 12 +- spec/lib/gitlab/themes_spec.rb | 48 ++++ ...custom_notification_settings_to_columns_spec.rb | 6 +- spec/models/user_spec.rb | 2 + spec/support/gitlab_stubs/session.json | 2 +- spec/support/gitlab_stubs/user.json | 4 +- 33 files changed, 648 insertions(+), 111 deletions(-) create mode 100644 app/assets/stylesheets/framework/gitlab-theme.scss create mode 100644 changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml create mode 100644 db/migrate/20170816234252_add_theme_id_to_users.rb create mode 100644 lib/gitlab/themes.rb create mode 100644 spec/lib/gitlab/themes_spec.rb diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c0524bf6aa3..35e7a10379f 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -19,6 +19,7 @@ @import "framework/flash"; @import "framework/forms"; @import "framework/gfm"; +@import "framework/gitlab-theme"; @import "framework/header"; @import "framework/highlight"; @import "framework/issue_box"; diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss new file mode 100644 index 00000000000..71f764923ff --- /dev/null +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -0,0 +1,265 @@ +/** + * Styles the GitLab application with a specific color theme + */ + +@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) { + // Header + + header.navbar-gitlab-new { + background: linear-gradient(to right, $color-900, $color-800); + + .navbar-collapse { + color: $color-200; + } + + .container-fluid { + .navbar-toggle { + border-left: 1px solid lighten($color-700, 10%); + } + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + background-color: rgba($color-200, .2); + } + + &.active > a, + &.dropdown.open > a { + color: $color-900; + background-color: $color-alternate; + + svg { + fill: currentColor; + } + } + + &.line-separator { + border-left: 1px solid rgba($color-200, .2); + } + } + } + + .navbar-sub-nav { + color: $color-200; + } + + .nav { + > li { + color: $color-200; + + > a { + svg { + fill: $color-200; + } + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $color-200; + } + } + + &:hover, + &:focus { + @media (min-width: $screen-sm-min) { + background-color: rgba($color-200, .2); + } + + svg { + fill: currentColor; + } + } + } + + &.active > a, + &.dropdown.open > a { + color: $color-900; + background-color: $color-alternate; + + &:hover { + svg { + fill: $color-900; + } + } + } + + .impersonated-user, + .impersonated-user:hover { + svg { + fill: $color-900; + } + } + } + } + } + + .title { + > a { + &:hover, + &:focus { + background-color: rgba($color-200, .2); + } + } + } + + .search { + form { + background-color: rgba($color-200, .2); + + &:hover { + background-color: rgba($color-200, .3); + } + } + + .location-badge { + color: $color-100; + background-color: rgba($color-200, .1); + border-right: 1px solid $color-800; + } + + .search-input::placeholder { + color: rgba($color-200, .8); + } + + .search-input-wrap { + .search-icon, + .clear-icon { + color: rgba($color-200, .8); + } + } + + &.search-active { + form { + background-color: $white-light; + } + + .location-badge { + color: $gl-text-color; + } + + .search-input-wrap { + .search-icon { + color: rgba($color-200, .8); + } + } + } + } + + .btn-sign-in { + background-color: $color-100; + color: $color-900; + } + + + // Sidebar + .nav-sidebar li.active { + box-shadow: inset 4px 0 0 $color-700; + + > a { + color: $color-900; + } + + svg { + fill: $color-900; + } + } +} + + +body { + &.ui_indigo { + @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light); + } + + &.ui_dark { + @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light); + } + + &.ui_blue { + @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light); + } + + &.ui_green { + @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light); + } + + &.ui_light { + @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); + + header.navbar-gitlab-new { + background: $theme-gray-100; + box-shadow: 0 2px 0 0 $border-color; + + .logo-text svg { + fill: $theme-gray-900; + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + color: $theme-gray-900; + } + + &.active > a { + color: $white-light; + + &:hover { + color: $white-light; + } + } + } + } + + .container-fluid { + .navbar-toggle, + .navbar-toggle:hover { + color: $theme-gray-700; + border-left: 1px solid $theme-gray-200; + } + } + } + + .search { + form { + background-color: $white-light; + box-shadow: inset 0 0 0 1px $border-color; + + &:hover { + background-color: $white-light; + box-shadow: inset 0 0 0 1px $blue-100; + + .location-badge { + box-shadow: inset 0 0 0 1px $blue-100; + } + } + } + + .search-input-wrap { + .search-icon { + color: $theme-gray-200; + } + } + + .location-badge { + color: $theme-gray-700; + box-shadow: inset 0 0 0 1px $border-color; + background-color: $nav-badge-bg; + border-right: 0; + } + } + + .nav-sidebar li.active { + > a { + color: $theme-gray-900; + } + + svg { + fill: $theme-gray-900; + } + } + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index b00a2d053e2..ab3c34df1fb 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -111,7 +111,6 @@ header { svg { height: 16px; width: 23px; - fill: currentColor; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index e300b006026..3857226cddb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -74,6 +74,8 @@ $red-700: #a62d19; $red-800: #8b2615; $red-900: #711e11; +// GitLab themes + $indigo-50: #f7f7ff; $indigo-100: #ebebfa; $indigo-200: #d1d1f0; @@ -86,6 +88,43 @@ $indigo-800: #393982; $indigo-900: #292961; $indigo-950: #1a1a40; +$theme-gray-50: #fafafa; +$theme-gray-100: #f2f2f2; +$theme-gray-200: #dfdfdf; +$theme-gray-300: #cccccc; +$theme-gray-400: #bababa; +$theme-gray-500: #a7a7a7; +$theme-gray-600: #949494; +$theme-gray-700: #707070; +$theme-gray-800: #4f4f4f; +$theme-gray-900: #2e2e2e; +$theme-gray-950: #1f1f1f; + +$theme-blue-50: #f4f8fc; +$theme-blue-100: #e6edf5; +$theme-blue-200: #c8d7e6; +$theme-blue-300: #97b3cf; +$theme-blue-400: #648cb4; +$theme-blue-500: #4a79a8; +$theme-blue-600: #3e6fa0; +$theme-blue-700: #305c88; +$theme-blue-800: #25496e; +$theme-blue-900: #1a3652; +$theme-blue-950: #0f2235; + +$theme-green-50: #f2faf6; +$theme-green-100: #e4f3ea; +$theme-green-200: #c0dfcd; +$theme-green-300: #8ac2a1; +$theme-green-400: #52a274; +$theme-green-500: #35935c; +$theme-green-600: #288a50; +$theme-green-700: #1c7441; +$theme-green-800: #145d33; +$theme-green-900: #0d4524; +$theme-green-950: #072d16; + + $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); $almost-black: #242424; diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 2b6c0fc015c..8e095cbdd7e 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -9,10 +9,20 @@ header.navbar-gitlab-new { color: $white-light; - background: linear-gradient(to right, $indigo-900, $indigo-800); border-bottom: 0; min-height: $new-navbar-height; + .logo-text { + line-height: initial; + + svg { + width: 55px; + height: 14px; + margin: 0; + fill: $white-light; + } + } + .header-content { display: -webkit-flex; display: flex; @@ -38,10 +48,10 @@ header.navbar-gitlab-new { img { height: 28px; - margin-right: 10px; + margin-right: 8px; } - > a { + a { display: -webkit-flex; display: flex; align-items: center; @@ -54,22 +64,6 @@ header.navbar-gitlab-new { margin-right: 8px; } } - - .logo-text { - line-height: initial; - - svg { - width: 55px; - height: 14px; - margin: 0; - fill: $white-light; - } - } - - &:hover, - &:focus { - background-color: rgba($indigo-200, .2); - } } } @@ -106,7 +100,6 @@ header.navbar-gitlab-new { .navbar-collapse { padding-left: 0; - color: $indigo-200; box-shadow: 0; @media (max-width: $screen-xs-max) { @@ -132,7 +125,6 @@ header.navbar-gitlab-new { font-size: 14px; text-align: center; color: currentColor; - border-left: 1px solid lighten($indigo-700, 10%); &:hover, &:focus, @@ -167,63 +159,49 @@ header.navbar-gitlab-new { will-change: color; margin: 4px 2px; padding: 6px 8px; - color: $indigo-200; height: 32px; @media (max-width: $screen-xs-max) { padding: 0; } - svg { - fill: $indigo-200; - } - &.header-user-dropdown-toggle { margin-left: 2px; .header-user-avatar { - border-color: $indigo-200; margin-right: 0; } } - } - - .header-new-dropdown-toggle { - margin-right: 0; - } - > a:hover, - > a:focus { - text-decoration: none; - outline: 0; - opacity: 1; - color: $white-light; - - @media (min-width: $screen-sm-min) { - background-color: rgba($indigo-200, .2); - } + &:hover, + &:focus { + text-decoration: none; + outline: 0; + opacity: 1; + color: $white-light; - svg { - fill: currentColor; - } + svg { + fill: currentColor; + } - &.header-user-dropdown-toggle { - .header-user-avatar { - border-color: $white-light; + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; + } } } } + .header-new-dropdown-toggle { + margin-right: 0; + } + .impersonated-user, .impersonated-user:hover { margin-right: 1px; background-color: $white-light; border-top-right-radius: 0; border-bottom-right-radius: 0; - - svg { - fill: $indigo-900; - } } .impersonation-btn, @@ -241,8 +219,6 @@ header.navbar-gitlab-new { &.active > a, &.dropdown.open > a { - color: $indigo-900; - background-color: $white-light; svg { fill: currentColor; @@ -256,7 +232,6 @@ header.navbar-gitlab-new { display: -webkit-flex; display: flex; margin: 0 0 0 6px; - color: $indigo-200; .dropdown-chevron { position: relative; @@ -274,17 +249,6 @@ header.navbar-gitlab-new { text-decoration: none; outline: 0; color: $white-light; - background-color: rgba($indigo-200, .2); - - svg { - fill: currentColor; - } - } - - &.active > a, - &.dropdown.open > a { - color: $indigo-900; - background-color: $white-light; svg { fill: currentColor; @@ -309,7 +273,6 @@ header.navbar-gitlab-new { } &.line-separator { - border-left: 1px solid rgba($indigo-200, .2); margin: 8px; } } @@ -339,17 +302,14 @@ header.navbar-gitlab-new { height: 32px; border: 0; border-radius: $border-radius-default; - background-color: rgba($indigo-200, .2); transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s; &:hover { - background-color: rgba($indigo-200, .3); box-shadow: none; } } &.search-active form { - background-color: $white-light; box-shadow: none; .search-input { @@ -377,43 +337,26 @@ header.navbar-gitlab-new { } .search-input::placeholder { - color: rgba($indigo-200, .8); transition: color ease-in-out 0.15s; } .location-badge { font-size: 12px; - color: $indigo-100; - background-color: rgba($indigo-200, .1); - will-change: color; margin: -4px 4px -4px -4px; line-height: 25px; padding: 4px 8px; border-radius: 2px 0 0 2px; - border-right: 1px solid $indigo-800; height: 32px; transition: border-color ease-in-out 0.15s; } - .search-input-wrap { - .search-icon, - .clear-icon { - color: rgba($indigo-200, .8); - } - } - &.search-active { .location-badge { - color: $gl-text-color; background-color: $nav-badge-bg; border-color: $border-color; } .search-input-wrap { - .search-icon { - color: rgba($indigo-200, .8); - } - .clear-icon { color: $white-light; } @@ -517,8 +460,6 @@ header.navbar-gitlab-new { .btn-sign-in { margin-top: 3px; - background-color: $indigo-100; - color: $indigo-900; font-weight: $gl-font-weight-bold; &:hover { diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index 378ef8926d5..e4c12e46056 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -162,16 +162,9 @@ $new-sidebar-collapsed-width: 50px; } li.active { - box-shadow: inset 4px 0 0 $active-border; - > a { - color: $active-color; font-weight: $gl-font-weight-bold; } - - svg { - fill: $active-color; - } } @media (max-width: $screen-xs-max) { diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index 305feaacaa1..c197494b152 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -1,3 +1,67 @@ +@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) { + .one { + background-color: $color-1; + border-top-left-radius: $border-radius-default; + } + + .two { + background-color: $color-2; + border-top-right-radius: $border-radius-default; + } + + .three { + background-color: $color-3; + border-bottom-left-radius: $border-radius-default; + } + + .four { + background-color: $color-4; + border-bottom-right-radius: $border-radius-default; + } +} + +.application-theme { + label { + margin-right: 20px; + text-align: center; + } + + .preview { + font-size: 0; + margin-bottom: 10px; + + &.indigo { + @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500); + } + + &.dark { + @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600); + } + + &.light { + @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100); + } + + &.blue { + @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500); + } + + &.green { + @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500); + } + } + + .preview-row { + display: block; + } + + .quadrant { + display: inline-block; + height: 50px; + width: 80px; + } +} + .syntax-theme { label { margin-right: 20px; diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 9ec7719fabb..cbcef70e957 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController :provider, :remember_me, :skype, + :theme_id, :twitter, :username, :website_url diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 1e557c47638..cce2a847b53 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController :color_scheme_id, :layout, :dashboard, - :project_view + :project_view, + :theme_id ) end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index d36bb4ab074..0d7347ed30d 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -40,6 +40,10 @@ module PreferencesHelper ] end + def user_application_theme + @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class + end + def user_color_scheme Gitlab::ColorSchemes.for_user(current_user).css_class end diff --git a/app/models/user.rb b/app/models/user.rb index 358b04ac71f..09c9b3250eb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,6 +35,7 @@ class User < ActiveRecord::Base default_value_for :project_view, :files default_value_for :notified_of_own_activity, false default_value_for :preferred_language, I18n.default_locale + default_value_for :theme_id, gitlab_config.default_theme attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 65ac8aaa59b..0ca34b276a7 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: I18n.locale, class: page_class } = render "layouts/head" - %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } + %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } = render "layouts/init_auto_complete" if @gfm_form = render 'peek/bar' = render "layouts/header/default" diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 352c2d66bab..69885008ecd 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -3,6 +3,26 @@ = render 'profiles/head' = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| + .col-lg-4.application-theme + %h4.prepend-top-0 + GitLab navigation theme + %p Customize the appearance of the application header and navigation sidebar. + .col-lg-8.application-theme + - Gitlab::Themes.each do |theme| + = label_tag do + .preview{ class: theme.name.downcase } + .preview-row + .quadrant.one + .quadrant.two + .preview-row + .quadrant.three + .quadrant.four + = f.radio_button :theme_id, theme.id + = theme.name + + .col-sm-12 + %hr + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Syntax highlighting theme diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 431ab9d052b..8966dd3fd86 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -1,3 +1,7 @@ +// Remove body class for any previous theme, re-add current one +$('body').removeClass('<%= Gitlab::Themes.body_classes %>') +$('body').addClass('<%= user_application_theme %>') + // Toggle container-fluid class if ('<%= current_user.layout %>' === 'fluid') { $('.content-wrapper .container-fluid').removeClass('container-limited') diff --git a/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml new file mode 100644 index 00000000000..74aa337a18c --- /dev/null +++ b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml @@ -0,0 +1,5 @@ +--- +title: Add option in preferences to change navigation theme color +merge_request: +author: +type: added diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e9661090844..cd44f888d3f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -76,6 +76,13 @@ production: &base # default_can_create_group: false # default: true # username_changing_enabled: false # default: true - User can change her username/namespace + ## Default theme ID + ## 1 - Indigo + ## 2 - Dark + ## 3 - Light + ## 4 - Blue + ## 5 - Green + # default_theme: 1 # default: 1 ## Automatic issue closing # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8a560d84f1f..94429ee91a9 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -232,6 +232,7 @@ Settings['gitlab'] ||= Settingslogic.new({}) Settings.gitlab['default_projects_limit'] ||= 100000 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? +Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? diff --git a/db/migrate/20170816234252_add_theme_id_to_users.rb b/db/migrate/20170816234252_add_theme_id_to_users.rb new file mode 100644 index 00000000000..5043f9ec591 --- /dev/null +++ b/db/migrate/20170816234252_add_theme_id_to_users.rb @@ -0,0 +1,10 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddThemeIdToUsers < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :users, :theme_id, :integer, limit: 2 + end +end diff --git a/db/schema.rb b/db/schema.rb index df941afa7d7..2149f5ad23d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1608,6 +1608,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.boolean "notified_of_own_activity" t.string "preferred_language" t.string "rss_token" + t.integer "theme_id", limit: 2 end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/api/keys.md b/doc/api/keys.md index 376ac27df3a..ddcf7830621 100644 --- a/doc/api/keys.md +++ b/doc/api/keys.md @@ -32,6 +32,7 @@ Parameters: "twitter": "", "website_url": "", "email": "john@example.com", + "theme_id": 2, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": null, diff --git a/doc/api/session.md b/doc/api/session.md index f79eac11689..b97e26f34a2 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -39,6 +39,7 @@ Example response: "twitter": "", "website_url": "", "email": "john@example.com", + "theme_id": 1, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": "2015-07-07T07:10:58.392Z", diff --git a/doc/api/users.md b/doc/api/users.md index 9f3e4caf2f4..6d5db16b36a 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -72,6 +72,7 @@ GET /users "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -105,6 +106,7 @@ GET /users "organization": "", "last_sign_in_at": null, "confirmed_at": "2012-05-30T16:53:06.148Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 3, "projects_limit": 100, @@ -215,6 +217,7 @@ Parameters: "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -341,6 +344,7 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -387,6 +391,7 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 216408064d1..52c49e5caa9 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -45,7 +45,7 @@ module API expose :confirmed_at expose :last_activity_on expose :email - expose :color_scheme_id, :projects_limit, :current_sign_in_at + expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb new file mode 100644 index 00000000000..d43eff5ba4a --- /dev/null +++ b/lib/gitlab/themes.rb @@ -0,0 +1,84 @@ +module Gitlab + # Module containing GitLab's application theme definitions and helper methods + # for accessing them. + module Themes + extend self + + # Theme ID used when no `default_theme` configuration setting is provided. + APPLICATION_DEFAULT = 1 + + # Struct class representing a single Theme + Theme = Struct.new(:id, :name, :css_class) + + # All available Themes + THEMES = [ + Theme.new(1, 'Indigo', 'ui_indigo'), + Theme.new(2, 'Dark', 'ui_dark'), + Theme.new(3, 'Light', 'ui_light'), + Theme.new(4, 'Blue', 'ui_blue'), + Theme.new(5, 'Green', 'ui_green') + ].freeze + + # Convenience method to get a space-separated String of all the theme + # classes that might be applied to the `body` element + # + # Returns a String + def body_classes + THEMES.collect(&:css_class).uniq.join(' ') + end + + # Get a Theme by its ID + # + # If the ID is invalid, returns the default Theme. + # + # id - Integer ID + # + # Returns a Theme + def by_id(id) + THEMES.detect { |t| t.id == id } || default + end + + # Returns the number of defined Themes + def count + THEMES.size + end + + # Get the default Theme + # + # Returns a Theme + def default + by_id(default_id) + end + + # Iterate through each Theme + # + # Yields the Theme object + def each(&block) + THEMES.each(&block) + end + + # Get the Theme for the specified user, or the default + # + # user - User record + # + # Returns a Theme + def for_user(user) + if user + by_id(user.theme_id) + else + default + end + end + + private + + def default_id + @default_id ||= begin + id = Gitlab.config.gitlab.default_theme.to_i + theme_ids = THEMES.map(&:id) + + theme_ids.include?(id) ? id : APPLICATION_DEFAULT + end + end + end +end diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index a5f544b4f92..a66b4ab0902 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -25,7 +25,8 @@ describe Profiles::PreferencesController do def go(params: {}, format: :js) params.reverse_merge!( color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '1' ) patch :update, user: params, format: format @@ -40,7 +41,8 @@ describe Profiles::PreferencesController do it "changes the user's preferences" do prefs = { color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '2' }.with_indifferent_access expect(user).to receive(:assign_attributes).with(prefs) diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json index 6181b3ccc86..e6c1d9c9d84 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/login.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json @@ -19,6 +19,7 @@ "organization", "last_sign_in_at", "confirmed_at", + "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index a04c87b08eb..8b8080563d3 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe PreferencesHelper do - describe 'dashboard_choices' do + describe '#dashboard_choices' do it 'raises an exception when defined choices may be missing' do expect(User).to receive(:dashboards).and_return(foo: 'foo') expect { helper.dashboard_choices }.to raise_error(RuntimeError) @@ -26,7 +26,33 @@ describe PreferencesHelper do end end - describe 'user_color_scheme' do + describe '#user_application_theme' do + context 'with a user' do + it "returns user's theme's css_class" do + stub_user(theme_id: 3) + + expect(helper.user_application_theme).to eq 'ui_light' + end + + it 'returns the default when id is invalid' do + stub_user(theme_id: Gitlab::Themes.count + 5) + + allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1) + + expect(helper.user_application_theme).to eq 'ui_indigo' + end + end + + context 'without a user' do + it 'returns the default theme' do + stub_user + + expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class + end + end + end + + describe '#user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do allow(helper).to receive(:current_user) diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index b155c20d8d3..cb52d971047 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -215,9 +215,17 @@ end # to a specific version of the database where said table is still present. # describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do + let(:user_class) do + Class.new(ActiveRecord::Base) do + self.table_name = 'users' + end + end + let(:migration) { described_class.new } - let(:project) { create(:project_empty_repo) } - let(:author) { create(:user) } + let(:user_class) { table(:users) } + let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) } + let(:namespace) { create(:namespace, owner: author) } + let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) } # We can not rely on FactoryGirl as the state of Event may change in ways that # the background migration does not expect, hence we use the Event class of diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb new file mode 100644 index 00000000000..ecacea6bb35 --- /dev/null +++ b/spec/lib/gitlab/themes_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Themes, lib: true do + describe '.body_classes' do + it 'returns a space-separated list of class names' do + css = described_class.body_classes + + expect(css).to include('ui_indigo') + expect(css).to include(' ui_dark ') + expect(css).to include(' ui_blue') + end + end + + describe '.by_id' do + it 'returns a Theme by its ID' do + expect(described_class.by_id(1).name).to eq 'Indigo' + expect(described_class.by_id(3).name).to eq 'Light' + end + end + + describe '.default' do + it 'returns the default application theme' do + allow(described_class).to receive(:default_id).and_return(2) + expect(described_class.default.id).to eq 2 + end + + it 'prevents an infinite loop when configuration default is invalid' do + default = described_class::APPLICATION_DEFAULT + themes = described_class::THEMES + + config = double(default_theme: 0).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + + config = double(default_theme: themes.size + 5).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + end + end + + describe '.each' do + it 'passes the block to the THEMES Array' do + ids = [] + described_class.each { |theme| ids << theme.id } + expect(ids).not_to be_empty + end + end +end diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb index 1396d12e5a9..759e77ac9db 100644 --- a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb +++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns') describe ConvertCustomNotificationSettingsToColumns, :migration do + let(:user_class) { table(:users) } + let(:settings_params) do [ { level: 0, events: [:new_note] }, # disabled, single event @@ -19,7 +21,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = create(:user) + user = build(:user).becomes(user_class).tap(&:save!) create_params = { user_id: user.id, level: params[:level], events: events } notification_setting = described_class::NotificationSetting.create(create_params) @@ -35,7 +37,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = create(:user) + user = build(:user).becomes(user_class).tap(&:save!) create_params = events.merge(user_id: user.id, level: params[:level]) notification_setting = described_class::NotificationSetting.create(create_params) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3ba01313efb..c1affa812aa 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -716,6 +716,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) + expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey end end @@ -726,6 +727,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey + expect(user.theme_id).to eq(1) end end diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index cd55d63125e..688175369ae 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index cd55d63125e..ce8dfe5ae75 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -} +} \ No newline at end of file -- cgit v1.2.1 From 82f18eabf3cf5d035508fb23c3f7072f9c7d9a41 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Sep 2017 02:49:32 +0800 Subject: Reset all connection schema cache after migration tests We might also want to consider reduce the number of connections in the tests. However I just tried setting it to 1 and that doesn't seem enough for feature tests. --- spec/support/migrations_helpers.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb index 4ca019c1b05..6522d74ba89 100644 --- a/spec/support/migrations_helpers.rb +++ b/spec/support/migrations_helpers.rb @@ -16,7 +16,9 @@ module MigrationsHelpers end def reset_column_in_migration_models - ActiveRecord::Base.clear_cache! + ActiveRecord::Base.connection_pool.connections.each do |conn| + conn.schema_cache.clear! + end described_class.constants.sort.each do |name| const = described_class.const_get(name) -- cgit v1.2.1 From 25c34608b9ecd73391aaf7fdc66740e43fee5acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?= Date: Wed, 6 Sep 2017 12:55:16 +0200 Subject: Migrate Git::CommitStats to Gitaly --- lib/gitlab/git/commit.rb | 2 +- lib/gitlab/git/commit_stats.rb | 19 +++++++++++++++- lib/gitlab/gitaly_client/commit_service.rb | 8 +++++++ spec/lib/gitlab/git/commit_spec.rb | 10 ++++++++- .../gitlab/gitaly_client/commit_service_spec.rb | 25 ++++++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 5ee6669050c..1f370686186 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -352,7 +352,7 @@ module Gitlab end def stats - Gitlab::Git::CommitStats.new(self) + Gitlab::Git::CommitStats.new(@repository, self) end def to_patch(options = {}) diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb index 00acb4763e9..6bf49a0af18 100644 --- a/lib/gitlab/git/commit_stats.rb +++ b/lib/gitlab/git/commit_stats.rb @@ -10,12 +10,29 @@ module Gitlab # Instantiate a CommitStats object # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/323 - def initialize(commit) + def initialize(repo, commit) @id = commit.id @additions = 0 @deletions = 0 @total = 0 + repo.gitaly_migrate(:commit_stats) do |is_enabled| + if is_enabled + gitaly_stats(repo, commit) + else + rugged_stats(commit) + end + end + end + + def gitaly_stats(repo, commit) + stats = repo.gitaly_commit_client.commit_stats(@id) + @additions = stats.additions + @deletions = stats.deletions + @total = @additions + @deletions + end + + def rugged_stats(commit) diff = commit.rugged_diff_from_parent diff.each_patch do |p| diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 0825a3a7694..1ba1a7830a4 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -204,6 +204,14 @@ module Gitlab response.sum(&:data) end + def commit_stats(revision) + request = Gitaly::CommitStatsRequest.new( + repository: @gitaly_repo, + revision: GitalyClient.encode(revision) + ) + GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request) + end + private def commit_diff_request_params(commit, options = {}) diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 14d64d8c4da..46e968cc398 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -401,7 +401,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe '#stats' do + shared_examples '#stats' do subject { commit.stats } describe '#additions' do @@ -415,6 +415,14 @@ describe Gitlab::Git::Commit, seed_helper: true do end end + describe '#stats with gitaly on' do + it_should_behave_like '#stats' + end + + describe '#stats with gitaly disabled', skip_gitaly_mock: true do + it_should_behave_like '#stats' + end + describe '#to_diff' do subject { commit.to_diff } diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index f32fe5d8150..ec3abcb0953 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -165,4 +165,29 @@ describe Gitlab::GitalyClient::CommitService do expect(subject).to eq("my diff") end end + + describe '#commit_stats' do + let(:request) do + Gitaly::CommitStatsRequest.new( + repository: repository_message, revision: revision + ) + end + let(:response) do + Gitaly::CommitStatsResponse.new( + oid: revision, + additions: 11, + deletions: 15 + ) + end + + subject { described_class.new(repository).commit_stats(revision) } + + it 'sends an RPC request' do + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:commit_stats) + .with(request, kind_of(Hash)).and_return(response) + + expect(subject.additions).to eq(11) + expect(subject.deletions).to eq(15) + end + end end -- cgit v1.2.1 From d912b49320417f5b3edde00a703adb8550a165da Mon Sep 17 00:00:00 2001 From: Jedidiah Date: Mon, 11 Sep 2017 20:23:51 +0000 Subject: Make all the tooltips in the same direction on the commit info box --- app/views/projects/commits/_commit.html.haml | 2 +- changelogs/unreleased/consistent-tooltip-direction-on-commits.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/consistent-tooltip-direction-on-commits.yml diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index b8655808d89..a16ffb433a5 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -32,7 +32,7 @@ .commiter - commit_author_link = commit_author_link(commit, avatar: false, size: 24) - - commit_timeago = time_ago_with_tooltip(commit.committed_date) + - commit_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom') - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } #{ commit_text.html_safe } diff --git a/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml b/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml new file mode 100644 index 00000000000..9e6a429f6f0 --- /dev/null +++ b/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml @@ -0,0 +1,5 @@ +--- +title: Tooltips in the commit info box now all face the same direction +merge_request: +author: Jedidiah Broadbent +type: fixed -- cgit v1.2.1 From c0b0e506e3bb713e5406c7f2052f01674f3d7dab Mon Sep 17 00:00:00 2001 From: "Vitaliy @blackst0ne Klachkov" Date: Tue, 12 Sep 2017 07:41:57 +1100 Subject: Replace the project/milestone.feature spinach test with an rspec analog --- .../unreleased/replace_milestone-feature.yml | 5 ++ features/project/milestone.feature | 16 ------ features/steps/project/project_milestone.rb | 62 ---------------------- .../milestones/user_interacts_with_labels_spec.rb | 40 ++++++++++++++ 4 files changed, 45 insertions(+), 78 deletions(-) create mode 100644 changelogs/unreleased/replace_milestone-feature.yml delete mode 100644 features/project/milestone.feature delete mode 100644 features/steps/project/project_milestone.rb create mode 100644 spec/features/projects/milestones/user_interacts_with_labels_spec.rb diff --git a/changelogs/unreleased/replace_milestone-feature.yml b/changelogs/unreleased/replace_milestone-feature.yml new file mode 100644 index 00000000000..effe6d65645 --- /dev/null +++ b/changelogs/unreleased/replace_milestone-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the project/milestone.feature spinach test with an rspec analog +merge_request: 14171 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/features/project/milestone.feature b/features/project/milestone.feature deleted file mode 100644 index 5e7b211fa27..00000000000 --- a/features/project/milestone.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Project Milestone - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has labels: "bug", "feature", "enhancement" - And project "Shop" has milestone "v2.2" - And milestone has issue "Bugfix1" with labels: "bug", "feature" - And milestone has issue "Bugfix2" with labels: "bug", "enhancement" - - @javascript - Scenario: Listing labels from labels tab - Given I visit project "Shop" milestones page - And I click link "v2.2" - And I click link "Labels" - Then I should see the list of labels - And I should see the labels "bug", "enhancement" and "feature" diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb deleted file mode 100644 index b2d08515e77..00000000000 --- a/features/steps/project/project_milestone.rb +++ /dev/null @@ -1,62 +0,0 @@ -class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include WaitForRequests - - step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do - project = Project.find_by(name: "Shop") - milestone = project.milestones.find_by(title: 'v2.2') - issue = create(:issue, title: "Bugfix1", project: project, milestone: milestone) - issue.labels << project.labels.find_by(title: 'bug') - issue.labels << project.labels.find_by(title: 'feature') - end - - step 'milestone has issue "Bugfix2" with labels: "bug", "enhancement"' do - project = Project.find_by(name: "Shop") - milestone = project.milestones.find_by(title: 'v2.2') - issue = create(:issue, title: "Bugfix2", project: project, milestone: milestone) - issue.labels << project.labels.find_by(title: 'bug') - issue.labels << project.labels.find_by(title: 'enhancement') - end - - step 'project "Shop" has milestone "v2.2"' do - project = Project.find_by(name: "Shop") - milestone = create(:milestone, - title: "v2.2", - project: project, - description: "# Description header" - ) - 3.times { create(:issue, project: project, milestone: milestone) } - end - - step 'I should see the list of labels' do - expect(page).to have_selector('ul.manage-labels-list') - end - - step 'I should see the labels "bug", "enhancement" and "feature"' do - wait_for_requests - - page.within('#tab-issues') do - expect(page).to have_content 'bug' - expect(page).to have_content 'enhancement' - expect(page).to have_content 'feature' - end - end - - step 'I should see the "bug" label listed only once' do - page.within('#tab-labels') do - expect(page).to have_content('bug', count: 1) - end - end - - step 'I click link "v2.2"' do - click_link "v2.2" - end - - step 'I click link "Labels"' do - page.within('.nav-sidebar') do - page.find(:xpath, "//a[@href='#tab-labels']").click - end - end -end diff --git a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb new file mode 100644 index 00000000000..f6a82f80d65 --- /dev/null +++ b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'User interacts with labels' do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:milestone) { create(:milestone, project: project, title: 'v2.2', description: '# Description header') } + let(:issue1) { create(:issue, project: project, title: 'Bugfix1', milestone: milestone) } + let(:issue2) { create(:issue, project: project, title: 'Bugfix2', milestone: milestone) } + let(:label_bug) { create(:label, project: project, title: 'bug') } + let(:label_feature) { create(:label, project: project, title: 'feature') } + let(:label_enhancement) { create(:label, project: project, title: 'enhancement') } + + before do + project.add_master(user) + sign_in(user) + + issue1.labels << [label_bug, label_feature] + issue2.labels << [label_bug, label_enhancement] + + visit(project_milestones_path(project)) + end + + it 'shows the list of labels', :js do + click_link('v2.2') + + page.within('.nav-sidebar') do + page.find(:xpath, "//a[@href='#tab-labels']").click + end + + expect(page).to have_selector('ul.manage-labels-list') + + wait_for_requests + + page.within('#tab-labels') do + expect(page).to have_content(label_bug.title) + expect(page).to have_content(label_enhancement.title) + expect(page).to have_content(label_feature.title) + end + end +end -- cgit v1.2.1 From b46d5b13ecb8e0c0793fa433bff7f49cb0612760 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Fri, 8 Sep 2017 15:48:10 +0100 Subject: Backport more EE changes to Gitlab::UrlSanitizer --- lib/gitlab/url_sanitizer.rb | 25 +++++++++++++++++++++---- spec/lib/gitlab/url_sanitizer_spec.rb | 9 +++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb index 703adae12cb..4e1ec1402ea 100644 --- a/lib/gitlab/url_sanitizer.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -19,13 +19,12 @@ module Gitlab end def initialize(url, credentials: nil) - @url = Addressable::URI.parse(url.to_s.strip) - %i[user password].each do |symbol| credentials[symbol] = credentials[symbol].presence if credentials&.key?(symbol) end @credentials = credentials + @url = parse_url(url) end def sanitized_url @@ -49,12 +48,30 @@ module Gitlab private + def parse_url(url) + url = url.to_s.strip + match = url.match(%r{\A(?:git|ssh|http(?:s?))\://(?:(.+)(?:@))?(.+)}) + raw_credentials = match[1] if match + + if raw_credentials.present? + url.sub!("#{raw_credentials}@", '') + + user, password = raw_credentials.split(':') + @credentials ||= { user: user.presence, password: password.presence } + end + + url = Addressable::URI.parse(url) + url.password = password if password.present? + url.user = user if user.present? + url + end + def generate_full_url return @url unless valid_credentials? @full_url = @url.dup - @full_url.password = credentials[:password] - @full_url.user = credentials[:user] + @full_url.password = credentials[:password] if credentials[:password].present? + @full_url.user = credentials[:user] if credentials[:user].present? @full_url end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index fdc3990132a..59c28431e1e 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -174,4 +174,13 @@ describe Gitlab::UrlSanitizer do end end end + + context 'when credentials contains special chars' do + it 'should parse the URL without errors' do + url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git") + + expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") + expect(url_sanitizer.full_url).to eq("https://foo:b?r@github.com/me/project.git") + end + end end -- cgit v1.2.1 From 6ae27381da5ac2dd4dfbd7c716c4940658efbb9c Mon Sep 17 00:00:00 2001 From: Victor Wu Date: Mon, 11 Sep 2017 20:50:45 +0000 Subject: Docs group mrs list view search bar --- .../img/group_merge_requests_list_view.png | Bin 283066 -> 89620 bytes doc/user/search/index.md | 2 -- 2 files changed, 2 deletions(-) diff --git a/doc/user/project/merge_requests/img/group_merge_requests_list_view.png b/doc/user/project/merge_requests/img/group_merge_requests_list_view.png index 02a88d0112f..7d0756505db 100644 Binary files a/doc/user/project/merge_requests/img/group_merge_requests_list_view.png and b/doc/user/project/merge_requests/img/group_merge_requests_list_view.png differ diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 21e96d8b11c..bcc3625f908 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -63,8 +63,6 @@ the same way as you do for projects. ![filter issues in a group](img/group_issues_filter.png) The same process is valid for merge requests. Navigate to your project's **Merge Requests** tab. -The search and filter UI currently uses dropdowns. In a future release, the same -dynamic UI as above will be carried over here. ## Search history -- cgit v1.2.1 From 44a1759f191ef6b148eec93d2712897248e9f8e1 Mon Sep 17 00:00:00 2001 From: "Vitaliy @blackst0ne Klachkov" Date: Tue, 12 Sep 2017 11:52:35 +1100 Subject: Replace the 'project/merge_requests/revert.feature' spinach test with an rspec analog --- ...place_project_merge_requests_revert-feature.yml | 6 +++ features/project/merge_requests/revert.feature | 29 ----------- features/steps/project/merge_requests/revert.rb | 56 -------------------- .../user_reverts_merge_request_spec.rb | 59 ++++++++++++++++++++++ 4 files changed, 65 insertions(+), 85 deletions(-) create mode 100644 changelogs/unreleased/replace_project_merge_requests_revert-feature.yml delete mode 100644 features/project/merge_requests/revert.feature delete mode 100644 features/steps/project/merge_requests/revert.rb create mode 100644 spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb diff --git a/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml b/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml new file mode 100644 index 00000000000..7d1ab4566b6 --- /dev/null +++ b/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml @@ -0,0 +1,6 @@ +--- +title: Replace the 'project/merge_requests/revert.feature' spinach test with an rspec + analog +merge_request: 14201 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature deleted file mode 100644 index aaac5fd7209..00000000000 --- a/features/project/merge_requests/revert.feature +++ /dev/null @@ -1,29 +0,0 @@ -@project_merge_requests -Feature: Revert Merge Requests - Background: - Given There is an open Merge Request - And I am signed in as a developer of the project - And I am on the Merge Request detail page - And I click on Accept Merge Request - And I am on the Merge Request detail page - - @javascript - Scenario: I revert a merge request - Given I click on the revert button - And I revert the changes directly - Then I should see the revert merge request notice - - @javascript - Scenario: I revert a merge request that was previously reverted - Given I click on the revert button - And I revert the changes directly - And I am on the Merge Request detail page - And I click on the revert button - And I revert the changes directly - Then I should see a revert error - - @javascript - Scenario: I revert a merge request in a new merge request - Given I click on the revert button - And I revert the changes in a new merge request - Then I should see the new merge request notice diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb deleted file mode 100644 index 25ccf5ab180..00000000000 --- a/features/steps/project/merge_requests/revert.rb +++ /dev/null @@ -1,56 +0,0 @@ -class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps - include LoginHelpers - include WaitForRequests - - step 'I click on the revert button' do - find("a[href='#modal-revert-commit']").click - end - - step 'I revert the changes directly' do - page.within('#modal-revert-commit') do - uncheck 'create_merge_request' - click_button 'Revert' - end - end - - step 'I should see the revert merge request notice' do - page.should have_content('The merge request has been successfully reverted.') - wait_for_requests - end - - step 'I should not see the revert button' do - expect(page).not_to have_selector(:xpath, "a[href='#modal-revert-commit']") - end - - step 'I am on the Merge Request detail page' do - visit merge_request_path(@merge_request) - end - - step 'I click on Accept Merge Request' do - click_button('Merge') - end - - step 'I am signed in as a developer of the project' do - @user = create(:user) { |u| @project.add_developer(u) } - sign_in(@user) - end - - step 'There is an open Merge Request' do - @merge_request = create(:merge_request, :with_diffs, :simple) - @project = @merge_request.source_project - end - - step 'I should see a revert error' do - page.should have_content('Sorry, we cannot revert this merge request automatically.') - end - - step 'I revert the changes in a new merge request' do - page.within('#modal-revert-commit') do - click_button 'Revert' - end - end - - step 'I should see the new merge request notice' do - page.should have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.') - end -end diff --git a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb new file mode 100644 index 00000000000..a41d683dbbb --- /dev/null +++ b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe 'User reverts a merge request', :js do + let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) } + let(:project) { create(:project, :public, :repository) } + let(:user) { create(:user) } + + before do + project.add_developer(user) + sign_in(user) + + visit(merge_request_path(merge_request)) + + click_button('Merge') + + visit(merge_request_path(merge_request)) + end + + it 'reverts a merge request' do + find("a[href='#modal-revert-commit']").click + + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + + expect(page).to have_content('The merge request has been successfully reverted.') + + wait_for_requests + end + + it 'does not revert a merge request that was previously reverted' do + find("a[href='#modal-revert-commit']").click + + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + + find("a[href='#modal-revert-commit']").click + + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + + expect(page).to have_content('Sorry, we cannot revert this merge request automatically.') + end + + it 'reverts a merge request in a new merge request' do + find("a[href='#modal-revert-commit']").click + + page.within('#modal-revert-commit') do + click_button('Revert') + end + + expect(page).to have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.') + end +end -- cgit v1.2.1 From cc28abeafdd7f32fa9e5d4e0f2b7c82271067796 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 5 Sep 2017 07:05:23 +0200 Subject: Detect orphaned repositories and namespaces in any storage --- lib/system_check/orphans/namespace_check.rb | 37 ++++++++++++++++++++ lib/system_check/orphans/repository_check.rb | 50 ++++++++++++++++++++++++++++ lib/tasks/gitlab/check.rake | 30 +++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 lib/system_check/orphans/namespace_check.rb create mode 100644 lib/system_check/orphans/repository_check.rb diff --git a/lib/system_check/orphans/namespace_check.rb b/lib/system_check/orphans/namespace_check.rb new file mode 100644 index 00000000000..1963cbfc985 --- /dev/null +++ b/lib/system_check/orphans/namespace_check.rb @@ -0,0 +1,37 @@ +module SystemCheck + module Orphans + class NamespaceCheck < SystemCheck::BaseCheck + set_name 'Orphaned namespaces:' + + def multi_check + Gitlab.config.repositories.storages.each do |name, repository_storage| + $stdout.puts + $stdout.puts "* Storage: #{name} (#{repository_storage['path']})".color(:yellow) + toplevel_namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*')).map{|p| File.basename(p)} + + orphans = (toplevel_namespace_dirs - existing_namespaces) + if orphans.empty? + $stdout.puts "* No orphaned namespaces for #{name} storage".color(:green) + next + end + + orphans.each do |orphan| + $stdout.puts " - #{orphan}".color(:red) + end + end + + clear_namespaces! # releases memory when check finishes + end + + private + + def existing_namespaces + @namespaces ||= Namespace.all.pluck(:path) + end + + def clear_namespaces! + @namespaces = nil + end + end + end +end diff --git a/lib/system_check/orphans/repository_check.rb b/lib/system_check/orphans/repository_check.rb new file mode 100644 index 00000000000..1de6bec165b --- /dev/null +++ b/lib/system_check/orphans/repository_check.rb @@ -0,0 +1,50 @@ +module SystemCheck + module Orphans + class RepositoryCheck < SystemCheck::BaseCheck + set_name 'Orphaned repositories:' + + def multi_check + Gitlab.config.repositories.storages.each do |name, repository_storage| + $stdout.puts + $stdout.puts "* Storage: #{name} (#{repository_storage['path']})".color(:yellow) + + repositories = toplevel_namespace_dirs(repository_storage['path']).map do |path| + namespace = File.basename(path) + Dir.glob(File.join(path, '*')).map {|repo| "#{namespace}/#{File.basename(repo)}"} + end.try(:flatten!) + + orphans = (repositories - list_repositories(name)) + if orphans.empty? + $stdout.puts "* No orphaned repositories for #{name} storage".color(:green) + next + end + + orphans.each do |orphan| + $stdout.puts " - #{orphan}".color(:red) + end + end + end + + private + + def list_repositories(storage_name) + sql = " + SELECT + CONCAT(n.path, '/', p.path, '.git') repo, + CONCAT(n.path, '/', p.path, '.wiki.git') wiki + FROM projects p + JOIN namespaces n + ON (p.namespace_id = n.id) + WHERE (p.repository_storage LIKE ?) + " + + query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, storage_name]) + ActiveRecord::Base.connection.select_all(query).rows.try(:flatten!) + end + + def toplevel_namespace_dirs(storage_path) + Dir.glob(File.join(storage_path, '*')) + end + end + end +end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 654f638c454..b1c63c4b2a6 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -398,6 +398,36 @@ namespace :gitlab do end end + namespace :orphans do + desc 'Gitlab | Check for orphaned namespaces and repositories' + task check: :environment do + warn_user_is_not_gitlab + checks = [ + SystemCheck::Orphans::NamespaceCheck, + SystemCheck::Orphans::RepositoryCheck + ] + + SystemCheck.run('Orphans', checks) + end + + desc 'GitLab | Check for orphaned namespaces in the repositories path' + task check_namespaces: :environment do + warn_user_is_not_gitlab + checks = [SystemCheck::Orphans::NamespaceCheck] + + SystemCheck.run('Orphans', checks) + end + + desc 'GitLab | Check for orphaned repositories in the repositories path' + task check_repositories: :environment do + warn_user_is_not_gitlab + checks = [SystemCheck::Orphans::RepositoryCheck] + + SystemCheck.run('Orphans', checks) + end + end + + namespace :user do desc "GitLab | Check the integrity of a specific user's repositories" task :check_repos, [:username] => :environment do |t, args| -- cgit v1.2.1 From c68adcba4a77d436baa2a2a6def5c8ed794365bc Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Mon, 11 Sep 2017 23:28:39 -0400 Subject: Minor improvements --- doc/user/project/integrations/prometheus_library/nginx_ingress.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md index 2a37cbd160b..17a47cfa646 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -11,7 +11,7 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI | Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) | | HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) | -## Configuring Prometheus to monitor for NGINX ingress metrics +## Configuring NGINX ingress monitoring If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, and your application is running in the same cluster, no further action is required. The ingress metrics will be automatically enabled and annotated for Prometheus monitoring. Simply ensure Prometheus monitoring is [enabled for your project](../prometheus.md), which is on by default. @@ -20,14 +20,14 @@ For other deployments, there is some configuration required depending on your in * NGINX Ingress should be annotated for Prometheus monitoring * Prometheus should be configured to monitor annotated pods -### Configuring NGINX Ingress for Prometheus monitoring +### Setting up NGINX Ingress for Prometheus monitoring Version 0.9.0 and above of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx) have built-in support for exporting Prometheus metrics. To enable, a ConfigMap setting must be passed: `enable-vts-status: "true"`. Once enabled, a Prometheus metrics endpoint will start running on port 10254. -With metric data now available, Prometheus needs to be configured to collect it. The easiest way to do this is to leverage Prometheus' [built-in Kubernetes service discovery](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config), which automatically detects a variety of Kubernetes components and makes them available for monitoring. NGINX ingress metrics are exposed per pod, a sample scrape configuration [is available](https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml#L248). This configuration will detect pods and enable collection of metrics **only if** they have been specifically annotated for monitoring. +With metric data now available, Prometheus needs to be configured to collect it. The easiest way to do this is to leverage Prometheus' [built-in Kubernetes service discovery](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config), which automatically detects a variety of Kubernetes components and makes them available for monitoring. Since NGINX ingress metrics are exposed per pod, a scrape job for Kubernetes pods is required. A sample pod scraping configuration [is available](https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml#L248). This configuration will detect pods and enable collection of metrics **only if** they have been specifically annotated for monitoring. Depending on how NGINX ingress was deployed, typically a DaemonSet or Deployment, edit the corresponding YML spec. Two new annotations need to be added: -* `prometheus.io/port: "true"` +* `prometheus.io/scrape: "true"` * `prometheus.io/port: "10254"` Prometheus should now be collecting NGINX ingress metrics. To validate view the Prometheus Targets, available under `Status > Targets` on the Prometheus dashboard. New entries for NGINX should be listed in the kubernetes pod monitoring job, `kubernetes-pods`. -- cgit v1.2.1 From 2c489f8289e5e887d86be32e2bf59bad22a19af4 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 12 Sep 2017 06:57:22 +0200 Subject: Refactor on namespace and repository checks and added specs --- lib/system_check/orphans/namespace_check.rb | 41 +++++++++---- lib/system_check/orphans/repository_check.rb | 58 +++++++++++------- lib/tasks/gitlab/check.rake | 1 - .../system_check/orphans/namespace_check_spec.rb | 61 +++++++++++++++++++ .../system_check/orphans/repository_check_spec.rb | 68 ++++++++++++++++++++++ 5 files changed, 196 insertions(+), 33 deletions(-) create mode 100644 spec/lib/system_check/orphans/namespace_check_spec.rb create mode 100644 spec/lib/system_check/orphans/repository_check_spec.rb diff --git a/lib/system_check/orphans/namespace_check.rb b/lib/system_check/orphans/namespace_check.rb index 1963cbfc985..b8446300f72 100644 --- a/lib/system_check/orphans/namespace_check.rb +++ b/lib/system_check/orphans/namespace_check.rb @@ -4,20 +4,13 @@ module SystemCheck set_name 'Orphaned namespaces:' def multi_check - Gitlab.config.repositories.storages.each do |name, repository_storage| + Gitlab.config.repositories.storages.each do |storage_name, repository_storage| $stdout.puts - $stdout.puts "* Storage: #{name} (#{repository_storage['path']})".color(:yellow) - toplevel_namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*')).map{|p| File.basename(p)} + $stdout.puts "* Storage: #{storage_name} (#{repository_storage['path']})".color(:yellow) + toplevel_namespace_dirs = disk_namespaces(repository_storage['path']) orphans = (toplevel_namespace_dirs - existing_namespaces) - if orphans.empty? - $stdout.puts "* No orphaned namespaces for #{name} storage".color(:green) - next - end - - orphans.each do |orphan| - $stdout.puts " - #{orphan}".color(:red) - end + print_orphans(orphans, storage_name) end clear_namespaces! # releases memory when check finishes @@ -25,8 +18,32 @@ module SystemCheck private + def print_orphans(orphans, storage_name) + if orphans.empty? + $stdout.puts "* No orphaned namespaces for #{storage_name} storage".color(:green) + return + end + + orphans.each do |orphan| + $stdout.puts " - #{orphan}".color(:red) + end + end + + def disk_namespaces(storage_path) + fetch_disk_namespaces(storage_path).each_with_object([]) do |namespace_path, result| + namespace = File.basename(namespace_path) + next if namespace.eql?('@hashed') + + result << namespace + end + end + + def fetch_disk_namespaces(storage_path) + Dir.glob(File.join(storage_path, '*')) + end + def existing_namespaces - @namespaces ||= Namespace.all.pluck(:path) + @namespaces ||= Namespace.where(parent: nil).all.pluck(:path) end def clear_namespaces! diff --git a/lib/system_check/orphans/repository_check.rb b/lib/system_check/orphans/repository_check.rb index 1de6bec165b..9b6b2429783 100644 --- a/lib/system_check/orphans/repository_check.rb +++ b/lib/system_check/orphans/repository_check.rb @@ -2,49 +2,67 @@ module SystemCheck module Orphans class RepositoryCheck < SystemCheck::BaseCheck set_name 'Orphaned repositories:' + attr_accessor :orphans def multi_check - Gitlab.config.repositories.storages.each do |name, repository_storage| + Gitlab.config.repositories.storages.each do |storage_name, repository_storage| $stdout.puts - $stdout.puts "* Storage: #{name} (#{repository_storage['path']})".color(:yellow) + $stdout.puts "* Storage: #{storage_name} (#{repository_storage['path']})".color(:yellow) - repositories = toplevel_namespace_dirs(repository_storage['path']).map do |path| - namespace = File.basename(path) - Dir.glob(File.join(path, '*')).map {|repo| "#{namespace}/#{File.basename(repo)}"} - end.try(:flatten!) + repositories = disk_repositories(repository_storage['path']) + orphans = (repositories - fetch_repositories(storage_name)) - orphans = (repositories - list_repositories(name)) - if orphans.empty? - $stdout.puts "* No orphaned repositories for #{name} storage".color(:green) - next - end - - orphans.each do |orphan| - $stdout.puts " - #{orphan}".color(:red) - end + print_orphans(orphans, storage_name) end end private - def list_repositories(storage_name) + def print_orphans(orphans, storage_name) + if orphans.empty? + $stdout.puts "* No orphaned repositories for #{storage_name} storage".color(:green) + return + end + + orphans.each do |orphan| + $stdout.puts " - #{orphan}".color(:red) + end + end + + def disk_repositories(storage_path) + fetch_disk_namespaces(storage_path).each_with_object([]) do |namespace_path, result| + namespace = File.basename(namespace_path) + next if namespace.eql?('@hashed') + + fetch_disk_repositories(namespace_path).each do |repo| + result << "#{namespace}/#{File.basename(repo)}" + end + end + end + + def fetch_repositories(storage_name) sql = " SELECT CONCAT(n.path, '/', p.path, '.git') repo, CONCAT(n.path, '/', p.path, '.wiki.git') wiki FROM projects p JOIN namespaces n - ON (p.namespace_id = n.id) + ON (p.namespace_id = n.id AND + n.parent_id IS NULL) WHERE (p.repository_storage LIKE ?) " - query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, storage_name]) - ActiveRecord::Base.connection.select_all(query).rows.try(:flatten!) + query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, storage_name]) # rubocop:disable GitlabSecurity/PublicSend + ActiveRecord::Base.connection.select_all(query).rows.try(:flatten!) || [] end - def toplevel_namespace_dirs(storage_path) + def fetch_disk_namespaces(storage_path) Dir.glob(File.join(storage_path, '*')) end + + def fetch_disk_repositories(namespace_path) + Dir.glob(File.join(namespace_path, '*')) + end end end end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index b1c63c4b2a6..dfade1f3885 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -427,7 +427,6 @@ namespace :gitlab do end end - namespace :user do desc "GitLab | Check the integrity of a specific user's repositories" task :check_repos, [:username] => :environment do |t, args| diff --git a/spec/lib/system_check/orphans/namespace_check_spec.rb b/spec/lib/system_check/orphans/namespace_check_spec.rb new file mode 100644 index 00000000000..2a61ff3ad65 --- /dev/null +++ b/spec/lib/system_check/orphans/namespace_check_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck::Orphans::NamespaceCheck do + let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } } + + before do + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces) + silence_output + end + + describe '#multi_check' do + context 'all orphans' do + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) } + + it 'prints list of all orphaned namespaces except @hashed' do + expect_list_of_orphans(%w(orphan1 orphan2)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) } + + it 'prints list of orphaned namespaces' do + expect_list_of_orphans(%w(orphan1 orphan2)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace and parents with same name as orphans' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:second_level) { create(:group, path: 'second-level', parent: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) } + + it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do + expect_list_of_orphans(%w(orphan1 orphan2 second-level)) + + subject.multi_check + end + end + + context 'no orphans' do + let(:disk_namespaces) { %w(@hashed) } + + it 'prints an empty list ignoring @hashed' do + expect_list_of_orphans([]) + + subject.multi_check + end + end + end + + def expect_list_of_orphans(orphans) + expect(subject).to receive(:print_orphans).with(orphans, 'default') + end +end diff --git a/spec/lib/system_check/orphans/repository_check_spec.rb b/spec/lib/system_check/orphans/repository_check_spec.rb new file mode 100644 index 00000000000..b0c2267d177 --- /dev/null +++ b/spec/lib/system_check/orphans/repository_check_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck::Orphans::RepositoryCheck do + let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } } + + before do + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces) + allow(subject).to receive(:fetch_disk_repositories).and_return(disk_repositories) + # silence_output + end + + describe '#multi_check' do + context 'all orphans' do + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) } + let(:disk_repositories) { %w(repo1.git repo2.git) } + + it 'prints list of all orphaned namespaces except @hashed' do + expect_list_of_orphans(%w(orphan1/repo1.git orphan1/repo2.git orphan2/repo1.git orphan2/repo2.git)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:project) { create(:project, path: 'repo', namespace: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints list of orphaned namespaces' do + expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace and parents with same name as orphans' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:second_level) { create(:group, path: 'second-level', parent: first_level) } + let!(:project) { create(:project, path: 'repo', namespace: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do + expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git second-level/repo.git)) + + subject.multi_check + end + end + + context 'no orphans' do + let(:disk_namespaces) { %w(@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints an empty list ignoring @hashed' do + expect_list_of_orphans([]) + + subject.multi_check + end + end + end + + def expect_list_of_orphans(orphans) + expect(subject).to receive(:print_orphans).with(orphans, 'default') + end +end -- cgit v1.2.1 From 162dc635c7d659c442e3f21ed2a36b91fcebbe1a Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 12 Sep 2017 07:16:50 +0200 Subject: Changelog --- changelogs/unreleased/detect-orphaned-repositories.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/detect-orphaned-repositories.yml diff --git a/changelogs/unreleased/detect-orphaned-repositories.yml b/changelogs/unreleased/detect-orphaned-repositories.yml new file mode 100644 index 00000000000..101c1897826 --- /dev/null +++ b/changelogs/unreleased/detect-orphaned-repositories.yml @@ -0,0 +1,5 @@ +--- +title: Scripts to detect orphaned repositories +merge_request: 14204 +author: +type: added -- cgit v1.2.1 From bb001ac3e5685294b500922a0ff807497c225a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 12 Sep 2017 09:04:20 +0200 Subject: Use WikiPages::CreateService in spec/features/projects/wiki/user_updates_wiki_page_spec.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/features/projects/wiki/user_updates_wiki_page_spec.rb | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index cfd6f3aa71f..1cf14204159 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -58,18 +58,8 @@ describe 'User updates wiki page' do end context 'when wiki is not empty' do - # This facory call is shorter: - # - # create(:wiki_page, wiki: create(:project, namespace: user.namespace).wiki, attrs: { title: 'home', content: 'Home page' }) - # - # But it always fails with this: - # - # Failure/Error: click_link('Edit') - # Capybara::ElementNotFound: - # Unable to find visible link "Edit" - - let(:project) { create(:project, namespace: user.namespace) } - let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' }) } + let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } + let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: 'home', content: 'Home page' }) } before do visit(project_wikis_path(project)) -- cgit v1.2.1 From 2066a12d71c00ce525e47c139729b5ce80ca3f5c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 12 Sep 2017 10:46:33 +0200 Subject: Fix Ruby syntax error in Gitaly config example --- doc/administration/gitaly/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 5732b6a1ca4..40099dcc967 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -145,8 +145,8 @@ Omnibus installations: ```ruby # /etc/gitlab/gitlab.rb git_data_dirs({ - { 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' } }, - { 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' } }, + 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' }, + 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' }, }) gitlab_rails['gitaly_token'] = 'abc123secret' -- cgit v1.2.1 From aecc6600bdbe472d5b8e67d275d049ed384c9a31 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Thu, 24 Aug 2017 18:32:23 +0200 Subject: Fixed the URL + renamed to Job Failed --- app/helpers/builds_helper.rb | 2 +- app/serializers/build_details_entity.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index 85bc784d53c..aa3a9a055a0 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -30,7 +30,7 @@ module BuildsHelper def build_failed_issue_options { - title: "Build Failed ##{@build.id}", + title: "Job Failed ##{@build.id}", description: project_job_url(@project, @build) } end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 743a08acefe..1e90bef0778 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity private def build_failed_issue_options - { title: "Build Failed ##{build.id}", - description: project_job_path(project, build) } + { title: "Job Failed ##{build.id}", + description: project_job_url(project, build) } end def current_user -- cgit v1.2.1 From 7c817c40cab4666407b94882d3f408ac9997745b Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Mon, 28 Aug 2017 11:55:10 +0200 Subject: Adapting Test for Jobs --- spec/features/projects/jobs_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 3b5c6966287..f26af9f1f0d 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -164,8 +164,8 @@ feature 'Jobs' do end it 'links to issues/new with the title and description filled in' do - button_title = "Build Failed ##{job.id}" - job_path = project_job_path(project, job) + button_title = "Job Failed ##{job.id}" + job_path = project_job_url(project, job) options = { issue: { title: button_title, description: job_path } } href = new_project_issue_path(project, options) -- cgit v1.2.1 From cc5ccb74ec56934fd7956792af7cedef92791168 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Mon, 28 Aug 2017 22:58:05 +0200 Subject: Changed Var name --- spec/features/projects/jobs_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index f26af9f1f0d..5d46b850e72 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -165,8 +165,8 @@ feature 'Jobs' do it 'links to issues/new with the title and description filled in' do button_title = "Job Failed ##{job.id}" - job_path = project_job_url(project, job) - options = { issue: { title: button_title, description: job_path } } + job_url = project_job_url(project, job) + options = { issue: { title: button_title, description: job_url } } href = new_project_issue_path(project, options) -- cgit v1.2.1 From da0df236543beac96f845cacdfb9d77d69cc23cf Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Mon, 11 Sep 2017 15:46:53 +0200 Subject: Implemented the new Description Content --- app/serializers/build_details_entity.rb | 2 +- features/steps/project/redirects.rb | 1 - spec/features/projects/jobs_spec.rb | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 1e90bef0778..8c89eea607f 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -33,7 +33,7 @@ class BuildDetailsEntity < JobEntity def build_failed_issue_options { title: "Job Failed ##{build.id}", - description: project_job_url(project, build) } + description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" } end def current_user diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index 9ce86ca45d0..ab6b464a405 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -17,7 +17,6 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps end step 'I should see project "Community" home page' do - Gitlab.config.gitlab.should_receive(:host).and_return("www.example.com") page.within '.breadcrumbs .breadcrumb-item-text' do expect(page).to have_content 'Community' end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 5d46b850e72..76630017b54 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -166,7 +166,7 @@ feature 'Jobs' do it 'links to issues/new with the title and description filled in' do button_title = "Job Failed ##{job.id}" job_url = project_job_url(project, job) - options = { issue: { title: button_title, description: job_url } } + options = { issue: { title: button_title, description: "Job [##{job.id}](#{ job_url}) failed for #{job.sha}:\n" } } href = new_project_issue_path(project, options) -- cgit v1.2.1 From d63aebb7ec18617c097f52a4cfc8ffd3300489d1 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Tue, 12 Sep 2017 11:28:43 +0200 Subject: Fixed Tests + Reverted Try out from before --- features/steps/project/redirects.rb | 1 + spec/features/projects/jobs_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index ab6b464a405..9ce86ca45d0 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -17,6 +17,7 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps end step 'I should see project "Community" home page' do + Gitlab.config.gitlab.should_receive(:host).and_return("www.example.com") page.within '.breadcrumbs .breadcrumb-item-text' do expect(page).to have_content 'Community' end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 76630017b54..a4ed589f3de 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -165,8 +165,8 @@ feature 'Jobs' do it 'links to issues/new with the title and description filled in' do button_title = "Job Failed ##{job.id}" - job_url = project_job_url(project, job) - options = { issue: { title: button_title, description: "Job [##{job.id}](#{ job_url}) failed for #{job.sha}:\n" } } + job_url = project_job_path(project, job) + options = { issue: { title: button_title, description: "Job [##{job.id}](#{job_url}) failed for #{job.sha}:\n" } } href = new_project_issue_path(project, options) -- cgit v1.2.1 From afaa72a04773d87b65a2fe11215ec0e9c0367827 Mon Sep 17 00:00:00 2001 From: "Vitaliy @blackst0ne Klachkov" Date: Tue, 12 Sep 2017 13:56:56 +1100 Subject: Replace the 'project/issues/award_emoji.feature' spinach test with an rspec analog --- .../replace_project_issues_award_emoji-feature.yml | 5 + features/project/issues/award_emoji.feature | 45 --------- features/steps/project/issues/award_emoji.rb | 107 --------------------- .../user_interacts_with_awards_in_issue_spec.rb | 104 ++++++++++++++++++++ 4 files changed, 109 insertions(+), 152 deletions(-) create mode 100644 changelogs/unreleased/replace_project_issues_award_emoji-feature.yml delete mode 100644 features/project/issues/award_emoji.feature delete mode 100644 features/steps/project/issues/award_emoji.rb create mode 100644 spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb diff --git a/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml b/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml new file mode 100644 index 00000000000..a4a7435d4fa --- /dev/null +++ b/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'project/issues/award_emoji.feature' spinach test with an rspec analog +merge_request: 14202 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature deleted file mode 100644 index 1d7adfdd2c2..00000000000 --- a/features/project/issues/award_emoji.feature +++ /dev/null @@ -1,45 +0,0 @@ -@project_issues -Feature: Award Emoji - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has issue "Bugfix" - And I visit "Bugfix" issue page - - @javascript - Scenario: I repeatedly add and remove thumbsup award in the issue - Given I click the thumbsup award Emoji - Then I have award added - Given I click the thumbsup award Emoji - Then I have no awards added - Given I click the thumbsup award Emoji - Then I have award added - - @javascript - Scenario: I add and remove custom award in the issue - Given I click to emoji-picker - Then The emoji menu is visible - And The search field is focused - Then I click to emoji in the picker - Then I have award added - And I can remove it by clicking to icon - - @javascript - Scenario: I can see the list of emoji categories - Given I click to emoji-picker - Then The emoji menu is visible - And The search field is focused - Then I can see the activity and food categories - - @javascript - Scenario: I can search emoji - Given I click to emoji-picker - Then The emoji menu is visible - And The search field is focused - And I search "hand" - Then I see search result for "hand" - - @javascript - Scenario: I add award emoji using regular comment - Given I leave comment with a single emoji - Then I have new comment with emoji added diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb deleted file mode 100644 index bbd284b4633..00000000000 --- a/features/steps/project/issues/award_emoji.rb +++ /dev/null @@ -1,107 +0,0 @@ -class Spinach::Features::AwardEmoji < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include Select2Helper - - step 'I visit "Bugfix" issue page' do - visit project_issue_path(@project, @issue) - end - - step 'I click the thumbsup award Emoji' do - page.within '.awards' do - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - end - end - - step 'I click to emoji-picker' do - page.within '.awards' do - page.find('.js-add-award').click - end - end - - step 'I click to emoji in the picker' do - page.within '.emoji-menu-content' do - emoji_button = page.first('.js-emoji-btn') - emoji_button.hover - emoji_button.click - end - end - - step 'I can remove it by clicking to icon' do - page.within '.awards' do - expect do - page.find('.js-emoji-btn.active').click - wait_for_requests - end.to change { page.all(".award-control.js-emoji-btn").size }.from(3).to(2) - end - end - - step 'I can see the activity and food categories' do - page.within '.emoji-menu' do - expect(page).not_to have_selector 'Activity' - expect(page).not_to have_selector 'Food' - end - end - - step 'I have new comment with emoji added' do - expect(page).to have_selector 'gl-emoji[data-name="smile"]' - end - - step 'I have award added' do - page.within '.awards' do - expect(page).to have_selector '.js-emoji-btn' - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1' - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - end - end - - step 'I have no awards added' do - page.within '.awards' do - expect(page).to have_selector '.award-control.js-emoji-btn' - expect(page.all('.award-control.js-emoji-btn').size).to eq(2) - - # Check tooltip data - page.all('.award-control.js-emoji-btn').each do |element| - expect(element['title']).to eq("") - end - - page.all('.award-control .js-counter').each do |element| - expect(element).to have_content '0' - end - end - end - - step 'project "Shop" has issue "Bugfix"' do - @project = Project.find_by(name: 'Shop') - @issue = create(:issue, title: 'Bugfix', project: project) - end - - step 'I leave comment with a single emoji' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: ':smile:' - click_button 'Comment' - end - end - - step 'I search "hand"' do - fill_in 'emoji-menu-search', with: 'hand' - end - - step 'I see search result for "hand"' do - page.within '.emoji-menu-content' do - expect(page).to have_selector '[data-name="raised_hand"]' - end - end - - step 'The emoji menu is visible' do - page.find(".emoji-menu.is-visible") - end - - step 'The search field is focused' do - expect(page).to have_selector('.js-emoji-menu-search') - expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) - end -end diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb new file mode 100644 index 00000000000..adff0a10f0e --- /dev/null +++ b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe 'User interacts with awards in an issue', :js do + let(:issue) { create(:issue, project: project)} + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_issue_path(project, issue)) + end + + it 'toggles the thumbsup award emoji' do + page.within('.awards') do + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.award-control.js-emoji-btn') + expect(page.all('.award-control.js-emoji-btn').size).to eq(2) + + page.all('.award-control.js-emoji-btn').each do |element| + expect(element['title']).to eq('') + end + + page.all('.award-control .js-counter').each do |element| + expect(element).to have_content('0') + end + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + end + end + + it 'toggles a custom award emoji' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + page.within('.emoji-menu-content') do + emoji_button = page.first('.js-emoji-btn') + emoji_button.hover + emoji_button.click + end + + page.within('.awards') do + expect(page).to have_selector('.js-emoji-btn') + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + + expect do + page.find('.js-emoji-btn.active').click + wait_for_requests + end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2) + end + end + + it 'shows the list of award emoji categories' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + fill_in('emoji-menu-search', with: 'hand') + + page.within('.emoji-menu-content') do + expect(page).to have_selector('[data-name="raised_hand"]') + end + end + + it 'adds an award emoji by a comment' do + page.within('.js-main-target-form') do + fill_in('note[note]', with: ':smile:') + + click_button('Comment') + end + + expect(page).to have_selector('gl-emoji[data-name="smile"]') + end +end -- cgit v1.2.1 From 6f0a727ec8350f4fd22ec4988b147c1699e7ba98 Mon Sep 17 00:00:00 2001 From: "Vitaliy @blackst0ne Klachkov" Date: Mon, 11 Sep 2017 16:44:43 +1100 Subject: Replace the 'project/builds/summary.feature' spinach test with an rspec analog --- .../replace_project_builds_summary-feature.yml | 5 +++ features/project/builds/summary.feature | 30 --------------- features/steps/project/builds/summary.rb | 43 ---------------------- .../projects/jobs/user_browses_job_spec.rb | 37 +++++++++++++++++++ .../projects/jobs/user_browses_jobs_spec.rb | 32 ++++++++++++++++ 5 files changed, 74 insertions(+), 73 deletions(-) create mode 100644 changelogs/unreleased/replace_project_builds_summary-feature.yml delete mode 100644 features/project/builds/summary.feature delete mode 100644 features/steps/project/builds/summary.rb create mode 100644 spec/features/projects/jobs/user_browses_job_spec.rb create mode 100644 spec/features/projects/jobs/user_browses_jobs_spec.rb diff --git a/changelogs/unreleased/replace_project_builds_summary-feature.yml b/changelogs/unreleased/replace_project_builds_summary-feature.yml new file mode 100644 index 00000000000..48652b39b7e --- /dev/null +++ b/changelogs/unreleased/replace_project_builds_summary-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'project/builds/summary.feature' spinach test with an rspec analog +merge_request: 14177 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature deleted file mode 100644 index 3bf15b0cf87..00000000000 --- a/features/project/builds/summary.feature +++ /dev/null @@ -1,30 +0,0 @@ -Feature: Project Builds Summary - Background: - Given I sign in as a user - And I own a project - And project has CI enabled - And project has coverage enabled - And project has a recent build - - @javascript - Scenario: I browse build details page - When I visit recent build details page - Then I see details of a build - And I see build trace - - @javascript - Scenario: I browse project builds page - When I visit project builds page - Then I see coverage - Then I see button to CI Lint - - @javascript - Scenario: I erase a build - Given recent build is successful - And recent build has a build trace - When I visit recent build details page - And I click erase build button - Then recent build has been erased - And recent build summary does not have artifacts widget - And recent build summary contains information saying that build has been erased - And the build count cache is updated diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb deleted file mode 100644 index 20a5c873ecd..00000000000 --- a/features/steps/project/builds/summary.rb +++ /dev/null @@ -1,43 +0,0 @@ -class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedBuilds - include RepoHelpers - - step 'I see coverage' do - page.within('td.coverage') do - expect(page).to have_content "99.9%" - end - end - - step 'I see button to CI Lint' do - page.within('.nav-controls') do - ci_lint_tool_link = page.find_link('CI lint') - expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path) - end - end - - step 'I click erase build button' do - click_link 'Erase' - end - - step 'recent build has been erased' do - expect(@build).not_to have_trace - expect(@build.artifacts_file.exists?).to be_falsy - expect(@build.artifacts_metadata.exists?).to be_falsy - end - - step 'recent build summary does not have artifacts widget' do - expect(page).to have_no_css('.artifacts') - end - - step 'recent build summary contains information saying that build has been erased' do - page.within('.erased') do - expect(page).to have_content 'Job has been erased' - end - end - - step 'the build count cache is updated' do - expect(@build.project.running_or_pending_build_count).to eq @build.project.builds.running_or_pending.count(:all) - end -end diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb new file mode 100644 index 00000000000..21c9acc7ac0 --- /dev/null +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe 'User browses a job', :js do + let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } + let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + project.add_master(user) + project.enable_ci + build.success + build.trace.set('job trace') + + sign_in(user) + + visit(project_job_path(project, build)) + end + + it 'erases the job log' do + expect(page).to have_content("Job ##{build.id}") + expect(page).to have_css('#build-trace') + + click_link('Erase') + + expect(build).not_to have_trace + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy + expect(page).to have_no_css('.artifacts') + + page.within('.erased') do + expect(page).to have_content('Job has been erased') + end + + expect(build.project.running_or_pending_build_count).to eq(build.project.builds.running_or_pending.count(:all)) + end +end diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb new file mode 100644 index 00000000000..767777f3bf9 --- /dev/null +++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'User browses jobs' do + let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } + let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + project.add_master(user) + project.enable_ci + project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/) + + sign_in(user) + + visit(project_jobs_path(project)) + end + + it 'shows the coverage' do + page.within('td.coverage') do + expect(page).to have_content('99.9%') + end + end + + it 'shows the "CI Lint" button' do + page.within('.nav-controls') do + ci_lint_tool_link = page.find_link('CI lint') + + expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path) + end + end +end -- cgit v1.2.1 From 442ea61843db22d3a3c4ac75f75d9001df577593 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 12 Sep 2017 10:01:40 +0100 Subject: Fixed the breadcrumbs project name not having hover style Closes #37661 --- app/assets/stylesheets/new_nav.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 2b6c0fc015c..cd8fdd3f2ea 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -488,6 +488,7 @@ header.navbar-gitlab-new { .breadcrumb-item-text { @include str-truncated(128px); + text-decoration: inherit; } .breadcrumbs-list-angle { -- cgit v1.2.1 From 4992f06f85ff7b2965a2844c2516d3eeed59b5dc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 12 Sep 2017 10:09:18 +0100 Subject: Fix JS error in fly-out nav --- app/assets/javascripts/fly_out_nav.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 4b19f7b4188..ad8254167a2 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -148,7 +148,7 @@ export const documentMouseMove = (e) => { export const subItemsMouseLeave = (relatedTarget) => { clearTimeout(timeoutId); - if (!relatedTarget.closest(`.${IS_OVER_CLASS}`)) { + if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) { hideMenu(currentOpenMenu); } }; -- cgit v1.2.1 From 74bf291c78538641bb871adf8d998bdf0f8e2508 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 12 Sep 2017 12:33:48 +0200 Subject: Add auto devops enabled/disabled to usage ping --- app/models/project_auto_devops.rb | 3 +++ lib/gitlab/usage_data.rb | 2 ++ spec/lib/gitlab/usage_data_spec.rb | 2 ++ 3 files changed, 7 insertions(+) diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 53731579e87..7af3b6870e2 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -1,6 +1,9 @@ class ProjectAutoDevops < ActiveRecord::Base belongs_to :project + scope :enabled, -> { where(enabled: true) } + scope :disabled, -> { where(enabled: false) } + validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } def variables diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 47b0be5835f..36708078136 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -27,6 +27,8 @@ module Gitlab ci_runners: ::Ci::Runner.count, ci_triggers: ::Ci::Trigger.count, ci_pipeline_schedules: ::Ci::PipelineSchedule.count, + auto_devops_enabled: ::ProjectAutoDevops.enabled.count, + auto_devops_disabled: ::ProjectAutoDevops.disabled.count, deploy_keys: DeployKey.count, deployments: Deployment.count, environments: ::Environment.count, diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index daca5c73415..c7d9f105f04 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -45,6 +45,8 @@ describe Gitlab::UsageData do ci_runners ci_triggers ci_pipeline_schedules + auto_devops_enabled + auto_devops_disabled deploy_keys deployments environments -- cgit v1.2.1 From b1d5186d0a2d8ea2b594398fbcfe28acb3e8ed23 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Tue, 12 Sep 2017 14:00:50 +0200 Subject: Allow all AutoDevOps banners to be disabled Given the default in the development and production environment is false, the negation of enabling is used in the flag to signal you'd turn it off. It reads a bit awkward, but makes us have a migration less. Fixes gitlab-org/gitlab-ce#37653 --- app/helpers/auto_devops_helper.rb | 3 ++- .../unreleased/zj-feature-flipper-disable-banner.yml | 5 +++++ doc/topics/autodevops/index.md | 17 +++++++++++++++++ spec/helpers/auto_devops_helper_spec.rb | 10 ++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/zj-feature-flipper-disable-banner.yml diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index 4ff38f86b5f..c132daed323 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -1,6 +1,7 @@ module AutoDevopsHelper def show_auto_devops_callout?(project) - show_callout?('auto_devops_settings_dismissed') && + Feature.get(:auto_devops_banner_disabled).off? && + show_callout?('auto_devops_settings_dismissed') && can?(current_user, :admin_pipeline, project) && project.has_auto_devops_implicitly_disabled? end diff --git a/changelogs/unreleased/zj-feature-flipper-disable-banner.yml b/changelogs/unreleased/zj-feature-flipper-disable-banner.yml new file mode 100644 index 00000000000..fd5dd1bbe37 --- /dev/null +++ b/changelogs/unreleased/zj-feature-flipper-disable-banner.yml @@ -0,0 +1,5 @@ +--- +title: Allow all AutoDevOps banners to be turned off +merge_request: +author: +type: changed diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index babf44d2665..b31b8eaaca0 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -323,6 +323,23 @@ container registry. **Restarting a pod, scaling a service, or other actions whic require on-going access to the registry will fail**. On-going secure access is planned for a subsequent release. +## Disable the banner instance wide + +If an administrater would like to disable the banners on an instance level, this +feature can be disabled either through the console: + +```basb +$ gitlab-rails console +[1] pry(main)> Feature.get(:auto_devops_banner_disabled).disable +=> true +``` + +Or through the HTTP API with the admin access token: + +``` +curl --data "value=true" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled +``` + ## Troubleshooting - Auto Build and Auto Test may fail in detecting your language/framework. There diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb index b6d892548ef..80d58ff6bf7 100644 --- a/spec/helpers/auto_devops_helper_spec.rb +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -10,6 +10,8 @@ describe AutoDevopsHelper do before do allow(helper).to receive(:can?).with(user, :admin_pipeline, project) { allowed } allow(helper).to receive(:current_user) { user } + + Feature.get(:auto_devops_banner_disabled).disable end subject { helper.show_auto_devops_callout?(project) } @@ -18,6 +20,14 @@ describe AutoDevopsHelper do it { is_expected.to eq(true) } end + context 'when the banner is disabled by feature flag' do + it 'allows the feature flag to disable' do + Feature.get(:auto_devops_banner_disabled).enable + + expect(subject).to be(false) + end + end + context 'when dismissed' do before do helper.request.cookies[:auto_devops_settings_dismissed] = 'true' -- cgit v1.2.1 From a31e0aff224d4047e10fae98b6f9dc9f84b7457a Mon Sep 17 00:00:00 2001 From: Micael Bergeron Date: Tue, 12 Sep 2017 14:07:31 +0000 Subject: Resolve "Error 500 in non-UTF8 branch names" --- .../37025-error-500-in-non-utf8-branch-names.yml | 4 ++++ lib/gitlab/git.rb | 2 +- spec/controllers/projects/branches_controller_spec.rb | 15 +++++++++++++++ spec/lib/gitlab/data_builder/push_spec.rb | 2 +- spec/lib/gitlab/git_spec.rb | 9 +++++++++ 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml diff --git a/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml b/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml new file mode 100644 index 00000000000..f3118cf0f2f --- /dev/null +++ b/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml @@ -0,0 +1,4 @@ +--- +title: Fixed non-UTF-8 valid branch names from causing an error. +merge_request: 14090 +type: fixed diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 8c9acbc9fbe..b4b6326cfdd 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -11,7 +11,7 @@ module Gitlab include Gitlab::EncodingHelper def ref_name(ref) - encode! ref.sub(/\Arefs\/(tags|heads|remotes)\//, '') + encode_utf8(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '') end def branch_name(ref) diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 745d051a5c1..5e0b57e9b2e 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -367,5 +367,20 @@ describe Projects::BranchesController do expect(parsed_response.first).to eq 'master' end end + + context 'when branch contains an invalid UTF-8 sequence' do + before do + project.repository.create_branch("wrong-\xE5-utf8-sequence") + end + + it 'return with a status 200' do + get :index, + namespace_id: project.namespace, + project_id: project, + format: :html + + expect(response).to have_http_status(200) + end + end end end diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index cb430b47463..befdc18d1aa 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -47,7 +47,7 @@ describe Gitlab::DataBuilder::Push do include_examples 'deprecated repository hook data' it 'does not raise an error when given nil commits' do - expect { described_class.build(spy, spy, spy, spy, spy, nil) } + expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) } .not_to raise_error end end diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb index 4702a978f19..494dfe0e595 100644 --- a/spec/lib/gitlab/git_spec.rb +++ b/spec/lib/gitlab/git_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe Gitlab::Git do @@ -29,4 +30,12 @@ describe Gitlab::Git do end end end + + describe '.ref_name' do + it 'ensure ref is a valid UTF-8 string' do + utf8_invalid_ref = Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5" + + expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_å") + end + end end -- cgit v1.2.1 From c2f28560a907b9f5df5245da329f9534322db698 Mon Sep 17 00:00:00 2001 From: Victor Wu Date: Tue, 12 Sep 2017 14:11:00 +0000 Subject: Automatically inherit the milestone and labels of the issue --- doc/user/project/issues/issues_functionalities.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md index 074b2c19c43..66140f389af 100644 --- a/doc/user/project/issues/issues_functionalities.md +++ b/doc/user/project/issues/issues_functionalities.md @@ -167,6 +167,7 @@ Once you wrote your comment, you can either: #### 18. New Merge Request - Create a new merge request (with a new source branch named after the issue) in one action. -The merge request will automatically close that issue as soon as merged. +The merge request will automatically inherit the milestone and labels of the issue. The merge +request will automatically close that issue as soon as merged. - Optionally, you can just create a [new branch](../repository/web_editor.md#create-a-new-branch-from-an-issue) named after that issue. -- cgit v1.2.1 From ac51f44f752476caf205dbeff03e094c829ea8e5 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 12 Sep 2017 11:41:55 -0400 Subject: Add developer documentation about working with sent emails and previews --- doc/development/README.md | 1 + doc/development/emails.md | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 doc/development/emails.md diff --git a/doc/development/README.md b/doc/development/README.md index eeff14819da..3096d9f25f0 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -43,6 +43,7 @@ - [Object state models](object_state_models.md) - [Building a package for testing purposes](build_test_package.md) - [Manage feature flags](feature_flags.md) +- [View sent emails or preview mailers](emails.md) ## Databases diff --git a/doc/development/emails.md b/doc/development/emails.md new file mode 100644 index 00000000000..18f47f44cb5 --- /dev/null +++ b/doc/development/emails.md @@ -0,0 +1,23 @@ +# Dealing with email in development + +## Sent emails + +To view rendered emails "sent" in your development instance, visit +[`/rails/letter_opener`](http://localhost:3000/rails/letter_opener). + +## Mailer previews + +Rails provides a way to preview our mailer templates in HTML and plaintext using +dummy data. + +The previews live in [`spec/mailers/previews`][previews] and can be viewed at +[`/rails/mailers`](http://localhost:3000/rails/mailers). + +See the [Rails guides] for more info. + +[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/mailers/previews +[Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails + +--- + +[Return to Development documentation](README.md) -- cgit v1.2.1 From 6a97759ea86cd6d15eea1b17fd2526b4f0569108 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 12 Sep 2017 11:14:54 -0400 Subject: Remove ImageLazyLoadFilter from EmailPipeline --- ...g-breaks-notification-mails-for-an-added-screenshot.yml | 5 +++++ lib/banzai/filter/image_lazy_load_filter.rb | 3 ++- lib/banzai/pipeline/email_pipeline.rb | 6 ++++++ spec/lib/banzai/pipeline/email_pipeline_spec.rb | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml create mode 100644 spec/lib/banzai/pipeline/email_pipeline_spec.rb diff --git a/changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml b/changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml new file mode 100644 index 00000000000..5735d59a2bd --- /dev/null +++ b/changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml @@ -0,0 +1,5 @@ +--- +title: Image attachments are properly displayed in notification emails again +merge_request: 14161 +author: +type: fixed diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb index bcb4f332267..4cd9b02b76c 100644 --- a/lib/banzai/filter/image_lazy_load_filter.rb +++ b/lib/banzai/filter/image_lazy_load_filter.rb @@ -1,6 +1,7 @@ module Banzai module Filter - # HTML filter that moves the value of the src attribute to the data-src attribute so it can be lazy loaded + # HTML filter that moves the value of image `src` attributes to `data-src` + # so they can be lazy loaded. class ImageLazyLoadFilter < HTML::Pipeline::Filter def call doc.xpath('descendant-or-self::img').each do |img| diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb index e47c384afc1..8f5f144d582 100644 --- a/lib/banzai/pipeline/email_pipeline.rb +++ b/lib/banzai/pipeline/email_pipeline.rb @@ -1,6 +1,12 @@ module Banzai module Pipeline class EmailPipeline < FullPipeline + def self.filters + super.tap do |filter_array| + filter_array.delete(Banzai::Filter::ImageLazyLoadFilter) + end + end + def self.transform_context(context) super(context).merge( only_path: false diff --git a/spec/lib/banzai/pipeline/email_pipeline_spec.rb b/spec/lib/banzai/pipeline/email_pipeline_spec.rb new file mode 100644 index 00000000000..6a11ca2f9d5 --- /dev/null +++ b/spec/lib/banzai/pipeline/email_pipeline_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +describe Banzai::Pipeline::EmailPipeline do + describe '.filters' do + it 'returns the expected type' do + expect(described_class.filters).to be_kind_of(Banzai::FilterArray) + end + + it 'excludes ImageLazyLoadFilter' do + expect(described_class.filters).not_to be_empty + expect(described_class.filters).not_to include(Banzai::Filter::ImageLazyLoadFilter) + end + end +end -- cgit v1.2.1 From 4db6b8f0bb35fabf48c2d795ef3151810bb8251c Mon Sep 17 00:00:00 2001 From: Maxim Rydkin Date: Tue, 12 Sep 2017 17:02:11 +0000 Subject: Decrease Cyclomatic Complexity threshold to 13 --- .rubocop.yml | 2 +- ...rease_cyclomatic_complexity_threshold_step4.yml | 5 ++++ lib/gitlab/conflict/parser.rb | 28 +++++++++++++++------- lib/gitlab/mail_room.rb | 27 +++++++++++++-------- 4 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml diff --git a/.rubocop.yml b/.rubocop.yml index 4640681379a..dc97a61b4a6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -643,7 +643,7 @@ Metrics/ClassLength: # of test cases needed to validate a method. Metrics/CyclomaticComplexity: Enabled: true - Max: 14 + Max: 13 # Limit lines to 80 characters. Metrics/LineLength: diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml new file mode 100644 index 00000000000..a404456198a --- /dev/null +++ b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml @@ -0,0 +1,5 @@ +--- +title: Decrease Cyclomatic Complexity threshold to 13 +merge_request: 14152 +author: Maxim Rydkin +type: other diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb index 84f9ecd3d23..e3678c914db 100644 --- a/lib/gitlab/conflict/parser.rb +++ b/lib/gitlab/conflict/parser.rb @@ -12,12 +12,7 @@ module Gitlab MissingEndDelimiter = Class.new(ParserError) def parse(text, our_path:, their_path:, parent_file: nil) - raise UnmergeableFile if text.blank? # Typically a binary file - raise UnmergeableFile if text.length > 200.kilobytes - - text.force_encoding('UTF-8') - - raise UnsupportedEncoding unless text.valid_encoding? + validate_text!(text) line_obj_index = 0 line_old = 1 @@ -32,15 +27,15 @@ module Gitlab full_line = line.delete("\n") if full_line == conflict_start - raise UnexpectedDelimiter unless type.nil? + validate_delimiter!(type.nil?) type = 'new' elsif full_line == conflict_middle - raise UnexpectedDelimiter unless type == 'new' + validate_delimiter!(type == 'new') type = 'old' elsif full_line == conflict_end - raise UnexpectedDelimiter unless type == 'old' + validate_delimiter!(type == 'old') type = nil elsif line[0] == '\\' @@ -59,6 +54,21 @@ module Gitlab lines end + + private + + def validate_text!(text) + raise UnmergeableFile if text.blank? # Typically a binary file + raise UnmergeableFile if text.length > 200.kilobytes + + text.force_encoding('UTF-8') + + raise UnsupportedEncoding unless text.valid_encoding? + end + + def validate_delimiter!(condition) + raise UnexpectedDelimiter unless condition + end end end end diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb index 9f432673a6e..344784c866f 100644 --- a/lib/gitlab/mail_room.rb +++ b/lib/gitlab/mail_room.rb @@ -4,6 +4,15 @@ require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues) module Gitlab module MailRoom + DEFAULT_CONFIG = { + enabled: false, + port: 143, + ssl: false, + start_tls: false, + mailbox: 'inbox', + idle_timeout: 60 + }.freeze + class << self def enabled? config[:enabled] && config[:address] @@ -22,16 +31,10 @@ module Gitlab def fetch_config return {} unless File.exist?(config_file) - rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' - all_config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys - - config = all_config[:incoming_email] || {} - config[:enabled] = false if config[:enabled].nil? - config[:port] = 143 if config[:port].nil? - config[:ssl] = false if config[:ssl].nil? - config[:start_tls] = false if config[:start_tls].nil? - config[:mailbox] = 'inbox' if config[:mailbox].nil? - config[:idle_timeout] = 60 if config[:idle_timeout].nil? + config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys[:incoming_email] || {} + config = DEFAULT_CONFIG.merge(config) do |_key, oldval, newval| + newval.nil? ? oldval : newval + end if config[:enabled] && config[:address] gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env) @@ -45,6 +48,10 @@ module Gitlab config end + def rails_env + @rails_env ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' + end + def config_file ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__) end -- cgit v1.2.1 From e723dc13be1e2e90fcf3ac9b97e8d159572f1d01 Mon Sep 17 00:00:00 2001 From: Maxim Rydkin Date: Tue, 12 Sep 2017 17:27:29 +0000 Subject: Decrease Perceived Complexity threshold to 15 --- .rubocop.yml | 2 +- app/models/event.rb | 32 ++++++++++++++-------- ...crease_perceived_complexity_threshold_step2.yml | 5 ++++ 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml diff --git a/.rubocop.yml b/.rubocop.yml index dc97a61b4a6..dbeb1880d39 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -665,7 +665,7 @@ Metrics/ParameterLists: # A complexity metric geared towards measuring complexity for a human reader. Metrics/PerceivedComplexity: Enabled: true - Max: 17 + Max: 15 # Lint ######################################################################## diff --git a/app/models/event.rb b/app/models/event.rb index 8e9490b66f4..0b1f053a7e6 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -241,13 +241,7 @@ class Event < ActiveRecord::Base def action_name if push? - if new_ref? - "pushed new" - elsif rm_ref? - "deleted" - else - "pushed to" - end + push_action_name elsif closed? "closed" elsif merged? @@ -263,11 +257,7 @@ class Event < ActiveRecord::Base elsif commented? "commented on" elsif created_project? - if project.external_import? - "imported" - else - "created" - end + created_project_action_name else "opened" end @@ -360,6 +350,24 @@ class Event < ActiveRecord::Base private + def push_action_name + if new_ref? + "pushed new" + elsif rm_ref? + "deleted" + else + "pushed to" + end + end + + def created_project_action_name + if project.external_import? + "imported" + else + "created" + end + end + def recent_update? project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago end diff --git a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml new file mode 100644 index 00000000000..6036e1a43a0 --- /dev/null +++ b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml @@ -0,0 +1,5 @@ +--- +title: Decrease Perceived Complexity threshold to 15 +merge_request: 14160 +author: Maxim Rydkin +type: other -- cgit v1.2.1 From 46e4e8f4dc968b7173aa37bb3e3bb944e5d63e10 Mon Sep 17 00:00:00 2001 From: "micael.bergeron" Date: Tue, 12 Sep 2017 13:42:40 -0400 Subject: changed InlineDiffMarker to make it html_safe its output updated the spec --- app/views/projects/diffs/_file_header.html.haml | 2 +- lib/gitlab/diff/inline_diff_marker.rb | 3 ++- spec/helpers/diff_helper_spec.rb | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index d5b9f328098..73c316472e3 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -17,7 +17,7 @@ = blob_icon diff_file.b_mode, diff_file.file_path - if diff_file.renamed_file? - - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path).map(&:html_safe) + - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } } = old_path → diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 919965100ae..010b4be7b40 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -2,9 +2,10 @@ module Gitlab module Diff class InlineDiffMarker < Gitlab::StringRangeMarker def mark(line_inline_diffs, mode: nil) - super(line_inline_diffs) do |text, left:, right:| + mark = super(line_inline_diffs) do |text, left:, right:| %{#{text}} end + mark.html_safe end private diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 0deea0ff6a3..f9c31ac61d8 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -136,9 +136,9 @@ describe DiffHelper do marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) expect(marked_old_line).to eq(%q{abc 'def'}) - expect(marked_old_line).not_to be_html_safe + expect(marked_old_line).to be_html_safe expect(marked_new_line).to eq(%q{abc "def"}) - expect(marked_new_line).not_to be_html_safe + expect(marked_new_line).to be_html_safe end end -- cgit v1.2.1 From 449455941ed1d0c05e5c067d148fd0c864c4e3a8 Mon Sep 17 00:00:00 2001 From: "micael.bergeron" Date: Mon, 11 Sep 2017 16:23:55 -0400 Subject: add association preloading for issue boards --- app/controllers/boards/issues_controller.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 8d4ec2d6d9d..1c33f9a69d8 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -11,9 +11,15 @@ module Boards issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute issues = issues.page(params[:page]).per(params[:per] || 20) make_sure_position_is_set(issues) + issues = issues.preload(:project, + :labels, + :milestone, + :assignees, + :notes => [:award_emoji, :author] + ) render json: { - issues: serialize_as_json(issues.preload(:project)), + issues: serialize_as_json(issues), size: issues.total_count } end @@ -76,14 +82,13 @@ module Boards def serialize_as_json(resource) resource.as_json( - labels: true, only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], + labels: true, include: { project: { only: [:id, :path] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } - }, - user: current_user + } ) end end -- cgit v1.2.1 From 53e632cbca5c9a304079721c26c140628c09e81c Mon Sep 17 00:00:00 2001 From: "micael.bergeron" Date: Mon, 11 Sep 2017 16:38:35 -0400 Subject: add changelog entry --- changelogs/unreleased/34510-board-issues-sql-speedup.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/34510-board-issues-sql-speedup.yml diff --git a/changelogs/unreleased/34510-board-issues-sql-speedup.yml b/changelogs/unreleased/34510-board-issues-sql-speedup.yml new file mode 100644 index 00000000000..244ff7e9dfa --- /dev/null +++ b/changelogs/unreleased/34510-board-issues-sql-speedup.yml @@ -0,0 +1,5 @@ +--- +title: Optimize the boards' issues fetching. +merge_request: 14198 +author: +type: other -- cgit v1.2.1 From 589217e69f8fdaf1d1685c3bbc9ab7ce4bc43da9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 12 Sep 2017 18:22:56 +0000 Subject: Fix Auto DevOps quick start guide --- doc/topics/autodevops/quick_start_guide.md | 80 +++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index f23c9d794b4..564dd3222ac 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -18,45 +18,66 @@ example for this guide. It contains two files: ## Fork sample project on GitLab.com Let’s start by forking our sample application. Go to [the project -page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork` -button. Soon you should have a project under your namespace with the necessary -files. +page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the +**Fork** button. Soon you should have a project under your namespace with the +necessary files. ## Setup your own cluster on Google Container Engine -If you do not already have a Google Cloud account, create one at https://console.cloud.google.com. +If you do not already have a Google Cloud account, create one at +https://console.cloud.google.com. -Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface. +Visit the [**Container Engine**](https://console.cloud.google.com/kubernetes/list) +tab and create a new cluster. You can change the name and leave the rest of the +default settings. Once you have your cluster running, you need to connect to the +cluster by following the Google interface. ## Connect to Kubernetes cluster You need to have the Google Cloud SDK installed. e.g. -On OSX, install [homebrew](https://brew.sh): +On macOS, install [homebrew](https://brew.sh): 1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask` 2. Install Google Cloud SDK: `brew cask install google-cloud-sdk` -3. Add `kubectl`: `gcloud components install kubectl` +3. Add `kubectl` with: `gcloud components install kubectl` 4. Log in: `gcloud auth login` -Now go back to the Google interface, find your cluster, and follow the instructions under `Connect to the cluster` and open the Kubernetes Dashboard. It will look something like `gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX` and then `kubectl proxy`. +Now go back to the Google interface, find your cluster, follow the instructions +under "Connect to the cluster" and open the Kubernetes Dashboard. It will look +something like: + +```sh +gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX +``` + +Finally, run `kubectl proxy`. ![connect to cluster](img/guide_connect_cluster.png) ## Copy credentials to GitLab.com project -Once you have the Kubernetes Dashboard interface running, you should visit `Secrets` under the `Config` section. There you should find the settings we need for GitLab integration: ca.crt and token. +Once you have the Kubernetes Dashboard interface running, you should visit +**Secrets** under the "Config" section. There, you should find the settings we +need for GitLab integration: `ca.crt` and token. ![connect to cluster](img/guide_secret.png) -You need to copy-paste the ca.crt and token into your project on GitLab.com in the Kubernetes integration page under project **Settings > Integrations > Project services > Kubernetes**. Don't actually copy the namespace though. Each project should have a unique namespace, and by leaving it blank, GitLab will create one for you. +You need to copy-paste the `ca.crt` and token into your project on GitLab.com in +the Kubernetes integration page under project +**Settings > Integrations > Project services > Kubernetes**. Don't actually copy +the namespace though. Each project should have a unique namespace, and by leaving +it blank, GitLab will create one for you. ![connect to cluster](img/guide_integration.png) -For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform. +For the API URL, you should use the "Endpoint" IP from your cluster page on +Google Cloud Platform. ## Expose application to the world -In order to be able to visit your application, you need to install an NGINX ingress controller and point your domain name to its external IP address. +In order to be able to visit your application, you need to install an NGINX +ingress controller and point your domain name to its external IP address. Let's +see how that's done. ### Set up Ingress controller @@ -68,28 +89,49 @@ helm init helm install --name ruby-app stable/nginx-ingress ``` -This should create several services including `ruby-app-nginx-ingress-controller`. You can list your services by running `kubectl get svc` to confirm that. +This should create several services including `ruby-app-nginx-ingress-controller`. +You can list your services by running `kubectl get svc` to confirm that. ### Point DNS at Cluster IP -Find out the external IP address of the `ruby-app-nginx-ingress-controller` by running: +Find out the external IP address of the `ruby-app-nginx-ingress-controller` by +running: ```sh kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}' ``` -Use this IP address to configure your DNS. This part heavily depends on your preferences and domain provider. But in case you are not sure, just create an A record with a wildcard host like `*.`. +Use this IP address to configure your DNS. This part heavily depends on your +preferences and domain provider. But in case you are not sure, just create an +A record with a wildcard host like `*.`. -Use `nslookup minimal-ruby-app-staging.` to confirm that domain is assigned to the cluster IP. +Use `nslookup minimal-ruby-app-staging.` to confirm that domain is +assigned to the cluster IP. ## Set up Auto DevOps -In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps section. Select "Enable Auto DevOps", add in your base domain, and save. +In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps +section. Select "Enable Auto DevOps", add in your base domain, and save. ![auto devops settings](img/auto_devops_settings.png) -Then trigger your first pipeline run. This will create a new pipeline with several jobs: `build`, `test`, `codequality`, and `production`. The `build` job will create a docker image with your new change and push it to the GitLab Container Registry. The `test` job will test your change. The `codequality` job will run static analysis on your change. The `production` job will deploy your change to a production application. Once the deploy job succeeds you should be able to see your application by visiting the Kubernetes dashboard. Select the namespace of your project, which will look like `minimal-ruby-app-23`, but with a unique ID for your project, and your app will be listed as "production" under the Deployment tab. +Next, a pipeline needs to be triggered. Since the test project doesn't have a +`.gitlab-ci.yml`, you need to either push a change to the repository or +manually visit `https://gitlab.com//minimal-ruby-app/pipelines/run`, +where `` is your username. + +This will create a new pipeline with several jobs: `build`, `test`, `codequality`, +and `production`. The `build` job will create a Docker image with your new +change and push it to the Container Registry. The `test` job will test your +changes, whereas the `codequality` job will run static analysis on your changes. +Finally, the `production` job will deploy your changes to a production application. + +Once the deploy job succeeds you should be able to see your application by +visiting the Kubernetes dashboard. Select the namespace of your project, which +will look like `minimal-ruby-app-23`, but with a unique ID for your project, +and your app will be listed as "production" under the Deployment tab. -Once its ready - just visit http://minimal-ruby-app.example.com to see “Hello, world!” +Once its ready, just visit `http://minimal-ruby-app.example.com` to see the +famous "Hello, world!"! [ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 -- cgit v1.2.1 From 060fc3905de795ff206260b97afd18177f876316 Mon Sep 17 00:00:00 2001 From: Maxim Rydkin Date: Wed, 6 Sep 2017 14:08:06 +0300 Subject: move `lib/ci/ansi2html.rb` into `lib/gitlab/ci/ansi2html.rb` --- lib/ci/ansi2html.rb | 331 ---------------------------------- lib/gitlab/ci/ansi2html.rb | 333 +++++++++++++++++++++++++++++++++++ lib/gitlab/ci/trace/stream.rb | 4 +- spec/lib/ci/ansi2html_spec.rb | 220 ----------------------- spec/lib/gitlab/ci/ansi2html_spec.rb | 220 +++++++++++++++++++++++ 5 files changed, 555 insertions(+), 553 deletions(-) delete mode 100644 lib/ci/ansi2html.rb create mode 100644 lib/gitlab/ci/ansi2html.rb delete mode 100644 spec/lib/ci/ansi2html_spec.rb create mode 100644 spec/lib/gitlab/ci/ansi2html_spec.rb diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb deleted file mode 100644 index b9e9f9f7f4a..00000000000 --- a/lib/ci/ansi2html.rb +++ /dev/null @@ -1,331 +0,0 @@ -# ANSI color library -# -# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code -module Ci - module Ansi2html - # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107) - COLOR = { - 0 => 'black', # not that this is gray in the intense color table - 1 => 'red', - 2 => 'green', - 3 => 'yellow', - 4 => 'blue', - 5 => 'magenta', - 6 => 'cyan', - 7 => 'white', # not that this is gray in the dark (aka default) color table - }.freeze - - STYLE_SWITCHES = { - bold: 0x01, - italic: 0x02, - underline: 0x04, - conceal: 0x08, - cross: 0x10 - }.freeze - - def self.convert(ansi, state = nil) - Converter.new.convert(ansi, state) - end - - class Converter - def on_0(s) reset() end - - def on_1(s) enable(STYLE_SWITCHES[:bold]) end - - def on_3(s) enable(STYLE_SWITCHES[:italic]) end - - def on_4(s) enable(STYLE_SWITCHES[:underline]) end - - def on_8(s) enable(STYLE_SWITCHES[:conceal]) end - - def on_9(s) enable(STYLE_SWITCHES[:cross]) end - - def on_21(s) disable(STYLE_SWITCHES[:bold]) end - - def on_22(s) disable(STYLE_SWITCHES[:bold]) end - - def on_23(s) disable(STYLE_SWITCHES[:italic]) end - - def on_24(s) disable(STYLE_SWITCHES[:underline]) end - - def on_28(s) disable(STYLE_SWITCHES[:conceal]) end - - def on_29(s) disable(STYLE_SWITCHES[:cross]) end - - def on_30(s) set_fg_color(0) end - - def on_31(s) set_fg_color(1) end - - def on_32(s) set_fg_color(2) end - - def on_33(s) set_fg_color(3) end - - def on_34(s) set_fg_color(4) end - - def on_35(s) set_fg_color(5) end - - def on_36(s) set_fg_color(6) end - - def on_37(s) set_fg_color(7) end - - def on_38(s) set_fg_color_256(s) end - - def on_39(s) set_fg_color(9) end - - def on_40(s) set_bg_color(0) end - - def on_41(s) set_bg_color(1) end - - def on_42(s) set_bg_color(2) end - - def on_43(s) set_bg_color(3) end - - def on_44(s) set_bg_color(4) end - - def on_45(s) set_bg_color(5) end - - def on_46(s) set_bg_color(6) end - - def on_47(s) set_bg_color(7) end - - def on_48(s) set_bg_color_256(s) end - - def on_49(s) set_bg_color(9) end - - def on_90(s) set_fg_color(0, 'l') end - - def on_91(s) set_fg_color(1, 'l') end - - def on_92(s) set_fg_color(2, 'l') end - - def on_93(s) set_fg_color(3, 'l') end - - def on_94(s) set_fg_color(4, 'l') end - - def on_95(s) set_fg_color(5, 'l') end - - def on_96(s) set_fg_color(6, 'l') end - - def on_97(s) set_fg_color(7, 'l') end - - def on_99(s) set_fg_color(9, 'l') end - - def on_100(s) set_bg_color(0, 'l') end - - def on_101(s) set_bg_color(1, 'l') end - - def on_102(s) set_bg_color(2, 'l') end - - def on_103(s) set_bg_color(3, 'l') end - - def on_104(s) set_bg_color(4, 'l') end - - def on_105(s) set_bg_color(5, 'l') end - - def on_106(s) set_bg_color(6, 'l') end - - def on_107(s) set_bg_color(7, 'l') end - - def on_109(s) set_bg_color(9, 'l') end - - attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask - - STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze - - def convert(stream, new_state) - reset_state - restore_state(new_state, stream) if new_state.present? - - append = false - truncated = false - - cur_offset = stream.tell - if cur_offset > @offset - @offset = cur_offset - truncated = true - else - stream.seek(@offset) - append = @offset > 0 - end - start_offset = @offset - - open_new_tag - - stream.each_line do |line| - s = StringScanner.new(line) - until s.eos? - if s.scan(/\e([@-_])(.*?)([@-~])/) - handle_sequence(s) - elsif s.scan(/\e(([@-_])(.*?)?)?$/) - break - elsif s.scan(/' - else - @out << s.scan(/./m) - end - @offset += s.matched_size - end - end - - close_open_tags() - - OpenStruct.new( - html: @out.force_encoding(Encoding.default_external), - state: state, - append: append, - truncated: truncated, - offset: start_offset, - size: stream.tell - start_offset, - total: stream.size - ) - end - - def handle_sequence(s) - indicator = s[1] - commands = s[2].split ';' - terminator = s[3] - - # We are only interested in color and text style changes - triggered by - # sequences starting with '\e[' and ending with 'm'. Any other control - # sequence gets stripped (including stuff like "delete last line") - return unless indicator == '[' && terminator == 'm' - - close_open_tags() - - if commands.empty?() - reset() - return - end - - evaluate_command_stack(commands) - - open_new_tag - end - - def evaluate_command_stack(stack) - return unless command = stack.shift() - - if self.respond_to?("on_#{command}", true) - self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend - end - - evaluate_command_stack(stack) - end - - def open_new_tag - css_classes = [] - - unless @fg_color.nil? - fg_color = @fg_color - # Most terminals show bold colored text in the light color variant - # Let's mimic that here - if @style_mask & STYLE_SWITCHES[:bold] != 0 - fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1') - end - css_classes << fg_color - end - css_classes << @bg_color unless @bg_color.nil? - - STYLE_SWITCHES.each do |css_class, flag| - css_classes << "term-#{css_class}" if @style_mask & flag != 0 - end - - return if css_classes.empty? - - @out << %{} - @n_open_tags += 1 - end - - def close_open_tags - while @n_open_tags > 0 - @out << %{} - @n_open_tags -= 1 - end - end - - def reset_state - @offset = 0 - @n_open_tags = 0 - @out = '' - reset - end - - def state - state = STATE_PARAMS.inject({}) do |h, param| - h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend - h - end - Base64.urlsafe_encode64(state.to_json) - end - - def restore_state(new_state, stream) - state = Base64.urlsafe_decode64(new_state) - state = JSON.parse(state, symbolize_names: true) - return if state[:offset].to_i > stream.size - - STATE_PARAMS.each do |param| - send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend - end - end - - def reset - @fg_color = nil - @bg_color = nil - @style_mask = 0 - end - - def enable(flag) - @style_mask |= flag - end - - def disable(flag) - @style_mask &= ~flag - end - - def set_fg_color(color_index, prefix = nil) - @fg_color = get_term_color_class(color_index, ["fg", prefix]) - end - - def set_bg_color(color_index, prefix = nil) - @bg_color = get_term_color_class(color_index, ["bg", prefix]) - end - - def get_term_color_class(color_index, prefix) - color_name = COLOR[color_index] - return nil if color_name.nil? - - get_color_class(["term", prefix, color_name]) - end - - def set_fg_color_256(command_stack) - css_class = get_xterm_color_class(command_stack, "fg") - @fg_color = css_class unless css_class.nil? - end - - def set_bg_color_256(command_stack) - css_class = get_xterm_color_class(command_stack, "bg") - @bg_color = css_class unless css_class.nil? - end - - def get_xterm_color_class(command_stack, prefix) - # the 38 and 48 commands have to be followed by "5" and the color index - return unless command_stack.length >= 2 - return unless command_stack[0] == "5" - - command_stack.shift() # ignore the "5" command - color_index = command_stack.shift().to_i - - return unless color_index >= 0 - return unless color_index <= 255 - - get_color_class(["xterm", prefix, color_index]) - end - - def get_color_class(segments) - [segments].flatten.compact.join('-') - end - end - end -end diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb new file mode 100644 index 00000000000..ad78ae244b2 --- /dev/null +++ b/lib/gitlab/ci/ansi2html.rb @@ -0,0 +1,333 @@ +# ANSI color library +# +# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code +module Gitlab + module Ci + module Ansi2html + # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107) + COLOR = { + 0 => 'black', # not that this is gray in the intense color table + 1 => 'red', + 2 => 'green', + 3 => 'yellow', + 4 => 'blue', + 5 => 'magenta', + 6 => 'cyan', + 7 => 'white', # not that this is gray in the dark (aka default) color table + }.freeze + + STYLE_SWITCHES = { + bold: 0x01, + italic: 0x02, + underline: 0x04, + conceal: 0x08, + cross: 0x10 + }.freeze + + def self.convert(ansi, state = nil) + Converter.new.convert(ansi, state) + end + + class Converter + def on_0(s) reset() end + + def on_1(s) enable(STYLE_SWITCHES[:bold]) end + + def on_3(s) enable(STYLE_SWITCHES[:italic]) end + + def on_4(s) enable(STYLE_SWITCHES[:underline]) end + + def on_8(s) enable(STYLE_SWITCHES[:conceal]) end + + def on_9(s) enable(STYLE_SWITCHES[:cross]) end + + def on_21(s) disable(STYLE_SWITCHES[:bold]) end + + def on_22(s) disable(STYLE_SWITCHES[:bold]) end + + def on_23(s) disable(STYLE_SWITCHES[:italic]) end + + def on_24(s) disable(STYLE_SWITCHES[:underline]) end + + def on_28(s) disable(STYLE_SWITCHES[:conceal]) end + + def on_29(s) disable(STYLE_SWITCHES[:cross]) end + + def on_30(s) set_fg_color(0) end + + def on_31(s) set_fg_color(1) end + + def on_32(s) set_fg_color(2) end + + def on_33(s) set_fg_color(3) end + + def on_34(s) set_fg_color(4) end + + def on_35(s) set_fg_color(5) end + + def on_36(s) set_fg_color(6) end + + def on_37(s) set_fg_color(7) end + + def on_38(s) set_fg_color_256(s) end + + def on_39(s) set_fg_color(9) end + + def on_40(s) set_bg_color(0) end + + def on_41(s) set_bg_color(1) end + + def on_42(s) set_bg_color(2) end + + def on_43(s) set_bg_color(3) end + + def on_44(s) set_bg_color(4) end + + def on_45(s) set_bg_color(5) end + + def on_46(s) set_bg_color(6) end + + def on_47(s) set_bg_color(7) end + + def on_48(s) set_bg_color_256(s) end + + def on_49(s) set_bg_color(9) end + + def on_90(s) set_fg_color(0, 'l') end + + def on_91(s) set_fg_color(1, 'l') end + + def on_92(s) set_fg_color(2, 'l') end + + def on_93(s) set_fg_color(3, 'l') end + + def on_94(s) set_fg_color(4, 'l') end + + def on_95(s) set_fg_color(5, 'l') end + + def on_96(s) set_fg_color(6, 'l') end + + def on_97(s) set_fg_color(7, 'l') end + + def on_99(s) set_fg_color(9, 'l') end + + def on_100(s) set_bg_color(0, 'l') end + + def on_101(s) set_bg_color(1, 'l') end + + def on_102(s) set_bg_color(2, 'l') end + + def on_103(s) set_bg_color(3, 'l') end + + def on_104(s) set_bg_color(4, 'l') end + + def on_105(s) set_bg_color(5, 'l') end + + def on_106(s) set_bg_color(6, 'l') end + + def on_107(s) set_bg_color(7, 'l') end + + def on_109(s) set_bg_color(9, 'l') end + + attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask + + STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze + + def convert(stream, new_state) + reset_state + restore_state(new_state, stream) if new_state.present? + + append = false + truncated = false + + cur_offset = stream.tell + if cur_offset > @offset + @offset = cur_offset + truncated = true + else + stream.seek(@offset) + append = @offset > 0 + end + start_offset = @offset + + open_new_tag + + stream.each_line do |line| + s = StringScanner.new(line) + until s.eos? + if s.scan(/\e([@-_])(.*?)([@-~])/) + handle_sequence(s) + elsif s.scan(/\e(([@-_])(.*?)?)?$/) + break + elsif s.scan(/' + else + @out << s.scan(/./m) + end + @offset += s.matched_size + end + end + + close_open_tags() + + OpenStruct.new( + html: @out.force_encoding(Encoding.default_external), + state: state, + append: append, + truncated: truncated, + offset: start_offset, + size: stream.tell - start_offset, + total: stream.size + ) + end + + def handle_sequence(s) + indicator = s[1] + commands = s[2].split ';' + terminator = s[3] + + # We are only interested in color and text style changes - triggered by + # sequences starting with '\e[' and ending with 'm'. Any other control + # sequence gets stripped (including stuff like "delete last line") + return unless indicator == '[' && terminator == 'm' + + close_open_tags() + + if commands.empty?() + reset() + return + end + + evaluate_command_stack(commands) + + open_new_tag + end + + def evaluate_command_stack(stack) + return unless command = stack.shift() + + if self.respond_to?("on_#{command}", true) + self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend + end + + evaluate_command_stack(stack) + end + + def open_new_tag + css_classes = [] + + unless @fg_color.nil? + fg_color = @fg_color + # Most terminals show bold colored text in the light color variant + # Let's mimic that here + if @style_mask & STYLE_SWITCHES[:bold] != 0 + fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1') + end + css_classes << fg_color + end + css_classes << @bg_color unless @bg_color.nil? + + STYLE_SWITCHES.each do |css_class, flag| + css_classes << "term-#{css_class}" if @style_mask & flag != 0 + end + + return if css_classes.empty? + + @out << %{} + @n_open_tags += 1 + end + + def close_open_tags + while @n_open_tags > 0 + @out << %{} + @n_open_tags -= 1 + end + end + + def reset_state + @offset = 0 + @n_open_tags = 0 + @out = '' + reset + end + + def state + state = STATE_PARAMS.inject({}) do |h, param| + h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend + h + end + Base64.urlsafe_encode64(state.to_json) + end + + def restore_state(new_state, stream) + state = Base64.urlsafe_decode64(new_state) + state = JSON.parse(state, symbolize_names: true) + return if state[:offset].to_i > stream.size + + STATE_PARAMS.each do |param| + send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def reset + @fg_color = nil + @bg_color = nil + @style_mask = 0 + end + + def enable(flag) + @style_mask |= flag + end + + def disable(flag) + @style_mask &= ~flag + end + + def set_fg_color(color_index, prefix = nil) + @fg_color = get_term_color_class(color_index, ["fg", prefix]) + end + + def set_bg_color(color_index, prefix = nil) + @bg_color = get_term_color_class(color_index, ["bg", prefix]) + end + + def get_term_color_class(color_index, prefix) + color_name = COLOR[color_index] + return nil if color_name.nil? + + get_color_class(["term", prefix, color_name]) + end + + def set_fg_color_256(command_stack) + css_class = get_xterm_color_class(command_stack, "fg") + @fg_color = css_class unless css_class.nil? + end + + def set_bg_color_256(command_stack) + css_class = get_xterm_color_class(command_stack, "bg") + @bg_color = css_class unless css_class.nil? + end + + def get_xterm_color_class(command_stack, prefix) + # the 38 and 48 commands have to be followed by "5" and the color index + return unless command_stack.length >= 2 + return unless command_stack[0] == "5" + + command_stack.shift() # ignore the "5" command + color_index = command_stack.shift().to_i + + return unless color_index >= 0 + return unless color_index <= 255 + + get_color_class(["xterm", prefix, color_index]) + end + + def get_color_class(segments) + [segments].flatten.compact.join('-') + end + end + end + end +end diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index 8503ecf8700..ab3408f48d6 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -56,13 +56,13 @@ module Gitlab end def html_with_state(state = nil) - ::Ci::Ansi2html.convert(stream, state) + ::Gitlab::Ci::Ansi2html.convert(stream, state) end def html(last_lines: nil) text = raw(last_lines: last_lines) buffer = StringIO.new(text) - ::Ci::Ansi2html.convert(buffer).html + ::Gitlab::Ci::Ansi2html.convert(buffer).html end def extract_coverage(regex) diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb deleted file mode 100644 index e49ecadde20..00000000000 --- a/spec/lib/ci/ansi2html_spec.rb +++ /dev/null @@ -1,220 +0,0 @@ -require 'spec_helper' - -describe Ci::Ansi2html do - subject { described_class } - - it "prints non-ansi as-is" do - expect(convert_html("Hello")).to eq('Hello') - end - - it "strips non-color-changing controll sequences" do - expect(convert_html("Hello \e[2Kworld")).to eq('Hello world') - end - - it "prints simply red" do - expect(convert_html("\e[31mHello\e[0m")).to eq('Hello') - end - - it "prints simply red without trailing reset" do - expect(convert_html("\e[31mHello")).to eq('Hello') - end - - it "prints simply yellow" do - expect(convert_html("\e[33mHello\e[0m")).to eq('Hello') - end - - it "prints default on blue" do - expect(convert_html("\e[39;44mHello")).to eq('Hello') - end - - it "prints red on blue" do - expect(convert_html("\e[31;44mHello")).to eq('Hello') - end - - it "resets colors after red on blue" do - expect(convert_html("\e[31;44mHello\e[0m world")).to eq('Hello world') - end - - it "performs color change from red/blue to yellow/blue" do - expect(convert_html("\e[31;44mHello \e[33mworld")).to eq('Hello world') - end - - it "performs color change from red/blue to yellow/green" do - expect(convert_html("\e[31;44mHello \e[33;42mworld")).to eq('Hello world') - end - - it "performs color change from red/blue to reset to yellow/green" do - expect(convert_html("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('Hello world') - end - - it "ignores unsupported codes" do - expect(convert_html("\e[51mHello\e[0m")).to eq('Hello') - end - - it "prints light red" do - expect(convert_html("\e[91mHello\e[0m")).to eq('Hello') - end - - it "prints default on light red" do - expect(convert_html("\e[101mHello\e[0m")).to eq('Hello') - end - - it "performs color change from red/blue to default/blue" do - expect(convert_html("\e[31;44mHello \e[39mworld")).to eq('Hello world') - end - - it "performs color change from light red/blue to default/blue" do - expect(convert_html("\e[91;44mHello \e[39mworld")).to eq('Hello world') - end - - it "prints bold text" do - expect(convert_html("\e[1mHello")).to eq('Hello') - end - - it "resets bold text" do - expect(convert_html("\e[1mHello\e[21m world")).to eq('Hello world') - expect(convert_html("\e[1mHello\e[22m world")).to eq('Hello world') - end - - it "prints italic text" do - expect(convert_html("\e[3mHello")).to eq('Hello') - end - - it "resets italic text" do - expect(convert_html("\e[3mHello\e[23m world")).to eq('Hello world') - end - - it "prints underlined text" do - expect(convert_html("\e[4mHello")).to eq('Hello') - end - - it "resets underlined text" do - expect(convert_html("\e[4mHello\e[24m world")).to eq('Hello world') - end - - it "prints concealed text" do - expect(convert_html("\e[8mHello")).to eq('Hello') - end - - it "resets concealed text" do - expect(convert_html("\e[8mHello\e[28m world")).to eq('Hello world') - end - - it "prints crossed-out text" do - expect(convert_html("\e[9mHello")).to eq('Hello') - end - - it "resets crossed-out text" do - expect(convert_html("\e[9mHello\e[29m world")).to eq('Hello world') - end - - it "can print 256 xterm fg colors" do - expect(convert_html("\e[38;5;16mHello")).to eq('Hello') - end - - it "can print 256 xterm fg colors on normal magenta background" do - expect(convert_html("\e[38;5;16;45mHello")).to eq('Hello') - end - - it "can print 256 xterm bg colors" do - expect(convert_html("\e[48;5;240mHello")).to eq('Hello') - end - - it "can print 256 xterm bg colors on normal magenta foreground" do - expect(convert_html("\e[48;5;16;35mHello")).to eq('Hello') - end - - it "prints bold colored text vividly" do - expect(convert_html("\e[1;31mHello\e[0m")).to eq('Hello') - end - - it "prints bold light colored text correctly" do - expect(convert_html("\e[1;91mHello\e[0m")).to eq('Hello') - end - - it "prints <" do - expect(convert_html("<")).to eq('<') - end - - it "replaces newlines with line break tags" do - expect(convert_html("\n")).to eq('
') - end - - it "groups carriage returns with newlines" do - expect(convert_html("\r\n")).to eq('
') - end - - describe "incremental update" do - shared_examples 'stateable converter' do - let(:pass1_stream) { StringIO.new(pre_text) } - let(:pass2_stream) { StringIO.new(pre_text + text) } - let(:pass1) { subject.convert(pass1_stream) } - let(:pass2) { subject.convert(pass2_stream, pass1.state) } - - it "to returns html to append" do - expect(pass2.append).to be_truthy - expect(pass2.html).to eq(html) - expect(pass1.html + pass2.html).to eq(pre_html + html) - end - end - - context "with split word" do - let(:pre_text) { "\e[1mHello" } - let(:pre_html) { "Hello" } - let(:text) { "\e[1mWorld" } - let(:html) { "World" } - - it_behaves_like 'stateable converter' - end - - context "with split sequence" do - let(:pre_text) { "\e[1m" } - let(:pre_html) { "" } - let(:text) { "Hello" } - let(:html) { "Hello" } - - it_behaves_like 'stateable converter' - end - - context "with partial sequence" do - let(:pre_text) { "Hello\e" } - let(:pre_html) { "Hello" } - let(:text) { "[1m World" } - let(:html) { " World" } - - it_behaves_like 'stateable converter' - end - - context 'with new line' do - let(:pre_text) { "Hello\r" } - let(:pre_html) { "Hello\r" } - let(:text) { "\nWorld" } - let(:html) { "
World" } - - it_behaves_like 'stateable converter' - end - end - - describe "truncates" do - let(:text) { "Hello World" } - let(:stream) { StringIO.new(text) } - let(:subject) { described_class.convert(stream) } - - before do - stream.seek(3, IO::SEEK_SET) - end - - it "returns truncated output" do - expect(subject.truncated).to be_truthy - end - - it "does not append output" do - expect(subject.append).to be_falsey - end - end - - def convert_html(data) - stream = StringIO.new(data) - subject.convert(stream).html - end -end diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb new file mode 100644 index 00000000000..e6645985ba4 --- /dev/null +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -0,0 +1,220 @@ +require 'spec_helper' + +describe Gitlab::Ci::Ansi2html do + subject { described_class } + + it "prints non-ansi as-is" do + expect(convert_html("Hello")).to eq('Hello') + end + + it "strips non-color-changing controll sequences" do + expect(convert_html("Hello \e[2Kworld")).to eq('Hello world') + end + + it "prints simply red" do + expect(convert_html("\e[31mHello\e[0m")).to eq('Hello') + end + + it "prints simply red without trailing reset" do + expect(convert_html("\e[31mHello")).to eq('Hello') + end + + it "prints simply yellow" do + expect(convert_html("\e[33mHello\e[0m")).to eq('Hello') + end + + it "prints default on blue" do + expect(convert_html("\e[39;44mHello")).to eq('Hello') + end + + it "prints red on blue" do + expect(convert_html("\e[31;44mHello")).to eq('Hello') + end + + it "resets colors after red on blue" do + expect(convert_html("\e[31;44mHello\e[0m world")).to eq('Hello world') + end + + it "performs color change from red/blue to yellow/blue" do + expect(convert_html("\e[31;44mHello \e[33mworld")).to eq('Hello world') + end + + it "performs color change from red/blue to yellow/green" do + expect(convert_html("\e[31;44mHello \e[33;42mworld")).to eq('Hello world') + end + + it "performs color change from red/blue to reset to yellow/green" do + expect(convert_html("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('Hello world') + end + + it "ignores unsupported codes" do + expect(convert_html("\e[51mHello\e[0m")).to eq('Hello') + end + + it "prints light red" do + expect(convert_html("\e[91mHello\e[0m")).to eq('Hello') + end + + it "prints default on light red" do + expect(convert_html("\e[101mHello\e[0m")).to eq('Hello') + end + + it "performs color change from red/blue to default/blue" do + expect(convert_html("\e[31;44mHello \e[39mworld")).to eq('Hello world') + end + + it "performs color change from light red/blue to default/blue" do + expect(convert_html("\e[91;44mHello \e[39mworld")).to eq('Hello world') + end + + it "prints bold text" do + expect(convert_html("\e[1mHello")).to eq('Hello') + end + + it "resets bold text" do + expect(convert_html("\e[1mHello\e[21m world")).to eq('Hello world') + expect(convert_html("\e[1mHello\e[22m world")).to eq('Hello world') + end + + it "prints italic text" do + expect(convert_html("\e[3mHello")).to eq('Hello') + end + + it "resets italic text" do + expect(convert_html("\e[3mHello\e[23m world")).to eq('Hello world') + end + + it "prints underlined text" do + expect(convert_html("\e[4mHello")).to eq('Hello') + end + + it "resets underlined text" do + expect(convert_html("\e[4mHello\e[24m world")).to eq('Hello world') + end + + it "prints concealed text" do + expect(convert_html("\e[8mHello")).to eq('Hello') + end + + it "resets concealed text" do + expect(convert_html("\e[8mHello\e[28m world")).to eq('Hello world') + end + + it "prints crossed-out text" do + expect(convert_html("\e[9mHello")).to eq('Hello') + end + + it "resets crossed-out text" do + expect(convert_html("\e[9mHello\e[29m world")).to eq('Hello world') + end + + it "can print 256 xterm fg colors" do + expect(convert_html("\e[38;5;16mHello")).to eq('Hello') + end + + it "can print 256 xterm fg colors on normal magenta background" do + expect(convert_html("\e[38;5;16;45mHello")).to eq('Hello') + end + + it "can print 256 xterm bg colors" do + expect(convert_html("\e[48;5;240mHello")).to eq('Hello') + end + + it "can print 256 xterm bg colors on normal magenta foreground" do + expect(convert_html("\e[48;5;16;35mHello")).to eq('Hello') + end + + it "prints bold colored text vividly" do + expect(convert_html("\e[1;31mHello\e[0m")).to eq('Hello') + end + + it "prints bold light colored text correctly" do + expect(convert_html("\e[1;91mHello\e[0m")).to eq('Hello') + end + + it "prints <" do + expect(convert_html("<")).to eq('<') + end + + it "replaces newlines with line break tags" do + expect(convert_html("\n")).to eq('
') + end + + it "groups carriage returns with newlines" do + expect(convert_html("\r\n")).to eq('
') + end + + describe "incremental update" do + shared_examples 'stateable converter' do + let(:pass1_stream) { StringIO.new(pre_text) } + let(:pass2_stream) { StringIO.new(pre_text + text) } + let(:pass1) { subject.convert(pass1_stream) } + let(:pass2) { subject.convert(pass2_stream, pass1.state) } + + it "to returns html to append" do + expect(pass2.append).to be_truthy + expect(pass2.html).to eq(html) + expect(pass1.html + pass2.html).to eq(pre_html + html) + end + end + + context "with split word" do + let(:pre_text) { "\e[1mHello" } + let(:pre_html) { "Hello" } + let(:text) { "\e[1mWorld" } + let(:html) { "World" } + + it_behaves_like 'stateable converter' + end + + context "with split sequence" do + let(:pre_text) { "\e[1m" } + let(:pre_html) { "" } + let(:text) { "Hello" } + let(:html) { "Hello" } + + it_behaves_like 'stateable converter' + end + + context "with partial sequence" do + let(:pre_text) { "Hello\e" } + let(:pre_html) { "Hello" } + let(:text) { "[1m World" } + let(:html) { " World" } + + it_behaves_like 'stateable converter' + end + + context 'with new line' do + let(:pre_text) { "Hello\r" } + let(:pre_html) { "Hello\r" } + let(:text) { "\nWorld" } + let(:html) { "
World" } + + it_behaves_like 'stateable converter' + end + end + + describe "truncates" do + let(:text) { "Hello World" } + let(:stream) { StringIO.new(text) } + let(:subject) { described_class.convert(stream) } + + before do + stream.seek(3, IO::SEEK_SET) + end + + it "returns truncated output" do + expect(subject.truncated).to be_truthy + end + + it "does not append output" do + expect(subject.append).to be_falsey + end + end + + def convert_html(data) + stream = StringIO.new(data) + subject.convert(stream).html + end +end -- cgit v1.2.1 From f364cc34ea7562e782964b2c65428055d525e440 Mon Sep 17 00:00:00 2001 From: Maxim Rydkin Date: Wed, 6 Sep 2017 14:11:00 +0300 Subject: move `lib/ci/charts.rb` into `lib/gitlab/ci/charts.rb` --- app/controllers/projects/pipelines_controller.rb | 8 +- lib/ci/charts.rb | 116 ---------------------- lib/gitlab/ci/charts.rb | 118 +++++++++++++++++++++++ spec/lib/ci/charts_spec.rb | 24 ----- spec/lib/gitlab/ci/charts_spec.rb | 24 +++++ 5 files changed, 146 insertions(+), 144 deletions(-) delete mode 100644 lib/ci/charts.rb create mode 100644 lib/gitlab/ci/charts.rb delete mode 100644 spec/lib/ci/charts_spec.rb create mode 100644 spec/lib/gitlab/ci/charts_spec.rb diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index a3bfbf0694e..7ad7b3003af 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController def charts @charts = {} - @charts[:week] = Ci::Charts::WeekChart.new(project) - @charts[:month] = Ci::Charts::MonthChart.new(project) - @charts[:year] = Ci::Charts::YearChart.new(project) - @charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project) + @charts[:week] = Gitlab::Ci::Charts::WeekChart.new(project) + @charts[:month] = Gitlab::Ci::Charts::MonthChart.new(project) + @charts[:year] = Gitlab::Ci::Charts::YearChart.new(project) + @charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project) @counts = {} @counts[:total] = @project.pipelines.count(:all) diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb deleted file mode 100644 index 76a69bf8a83..00000000000 --- a/lib/ci/charts.rb +++ /dev/null @@ -1,116 +0,0 @@ -module Ci - module Charts - module DailyInterval - def grouped_count(query) - query - .group("DATE(#{Ci::Pipeline.table_name}.created_at)") - .count(:created_at) - .transform_keys { |date| date.strftime(@format) } - end - - def interval_step - @interval_step ||= 1.day - end - end - - module MonthlyInterval - def grouped_count(query) - if Gitlab::Database.postgresql? - query - .group("to_char(#{Ci::Pipeline.table_name}.created_at, '01 Month YYYY')") - .count(:created_at) - .transform_keys(&:squish) - else - query - .group("DATE_FORMAT(#{Ci::Pipeline.table_name}.created_at, '01 %M %Y')") - .count(:created_at) - end - end - - def interval_step - @interval_step ||= 1.month - end - end - - class Chart - attr_reader :labels, :total, :success, :project, :pipeline_times - - def initialize(project) - @labels = [] - @total = [] - @success = [] - @pipeline_times = [] - @project = project - - collect - end - - def collect - query = project.pipelines - .where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection - - totals_count = grouped_count(query) - success_count = grouped_count(query.success) - - current = @from - while current < @to - label = current.strftime(@format) - - @labels << label - @total << (totals_count[label] || 0) - @success << (success_count[label] || 0) - - current += interval_step - end - end - end - - class YearChart < Chart - include MonthlyInterval - - def initialize(*) - @to = Date.today.end_of_month - @from = @to.years_ago(1).beginning_of_month - @format = '%d %B %Y' - - super - end - end - - class MonthChart < Chart - include DailyInterval - - def initialize(*) - @to = Date.today - @from = @to - 30.days - @format = '%d %B' - - super - end - end - - class WeekChart < Chart - include DailyInterval - - def initialize(*) - @to = Date.today - @from = @to - 7.days - @format = '%d %B' - - super - end - end - - class PipelineTime < Chart - def collect - commits = project.pipelines.last(30) - - commits.each do |commit| - @labels << commit.short_sha - duration = commit.duration || 0 - @pipeline_times << (duration / 60) - end - end - end - end -end diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb new file mode 100644 index 00000000000..ac4bd5d6684 --- /dev/null +++ b/lib/gitlab/ci/charts.rb @@ -0,0 +1,118 @@ +module Gitlab + module Ci + module Charts + module DailyInterval + def grouped_count(query) + query + .group("DATE(#{Ci::Pipeline.table_name}.created_at)") + .count(:created_at) + .transform_keys { |date| date.strftime(@format) } + end + + def interval_step + @interval_step ||= 1.day + end + end + + module MonthlyInterval + def grouped_count(query) + if Gitlab::Database.postgresql? + query + .group("to_char(#{Ci::Pipeline.table_name}.created_at, '01 Month YYYY')") + .count(:created_at) + .transform_keys(&:squish) + else + query + .group("DATE_FORMAT(#{Ci::Pipeline.table_name}.created_at, '01 %M %Y')") + .count(:created_at) + end + end + + def interval_step + @interval_step ||= 1.month + end + end + + class Chart + attr_reader :labels, :total, :success, :project, :pipeline_times + + def initialize(project) + @labels = [] + @total = [] + @success = [] + @pipeline_times = [] + @project = project + + collect + end + + def collect + query = project.pipelines + .where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection + + totals_count = grouped_count(query) + success_count = grouped_count(query.success) + + current = @from + while current < @to + label = current.strftime(@format) + + @labels << label + @total << (totals_count[label] || 0) + @success << (success_count[label] || 0) + + current += interval_step + end + end + end + + class YearChart < Chart + include MonthlyInterval + + def initialize(*) + @to = Date.today.end_of_month + @from = @to.years_ago(1).beginning_of_month + @format = '%d %B %Y' + + super + end + end + + class MonthChart < Chart + include DailyInterval + + def initialize(*) + @to = Date.today + @from = @to - 30.days + @format = '%d %B' + + super + end + end + + class WeekChart < Chart + include DailyInterval + + def initialize(*) + @to = Date.today + @from = @to - 7.days + @format = '%d %B' + + super + end + end + + class PipelineTime < Chart + def collect + commits = project.pipelines.last(30) + + commits.each do |commit| + @labels << commit.short_sha + duration = commit.duration || 0 + @pipeline_times << (duration / 60) + end + end + end + end + end +end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb deleted file mode 100644 index f0769deef21..00000000000 --- a/spec/lib/ci/charts_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'spec_helper' - -describe Ci::Charts do - context "pipeline_times" do - let(:project) { create(:project) } - let(:chart) { Ci::Charts::PipelineTime.new(project) } - - subject { chart.pipeline_times } - - before do - create(:ci_empty_pipeline, project: project, duration: 120) - end - - it 'returns pipeline times in minutes' do - is_expected.to contain_exactly(2) - end - - it 'handles nil pipeline times' do - create(:ci_empty_pipeline, project: project, duration: nil) - - is_expected.to contain_exactly(2, 0) - end - end -end diff --git a/spec/lib/gitlab/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb new file mode 100644 index 00000000000..f8188675013 --- /dev/null +++ b/spec/lib/gitlab/ci/charts_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::Ci::Charts do + context "pipeline_times" do + let(:project) { create(:project) } + let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) } + + subject { chart.pipeline_times } + + before do + create(:ci_empty_pipeline, project: project, duration: 120) + end + + it 'returns pipeline times in minutes' do + is_expected.to contain_exactly(2) + end + + it 'handles nil pipeline times' do + create(:ci_empty_pipeline, project: project, duration: nil) + + is_expected.to contain_exactly(2, 0) + end + end +end -- cgit v1.2.1 From c295d3362b610945de5c722616b39323a48c377e Mon Sep 17 00:00:00 2001 From: Maxim Rydkin Date: Wed, 6 Sep 2017 14:13:52 +0300 Subject: move `lib/ci/model.rb` into `lib/gitlab/ci/model.rb` --- app/models/ci/group_variable.rb | 2 +- app/models/ci/pipeline.rb | 2 +- app/models/ci/pipeline_schedule.rb | 2 +- app/models/ci/pipeline_schedule_variable.rb | 2 +- app/models/ci/pipeline_variable.rb | 2 +- app/models/ci/runner.rb | 2 +- app/models/ci/runner_project.rb | 2 +- app/models/ci/stage.rb | 2 +- app/models/ci/trigger.rb | 2 +- app/models/ci/trigger_request.rb | 2 +- app/models/ci/variable.rb | 2 +- lib/ci/model.rb | 11 ----------- lib/gitlab/ci/model.rb | 13 +++++++++++++ 13 files changed, 24 insertions(+), 22 deletions(-) delete mode 100644 lib/ci/model.rb create mode 100644 lib/gitlab/ci/model.rb diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index f64bc245a67..afeae69ba39 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -1,6 +1,6 @@ module Ci class GroupVariable < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasVariable include Presentable diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 871c76fbad3..72946439d43 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1,6 +1,6 @@ module Ci class Pipeline < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasStatus include Importable include AfterCommitQueue diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index e7e02587759..10ead6b6d3b 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -1,6 +1,6 @@ module Ci class PipelineSchedule < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include Importable acts_as_paranoid diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb index ee5b8733fac..af989fb14b4 100644 --- a/app/models/ci/pipeline_schedule_variable.rb +++ b/app/models/ci/pipeline_schedule_variable.rb @@ -1,6 +1,6 @@ module Ci class PipelineScheduleVariable < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasVariable belongs_to :pipeline_schedule diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index 00b419c3efa..de5aae17a15 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -1,6 +1,6 @@ module Ci class PipelineVariable < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasVariable belongs_to :pipeline diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index b1798084787..a0d07902ba2 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -1,6 +1,6 @@ module Ci class Runner < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model RUNNER_QUEUE_EXPIRY_TIME = 60.minutes ONLINE_CONTACT_TIMEOUT = 1.hour diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 5f01a0daae9..505d178ba8e 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -1,6 +1,6 @@ module Ci class RunnerProject < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model belongs_to :runner belongs_to :project diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 754c37518b3..75b8ea2a371 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -1,6 +1,6 @@ module Ci class Stage < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include Importable include HasStatus include Gitlab::OptimisticLocking diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 6df41a3f301..b5290bcaf53 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -1,6 +1,6 @@ module Ci class Trigger < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model acts_as_paranoid diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 2c860598281..215b1cf6753 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -1,6 +1,6 @@ module Ci class TriggerRequest < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model belongs_to :trigger belongs_to :pipeline, foreign_key: :commit_id diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index cf0fe04ddaf..67d3ec81b6f 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -1,6 +1,6 @@ module Ci class Variable < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasVariable include Presentable diff --git a/lib/ci/model.rb b/lib/ci/model.rb deleted file mode 100644 index c42a0ad36db..00000000000 --- a/lib/ci/model.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Ci - module Model - def table_name_prefix - "ci_" - end - - def model_name - @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last) - end - end -end diff --git a/lib/gitlab/ci/model.rb b/lib/gitlab/ci/model.rb new file mode 100644 index 00000000000..3994a50772b --- /dev/null +++ b/lib/gitlab/ci/model.rb @@ -0,0 +1,13 @@ +module Gitlab + module Ci + module Model + def table_name_prefix + "ci_" + end + + def model_name + @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last) + end + end + end +end -- cgit v1.2.1 From c45ace8972f18af1f232f9074d6e4104fc4f0c14 Mon Sep 17 00:00:00 2001 From: Maxim Rydkin Date: Wed, 6 Sep 2017 14:23:24 +0300 Subject: move `lib/ci/gitlab_ci_yaml_processor.rb` into `lib/gitlab/ci/yaml_processor.rb` --- app/controllers/ci/lints_controller.rb | 4 +- app/models/blob_viewer/gitlab_ci_yml.rb | 2 +- app/models/ci/pipeline.rb | 4 +- lib/api/lint.rb | 2 +- lib/ci/gitlab_ci_yaml_processor.rb | 251 ---- lib/gitlab/ci/yaml_processor.rb | 253 ++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 1697 ------------------------- spec/lib/gitlab/ci/yaml_processor_spec.rb | 1699 ++++++++++++++++++++++++++ spec/views/ci/lints/show.html.haml_spec.rb | 4 +- 9 files changed, 1960 insertions(+), 1956 deletions(-) delete mode 100644 lib/ci/gitlab_ci_yaml_processor.rb create mode 100644 lib/gitlab/ci/yaml_processor.rb delete mode 100644 spec/lib/ci/gitlab_ci_yaml_processor_spec.rb create mode 100644 spec/lib/gitlab/ci/yaml_processor_spec.rb diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 3eb485de9db..be667687c18 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -7,11 +7,11 @@ module Ci def create @content = params[:content] - @error = Ci::GitlabCiYamlProcessor.validation_message(@content) + @error = Gitlab::Ci::YamlProcessor.validation_message(@content) @status = @error.blank? if @error.blank? - @config_processor = Ci::GitlabCiYamlProcessor.new(@content) + @config_processor = Gitlab::Ci::YamlProcessor.new(@content) @stages = @config_processor.stages @builds = @config_processor.builds @jobs = @config_processor.jobs diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb index 7267c3965d3..53bc247dec1 100644 --- a/app/models/blob_viewer/gitlab_ci_yml.rb +++ b/app/models/blob_viewer/gitlab_ci_yml.rb @@ -13,7 +13,7 @@ module BlobViewer prepare! - @validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data) + @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data) end def valid? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 72946439d43..476db384bbd 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -336,8 +336,8 @@ module Ci return @config_processor if defined?(@config_processor) @config_processor ||= begin - Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path) - rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e + Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path) + rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e self.yaml_errors = e.message nil rescue diff --git a/lib/api/lint.rb b/lib/api/lint.rb index ae43a4a3237..d202eaa4c49 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -6,7 +6,7 @@ module API requires :content, type: String, desc: 'Content of .gitlab-ci.yml' end post '/lint' do - error = Ci::GitlabCiYamlProcessor.validation_message(params[:content]) + error = Gitlab::Ci::YamlProcessor.validation_message(params[:content]) status 200 diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb deleted file mode 100644 index 62b44389b15..00000000000 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ /dev/null @@ -1,251 +0,0 @@ -module Ci - class GitlabCiYamlProcessor - ValidationError = Class.new(StandardError) - - include Gitlab::Ci::Config::Entry::LegacyValidationHelpers - - attr_reader :path, :cache, :stages, :jobs - - def initialize(config, path = nil) - @ci_config = Gitlab::Ci::Config.new(config) - @config = @ci_config.to_hash - @path = path - - unless @ci_config.valid? - raise ValidationError, @ci_config.errors.first - end - - initial_parsing - rescue Gitlab::Ci::Config::Loader::FormatError => e - raise ValidationError, e.message - end - - def builds_for_stage_and_ref(stage, ref, tag = false, source = nil) - jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _| - build_attributes(name) - end - end - - def builds - @jobs.map do |name, _| - build_attributes(name) - end - end - - def stage_seeds(pipeline) - seeds = @stages.uniq.map do |stage| - builds = pipeline_stage_builds(stage, pipeline) - - Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? - end - - seeds.compact - end - - def build_attributes(name) - job = @jobs[name.to_sym] || {} - - { stage_idx: @stages.index(job[:stage]), - stage: job[:stage], - commands: job[:commands], - tag_list: job[:tags] || [], - name: job[:name].to_s, - allow_failure: job[:ignore], - when: job[:when] || 'on_success', - environment: job[:environment_name], - coverage_regex: job[:coverage], - yaml_variables: yaml_variables(name), - options: { - image: job[:image], - services: job[:services], - artifacts: job[:artifacts], - cache: job[:cache], - dependencies: job[:dependencies], - before_script: job[:before_script], - script: job[:script], - after_script: job[:after_script], - environment: job[:environment], - retry: job[:retry] - }.compact } - end - - def self.validation_message(content) - return 'Please provide content of .gitlab-ci.yml' if content.blank? - - begin - Ci::GitlabCiYamlProcessor.new(content) - nil - rescue ValidationError, Psych::SyntaxError => e - e.message - end - end - - private - - def pipeline_stage_builds(stage, pipeline) - builds = builds_for_stage_and_ref( - stage, pipeline.ref, pipeline.tag?, pipeline.source) - - builds.select do |build| - job = @jobs[build.fetch(:name).to_sym] - has_kubernetes = pipeline.has_kubernetes_active? - only_kubernetes = job.dig(:only, :kubernetes) - except_kubernetes = job.dig(:except, :kubernetes) - - [!only_kubernetes && !except_kubernetes, - only_kubernetes && has_kubernetes, - except_kubernetes && !has_kubernetes].any? - end - end - - def jobs_for_ref(ref, tag = false, source = nil) - @jobs.select do |_, job| - process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source) - end - end - - def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) - jobs_for_ref(ref, tag, source).select do |_, job| - job[:stage] == stage - end - end - - def initial_parsing - ## - # Global config - # - @before_script = @ci_config.before_script - @image = @ci_config.image - @after_script = @ci_config.after_script - @services = @ci_config.services - @variables = @ci_config.variables - @stages = @ci_config.stages - @cache = @ci_config.cache - - ## - # Jobs - # - @jobs = @ci_config.jobs - - @jobs.each do |name, job| - # logical validation for job - - validate_job_stage!(name, job) - validate_job_dependencies!(name, job) - validate_job_environment!(name, job) - end - end - - def yaml_variables(name) - variables = (@variables || {}) - .merge(job_variables(name)) - - variables.map do |key, value| - { key: key.to_s, value: value, public: true } - end - end - - def job_variables(name) - job = @jobs[name.to_sym] - return {} unless job - - job[:variables] || {} - end - - def validate_job_stage!(name, job) - return unless job[:stage] - - unless job[:stage].is_a?(String) && job[:stage].in?(@stages) - raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" - end - end - - def validate_job_dependencies!(name, job) - return unless job[:dependencies] - - stage_index = @stages.index(job[:stage]) - - job[:dependencies].each do |dependency| - raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym] - - unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index - raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" - end - end - end - - def validate_job_environment!(name, job) - return unless job[:environment] - return unless job[:environment].is_a?(Hash) - - environment = job[:environment] - validate_on_stop_job!(name, environment, environment[:on_stop]) - end - - def validate_on_stop_job!(name, environment, on_stop) - return unless on_stop - - on_stop_job = @jobs[on_stop.to_sym] - unless on_stop_job - raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined" - end - - unless on_stop_job[:environment] - raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined" - end - - unless on_stop_job[:environment][:name] == environment[:name] - raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name" - end - - unless on_stop_job[:environment][:action] == 'stop' - raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined" - end - end - - def process?(only_params, except_params, ref, tag, source) - if only_params.present? - return false unless matching?(only_params, ref, tag, source) - end - - if except_params.present? - return false if matching?(except_params, ref, tag, source) - end - - true - end - - def matching?(patterns, ref, tag, source) - patterns.any? do |pattern| - pattern, path = pattern.split('@', 2) - matches_path?(path) && matches_pattern?(pattern, ref, tag, source) - end - end - - def matches_path?(path) - return true unless path - - path == self.path - end - - def matches_pattern?(pattern, ref, tag, source) - return true if tag && pattern == 'tags' - return true if !tag && pattern == 'branches' - return true if source_to_pattern(source) == pattern - - if pattern.first == "/" && pattern.last == "/" - Regexp.new(pattern[1...-1]) =~ ref - else - pattern == ref - end - end - - def source_to_pattern(source) - if %w[api external web].include?(source) - source - else - source&.pluralize - end - end - end -end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb new file mode 100644 index 00000000000..7582964b24e --- /dev/null +++ b/lib/gitlab/ci/yaml_processor.rb @@ -0,0 +1,253 @@ +module Gitlab + module Ci + class YamlProcessor + ValidationError = Class.new(StandardError) + + include Gitlab::Ci::Config::Entry::LegacyValidationHelpers + + attr_reader :path, :cache, :stages, :jobs + + def initialize(config, path = nil) + @ci_config = Gitlab::Ci::Config.new(config) + @config = @ci_config.to_hash + @path = path + + unless @ci_config.valid? + raise ValidationError, @ci_config.errors.first + end + + initial_parsing + rescue Gitlab::Ci::Config::Loader::FormatError => e + raise ValidationError, e.message + end + + def builds_for_stage_and_ref(stage, ref, tag = false, source = nil) + jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _| + build_attributes(name) + end + end + + def builds + @jobs.map do |name, _| + build_attributes(name) + end + end + + def stage_seeds(pipeline) + seeds = @stages.uniq.map do |stage| + builds = pipeline_stage_builds(stage, pipeline) + + Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? + end + + seeds.compact + end + + def build_attributes(name) + job = @jobs[name.to_sym] || {} + + { stage_idx: @stages.index(job[:stage]), + stage: job[:stage], + commands: job[:commands], + tag_list: job[:tags] || [], + name: job[:name].to_s, + allow_failure: job[:ignore], + when: job[:when] || 'on_success', + environment: job[:environment_name], + coverage_regex: job[:coverage], + yaml_variables: yaml_variables(name), + options: { + image: job[:image], + services: job[:services], + artifacts: job[:artifacts], + cache: job[:cache], + dependencies: job[:dependencies], + before_script: job[:before_script], + script: job[:script], + after_script: job[:after_script], + environment: job[:environment], + retry: job[:retry] + }.compact } + end + + def self.validation_message(content) + return 'Please provide content of .gitlab-ci.yml' if content.blank? + + begin + Gitlab::Ci::YamlProcessor.new(content) + nil + rescue ValidationError, Psych::SyntaxError => e + e.message + end + end + + private + + def pipeline_stage_builds(stage, pipeline) + builds = builds_for_stage_and_ref( + stage, pipeline.ref, pipeline.tag?, pipeline.source) + + builds.select do |build| + job = @jobs[build.fetch(:name).to_sym] + has_kubernetes = pipeline.has_kubernetes_active? + only_kubernetes = job.dig(:only, :kubernetes) + except_kubernetes = job.dig(:except, :kubernetes) + + [!only_kubernetes && !except_kubernetes, + only_kubernetes && has_kubernetes, + except_kubernetes && !has_kubernetes].any? + end + end + + def jobs_for_ref(ref, tag = false, source = nil) + @jobs.select do |_, job| + process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source) + end + end + + def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) + jobs_for_ref(ref, tag, source).select do |_, job| + job[:stage] == stage + end + end + + def initial_parsing + ## + # Global config + # + @before_script = @ci_config.before_script + @image = @ci_config.image + @after_script = @ci_config.after_script + @services = @ci_config.services + @variables = @ci_config.variables + @stages = @ci_config.stages + @cache = @ci_config.cache + + ## + # Jobs + # + @jobs = @ci_config.jobs + + @jobs.each do |name, job| + # logical validation for job + + validate_job_stage!(name, job) + validate_job_dependencies!(name, job) + validate_job_environment!(name, job) + end + end + + def yaml_variables(name) + variables = (@variables || {}) + .merge(job_variables(name)) + + variables.map do |key, value| + { key: key.to_s, value: value, public: true } + end + end + + def job_variables(name) + job = @jobs[name.to_sym] + return {} unless job + + job[:variables] || {} + end + + def validate_job_stage!(name, job) + return unless job[:stage] + + unless job[:stage].is_a?(String) && job[:stage].in?(@stages) + raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" + end + end + + def validate_job_dependencies!(name, job) + return unless job[:dependencies] + + stage_index = @stages.index(job[:stage]) + + job[:dependencies].each do |dependency| + raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym] + + unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index + raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" + end + end + end + + def validate_job_environment!(name, job) + return unless job[:environment] + return unless job[:environment].is_a?(Hash) + + environment = job[:environment] + validate_on_stop_job!(name, environment, environment[:on_stop]) + end + + def validate_on_stop_job!(name, environment, on_stop) + return unless on_stop + + on_stop_job = @jobs[on_stop.to_sym] + unless on_stop_job + raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined" + end + + unless on_stop_job[:environment] + raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined" + end + + unless on_stop_job[:environment][:name] == environment[:name] + raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name" + end + + unless on_stop_job[:environment][:action] == 'stop' + raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined" + end + end + + def process?(only_params, except_params, ref, tag, source) + if only_params.present? + return false unless matching?(only_params, ref, tag, source) + end + + if except_params.present? + return false if matching?(except_params, ref, tag, source) + end + + true + end + + def matching?(patterns, ref, tag, source) + patterns.any? do |pattern| + pattern, path = pattern.split('@', 2) + matches_path?(path) && matches_pattern?(pattern, ref, tag, source) + end + end + + def matches_path?(path) + return true unless path + + path == self.path + end + + def matches_pattern?(pattern, ref, tag, source) + return true if tag && pattern == 'tags' + return true if !tag && pattern == 'branches' + return true if source_to_pattern(source) == pattern + + if pattern.first == "/" && pattern.last == "/" + Regexp.new(pattern[1...-1]) =~ ref + else + pattern == ref + end + end + + def source_to_pattern(source) + if %w[api external web].include?(source) + source + else + source&.pluralize + end + end + end + end +end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb deleted file mode 100644 index 1efd3113a43..00000000000 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ /dev/null @@ -1,1697 +0,0 @@ -require 'spec_helper' - -module Ci - describe GitlabCiYamlProcessor, :lib do - subject { described_class.new(config, path) } - let(:path) { 'path' } - - describe 'our current .gitlab-ci.yml' do - let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } - - it 'is valid' do - error_message = described_class.validation_message(config) - - expect(error_message).to be_nil - end - end - - describe '#build_attributes' do - subject { described_class.new(config, path).build_attributes(:rspec) } - - describe 'coverage entry' do - describe 'code coverage regexp' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - coverage: '/Code coverage: \d+\.\d+/' }) - end - - it 'includes coverage regexp in build attributes' do - expect(subject) - .to include(coverage_regex: 'Code coverage: \d+\.\d+') - end - end - end - - describe 'retry entry' do - context 'when retry count is specified' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', retry: 1 }) - end - - it 'includes retry count in build options attribute' do - expect(subject[:options]).to include(retry: 1) - end - end - - context 'when retry count is not specified' do - let(:config) do - YAML.dump(rspec: { script: 'rspec' }) - end - - it 'does not persist retry count in the database' do - expect(subject[:options]).not_to have_key(:retry) - end - end - end - - describe 'allow failure entry' do - context 'when job is a manual action' do - context 'when allow_failure is defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - when: 'manual', - allow_failure: false }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - - context 'when allow_failure is not defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - when: 'manual' }) - end - - it 'is allowed to fail' do - expect(subject[:allow_failure]).to be true - end - end - end - - context 'when job is not a manual action' do - context 'when allow_failure is defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - allow_failure: false }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - - context 'when allow_failure is not defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec' }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - end - end - end - - describe '#stage_seeds' do - context 'when no refs policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, - rspec: { stage: 'test', script: 'rspec' }, - spinach: { stage: 'test', script: 'spinach' }) - end - - let(:pipeline) { create(:ci_empty_pipeline) } - - it 'correctly fabricates a stage seeds object' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 2 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.second.stage[:name]).to eq 'deploy' - expect(seeds.first.builds.dig(0, :name)).to eq 'rspec' - expect(seeds.first.builds.dig(1, :name)).to eq 'spinach' - expect(seeds.second.builds.dig(0, :name)).to eq 'production' - end - end - - context 'when refs policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, - spinach: { stage: 'test', script: 'spinach', only: ['tags'] }) - end - - let(:pipeline) do - create(:ci_empty_pipeline, ref: 'feature', tag: true) - end - - it 'returns stage seeds only assigned to master to master' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - - context 'when source policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, - spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }) - end - - let(:pipeline) do - create(:ci_empty_pipeline, source: :schedule) - end - - it 'returns stage seeds only assigned to schedules' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - - context 'when kubernetes policy is specified' do - let(:pipeline) { create(:ci_empty_pipeline) } - - let(:config) do - YAML.dump( - spinach: { stage: 'test', script: 'spinach' }, - production: { - stage: 'deploy', - script: 'cap', - only: { kubernetes: 'active' } - } - ) - end - - context 'when kubernetes is active' do - let(:project) { create(:kubernetes_project) } - let(:pipeline) { create(:ci_empty_pipeline, project: project) } - - it 'returns seeds for kubernetes dependent job' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 2 - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - expect(seeds.second.builds.dig(0, :name)).to eq 'production' - end - end - - context 'when kubernetes is not active' do - it 'does not return seeds for kubernetes dependent job' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - end - end - - describe "#builds_for_stage_and_ref" do - let(:type) { 'test' } - - it "returns builds if no branch specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec" } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - describe 'only' do - it "does not return builds if only has another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "does not return builds if only has regexp with another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["/^deploy$/"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "returns builds if only has specified this branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["master"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "returns builds if only has a list of branches including specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: %w(master deploy) } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "returns builds if only has a branches keyword specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if only has a tags keyword" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["tags"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if only has special keywords specified and source matches" do - possibilities = [{ keyword: 'pushes', source: 'push' }, - { keyword: 'web', source: 'web' }, - { keyword: 'triggers', source: 'trigger' }, - { keyword: 'schedules', source: 'schedule' }, - { keyword: 'api', source: 'api' }, - { keyword: 'external', source: 'external' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) - end - end - - it "does not return builds if only has special keywords specified and source doesn't match" do - possibilities = [{ keyword: 'pushes', source: 'web' }, - { keyword: 'web', source: 'push' }, - { keyword: 'triggers', source: 'schedule' }, - { keyword: 'schedules', source: 'external' }, - { keyword: 'api', source: 'trigger' }, - { keyword: 'external', source: 'api' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) - end - end - - it "returns builds if only has current repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches@path"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if only has different repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns build only for specified type" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: "test", only: %w(master deploy) }, - staging: { script: "deploy", type: "deploy", only: %w(master deploy) }, - production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, 'fork') - - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1) - end - - context 'for invalid value' do - let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'when it is integer' do - let(:only) { 1 } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only has to be either an array of conditions or a hash') - end - end - - context 'when it is an array of integers' do - let(:only) { [1, 1] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only config should be an array of strings or regexps') - end - end - - context 'when it is invalid regex' do - let(:only) { ["/*invalid/"] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only config should be an array of strings or regexps') - end - end - end - end - - describe 'except' do - it "returns builds if except has another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "returns builds if except has regexp with another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["/^deploy$/"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "does not return builds if except has specified this branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["master"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "does not return builds if except has a list of branches including specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: %w(master deploy) } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "does not return builds if except has a branches keyword specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if except has a tags keyword" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["tags"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if except has special keywords specified and source matches" do - possibilities = [{ keyword: 'pushes', source: 'push' }, - { keyword: 'web', source: 'web' }, - { keyword: 'triggers', source: 'trigger' }, - { keyword: 'schedules', source: 'schedule' }, - { keyword: 'api', source: 'api' }, - { keyword: 'external', source: 'external' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) - end - end - - it "returns builds if except has special keywords specified and source doesn't match" do - possibilities = [{ keyword: 'pushes', source: 'web' }, - { keyword: 'web', source: 'push' }, - { keyword: 'triggers', source: 'schedule' }, - { keyword: 'schedules', source: 'external' }, - { keyword: 'api', source: 'trigger' }, - { keyword: 'external', source: 'api' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) - end - end - - it "does not return builds if except has current repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches@path"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if except has different repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "returns build except specified type" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] }, - staging: { script: "deploy", type: "deploy", except: ["master"] }, - production: { script: "deploy", type: "deploy", except: ["master@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, 'fork') - - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0) - end - - context 'for invalid value' do - let(:config) { { rspec: { script: "rspec", except: except } } } - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'when it is integer' do - let(:except) { 1 } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except has to be either an array of conditions or a hash') - end - end - - context 'when it is an array of integers' do - let(:except) { [1, 1] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except config should be an array of strings or regexps') - end - end - - context 'when it is invalid regex' do - let(:except) { ["/*invalid/"] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except config should be an array of strings or regexps') - end - end - end - end - end - - describe "Scripts handling" do - let(:config_data) { YAML.dump(config) } - let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) } - - subject { config_processor.builds_for_stage_and_ref("test", "master").first } - - describe "before_script" do - context "in global context" do - let(:config) do - { - before_script: ["global script"], - test: { script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("global script\nscript") - end - end - - context "overwritten in local context" do - let(:config) do - { - before_script: ["global script"], - test: { before_script: ["local script"], script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("local script\nscript") - end - end - end - - describe "script" do - let(:config) do - { - test: { script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("script") - end - end - - describe "after_script" do - context "in global context" do - let(:config) do - { - after_script: ["after_script"], - test: { script: ["script"] } - } - end - - it "return after_script in options" do - expect(subject[:options][:after_script]).to eq(["after_script"]) - end - end - - context "overwritten in local context" do - let(:config) do - { - after_script: ["local after_script"], - test: { after_script: ["local after_script"], script: ["script"] } - } - end - - it "return after_script in options" do - expect(subject[:options][:after_script]).to eq(["local after_script"]) - end - end - end - end - - 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", 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" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - 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", - environment: nil, - yaml_variables: [] - }) - end - - it "returns image and service when overridden for job" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - 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) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["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"] }, - { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - end - - context "when etended docker configuration is not used" do - it "returns image and service when defined" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql", "docker:dind"], - before_script: ["pwd"], - rspec: { script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }, { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - it "returns image and service when overridden for job" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.5" }, - services: [{ name: "postgresql" }, { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - end - end - - describe 'Variables' do - let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) } - - subject { config_processor.builds.first[:yaml_variables] } - - context 'when global variables are defined' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - let(:config) do - { - variables: variables, - before_script: ['pwd'], - rspec: { script: 'rspec' } - } - end - - it 'returns global variables' do - expect(subject).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when job and global variables are defined' do - let(:global_variables) do - { 'VAR1' => 'global1', 'VAR3' => 'global3' } - end - let(:job_variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - let(:config) do - { - before_script: ['pwd'], - variables: global_variables, - rspec: { script: 'rspec', variables: job_variables } - } - end - - it 'returns all unique variables' do - expect(subject).to contain_exactly( - { key: 'VAR3', value: 'global3', public: true }, - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when job variables are defined' do - let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec', variables: variables } - } - end - - context 'when syntax is correct' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - - it 'returns job variables' do - expect(subject).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when syntax is incorrect' do - context 'when variables defined but invalid' do - let(:variables) do - %w(VAR1 value1 VAR2 value2) - end - - it 'raises error' do - expect { subject } - .to raise_error(GitlabCiYamlProcessor::ValidationError, - /jobs:rspec:variables config should be a hash of key value pairs/) - end - end - - context 'when variables key defined but value not specified' do - let(:variables) do - nil - end - - it 'returns empty array' do - ## - # When variables config is empty, we assume this is a valid - # configuration, see issue #18775 - # - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty - end - end - end - end - - context 'when job variables are not defined' do - let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec' } - } - end - - it 'returns empty array' do - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty - end - end - end - - describe "When" do - %w(on_success on_failure always).each do |when_state| - it "returns #{when_state} when defined" do - config = YAML.dump({ - rspec: { script: "rspec", when: when_state } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - builds = config_processor.builds_for_stage_and_ref("test", "master") - expect(builds.size).to eq(1) - expect(builds.first[:when]).to eq(when_state) - end - end - end - - describe 'cache' do - context 'when cache definition has unknown keys' do - it 'raises relevant validation error' do - config = YAML.dump( - { cache: { untracked: true, invalid: 'key' }, - rspec: { script: 'rspec' } }) - - expect { GitlabCiYamlProcessor.new(config) }.to raise_error( - GitlabCiYamlProcessor::ValidationError, - 'cache config contains unknown keys: invalid' - ) - end - end - - it "returns cache when defined globally" do - config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, - rspec: { - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["logs/", "binaries/"], - untracked: true, - key: 'key', - policy: 'pull-push' - ) - end - - it "returns cache when defined in a job" do - config = YAML.dump({ - rspec: { - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["logs/", "binaries/"], - untracked: true, - key: 'key', - policy: 'pull-push' - ) - end - - it "overwrite cache when defined for a job and globally" do - config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, - rspec: { - script: "rspec", - cache: { paths: ["test/"], untracked: false, key: 'local' } - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["test/"], - untracked: false, - key: 'local', - policy: 'pull-push' - ) - end - end - - describe "Artifacts" do - it "returns artifacts when defined" do - config = YAML.dump({ - image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { - artifacts: { - paths: ["logs/", "binaries/"], - untracked: true, - name: "custom_name", - expire_in: "7d" - }, - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }], - artifacts: { - name: "custom_name", - paths: ["logs/", "binaries/"], - untracked: true, - expire_in: "7d" - } - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - - %w[on_success on_failure always].each do |when_state| - it "returns artifacts for when #{when_state} defined" do - config = YAML.dump({ - rspec: { - script: "rspec", - artifacts: { paths: ["logs/", "binaries/"], when: when_state } - } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - builds = config_processor.builds_for_stage_and_ref("test", "master") - expect(builds.size).to eq(1) - expect(builds.first[:options][:artifacts][:when]).to eq(when_state) - end - end - end - - describe '#environment' do - let(:config) do - { - deploy_to_production: { stage: 'deploy', script: 'test', environment: environment } - } - end - - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') } - - context 'when a production environment is specified' do - let(:environment) { 'production' } - - it 'does return production' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment) - expect(builds.first[:options]).to include(environment: { name: environment, action: "start" }) - end - end - - context 'when hash is specified' do - let(:environment) do - { name: 'production', - url: 'http://production.gitlab.com' } - end - - it 'does return production and URL' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment[:name]) - expect(builds.first[:options]).to include(environment: environment) - end - - context 'the url has a port as variable' do - let(:environment) do - { name: 'production', - url: 'http://production.gitlab.com:$PORT' } - end - - it 'allows a variable for the port' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment[:name]) - expect(builds.first[:options]).to include(environment: environment) - end - end - end - - context 'when no environment is specified' do - let(:environment) { nil } - - it 'does return nil environment' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to be_nil - end - end - - context 'is not a string' do - let(:environment) { 1 } - - it 'raises error' do - expect { builds }.to raise_error( - 'jobs:deploy_to_production:environment config should be a hash or a string') - end - end - - context 'is not a valid string' do - let(:environment) { 'production:staging' } - - it 'raises error' do - expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") - end - end - - context 'when on_stop is specified' do - let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } } - let(:config) { { review: review, close_review: close_review }.compact } - - context 'with matching job' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } } - - it 'does return a list of builds' do - expect(builds.size).to eq(2) - expect(builds.first[:environment]).to eq('review') - end - end - - context 'without matching job' do - let(:close_review) { nil } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review is not defined') - end - end - - context 'with close job without environment' do - let(:close_review) { { stage: 'deploy', script: 'test' } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined') - end - end - - context 'with close job for different environment' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review have different environment name') - end - end - - context 'with close job without stop action' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined') - end - end - end - end - - describe "Dependencies" do - let(:config) do - { - build1: { stage: 'build', script: 'test' }, - build2: { stage: 'build', script: 'test' }, - test1: { stage: 'test', script: 'test', dependencies: dependencies }, - test2: { stage: 'test', script: 'test' }, - deploy: { stage: 'test', script: 'test' } - } - end - - subject { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'no dependencies' do - let(:dependencies) { } - - it { expect { subject }.not_to raise_error } - end - - context 'dependencies to builds' do - let(:dependencies) { %w(build1 build2) } - - it { expect { subject }.not_to raise_error } - end - - context 'dependencies to builds defined as symbols' do - let(:dependencies) { [:build1, :build2] } - - it { expect { subject }.not_to raise_error } - end - - context 'undefined dependency' do - let(:dependencies) { ['undefined'] } - - it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') } - end - - context 'dependencies to deploy' do - let(:dependencies) { ['deploy'] } - - it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') } - end - end - - describe "Hidden jobs" do - let(:config_processor) { GitlabCiYamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("test", "master") } - - shared_examples 'hidden_job_handling' do - it "doesn't create jobs that start with dot" do - expect(subject.size).to eq(1) - expect(subject.first).to eq({ - stage: "test", - stage_idx: 1, - name: "normal_job", - commands: "test", - coverage_regex: nil, - tag_list: [], - options: { - script: ["test"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - end - - context 'when hidden job have a script definition' do - let(:config) do - YAML.dump({ - '.hidden_job' => { image: 'ruby:2.1', script: 'test' }, - 'normal_job' => { script: 'test' } - }) - end - - it_behaves_like 'hidden_job_handling' - end - - context "when hidden job doesn't have a script definition" do - let(:config) do - YAML.dump({ - '.hidden_job' => { image: 'ruby:2.1' }, - 'normal_job' => { script: 'test' } - }) - end - - it_behaves_like 'hidden_job_handling' - end - end - - describe "YAML Alias/Anchor" do - let(:config_processor) { GitlabCiYamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("build", "master") } - - shared_examples 'job_templates_handling' do - it "is correctly supported for jobs" do - expect(subject.size).to eq(2) - expect(subject.first).to eq({ - stage: "build", - stage_idx: 0, - name: "job1", - commands: "execute-script-for-job", - coverage_regex: nil, - tag_list: [], - options: { - script: ["execute-script-for-job"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - expect(subject.second).to eq({ - stage: "build", - stage_idx: 0, - name: "job2", - commands: "execute-script-for-job", - coverage_regex: nil, - tag_list: [], - options: { - script: ["execute-script-for-job"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - end - - context 'when template is a job' do - let(:config) do - <