From 8aa757aa532c14445aca8731b7a553e657a58593 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Feb 2017 17:12:18 +0000 Subject: [PATCH 01/95] Formats timeago dates to be more friendly Formats the timeago timestamps to be a short date. This will only be visible on slower connections whilst the JS is loading, after the JS has loaded it will be turned into a timeago string Closes #27537 --- app/helpers/application_helper.rb | 2 +- changelogs/unreleased/format-timeago-date.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/format-timeago-date.yml diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bee323993a0..724641fd9d8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -166,7 +166,7 @@ module ApplicationHelper css_classes = short_format ? 'js-short-timeago' : 'js-timeago' css_classes << " #{html_class}" unless html_class.blank? - element = content_tag :time, time.to_s, + element = content_tag :time, time.strftime("%b %d, %Y"), class: css_classes, title: time.to_time.in_time_zone.to_s(:medium), datetime: time.to_time.getutc.iso8601, diff --git a/changelogs/unreleased/format-timeago-date.yml b/changelogs/unreleased/format-timeago-date.yml new file mode 100644 index 00000000000..f331c34abbc --- /dev/null +++ b/changelogs/unreleased/format-timeago-date.yml @@ -0,0 +1,4 @@ +--- +title: Format timeago date to short format +merge_request: +author: From a9c80dceb688623e8f06a925caf629fddfa180ec Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 13 Feb 2017 09:01:09 +0000 Subject: [PATCH 02/95] Fixed timeago specs --- spec/features/commits_spec.rb | 2 +- spec/helpers/application_helper_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 8f561c8f90b..dede562c2b0 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -192,7 +192,7 @@ describe 'Commits' do commits = project.repository.commits(branch_name) commits.each do |commit| - expect(page).to have_content("committed #{commit.committed_date}") + expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}") end end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 8b201f348f1..9749936a2a0 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -193,8 +193,8 @@ describe ApplicationHelper do describe 'time_ago_with_tooltip' do def element(*arguments) Time.zone = 'UTC' - time = Time.zone.parse('2015-07-02 08:23') - element = helper.time_ago_with_tooltip(time, *arguments) + @time = Time.zone.parse('2015-07-02 08:23') + element = helper.time_ago_with_tooltip(@time, *arguments) Nokogiri::HTML::DocumentFragment.parse(element).first_element_child end @@ -204,7 +204,7 @@ describe ApplicationHelper do end it 'includes the date string' do - expect(element.text).to eq '2015-07-02 08:23:00 UTC' + expect(element.text).to eq @time.strftime("%b %d, %Y") end it 'has a datetime attribute' do From 1ca5ab704eb086b6b09caf8abaabb373c58dbf09 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Thu, 23 Feb 2017 17:56:36 +0000 Subject: [PATCH 03/95] add Microsoft Exchange reply by email details fix #28131 [skip ci] --- doc/administration/reply_by_email.md | 56 ++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md index 00494e7e9d6..85b60f09b58 100644 --- a/doc/administration/reply_by_email.md +++ b/doc/administration/reply_by_email.md @@ -13,7 +13,8 @@ three strategies for this feature: ### Email sub-addressing -**If your provider or server supports email sub-addressing, we recommend using it.** +**If your provider or server supports email sub-addressing, we recommend using it. +Some features (e.g. create new issue via email) only work with sub-addressing.** [Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is a feature where any email to `user+some_arbitrary_tag@example.com` will end up @@ -138,12 +139,32 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the # The IDLE command timeout. gitlab_rails['incoming_email_idle_timeout'] = 60 ``` + + ```ruby + # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com + gitlab_rails['incoming_email_enabled'] = true -1. Reconfigure GitLab and restart mailroom for the changes to take effect: + # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here + gitlab_rails['incoming_email_address'] = "incoming@exchange.example.com" + + # Email account username + # Typically this is the userPrincipalName (UPN) + gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com" + # Email account password + gitlab_rails['incoming_email_password'] = "[REDACTED]" + + # IMAP server host + gitlab_rails['incoming_email_host'] = "exchange.example.com" + # IMAP server port + gitlab_rails['incoming_email_port'] = 993 + # Whether the IMAP server uses SSL + gitlab_rails['incoming_email_ssl'] = true + ``` + +1. Reconfigure GitLab for the changes to take effect: ```sh sudo gitlab-ctl reconfigure - sudo gitlab-ctl restart mailroom ``` 1. Verify that everything is configured correctly: @@ -230,6 +251,35 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the # The IDLE command timeout. idle_timeout: 60 ``` + + ```yaml + # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com + incoming_email: + enabled: true + + # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here + address: "incoming@exchange.example.com" + + # Email account username + # Typically this is the userPrincipalName (UPN) + user: "incoming@ad-domain.example.com" + # Email account password + password: "[REDACTED]" + + # IMAP server host + host: "exchange.example.com" + # IMAP server port + port: 993 + # Whether the IMAP server uses SSL + ssl: true + # Whether the IMAP server uses StartTLS + start_tls: false + + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" + # The IDLE command timeout. + idle_timeout: 60 + ``` 1. Enable `mail_room` in the init script at `/etc/default/gitlab`: From 40c9a948b068e84873c8369d16d328790cab1cda Mon Sep 17 00:00:00 2001 From: Georg G Date: Sun, 26 Feb 2017 20:14:02 +0100 Subject: [PATCH 04/95] Change language for testing colour fallback --- spec/controllers/projects/graphs_controller_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb index c4a7aa7d63e..203b20667bf 100644 --- a/spec/controllers/projects/graphs_controller_spec.rb +++ b/spec/controllers/projects/graphs_controller_spec.rb @@ -14,18 +14,18 @@ describe Projects::GraphsController do double(languages: { 'Ruby' => 1000, 'CoffeeScript' => 350, - 'PowerShell' => 15 + 'NSIS' => 15 }) end let(:expected_values) do - ps_color = "##{Digest::SHA256.hexdigest('PowerShell')[0...6]}" + nsis_color = "##{Digest::SHA256.hexdigest('NSIS')[0...6]}" [ # colors from Linguist: - { label: "Ruby", color: "#701516", highlight: "#701516" }, - { label: "CoffeeScript", color: "#244776", highlight: "#244776" }, + { label: "Ruby", color: "#701516", highlight: "#701516" }, + { label: "CoffeeScript", color: "#244776", highlight: "#244776" }, # colors from SHA256 fallback: - { label: "PowerShell", color: ps_color, highlight: ps_color } + { label: "NSIS", color: nsis_color, highlight: nsis_color } ] end From 0a31efb57768345e2b3350a493021a26b54994a3 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Tue, 7 Feb 2017 18:02:02 +0100 Subject: [PATCH 05/95] Remove query parameters from notes polling endpoint to make caching easier --- app/assets/javascripts/notes.js | 2 +- app/controllers/projects/notes_controller.rb | 7 ++++- .../projects/notes/_notes_with_form.html.haml | 2 +- config/routes/project.rb | 4 ++- .../projects/notes_controller_spec.rb | 27 +++++++++++++++++++ spec/routing/project_routing_spec.rb | 18 ++++++++++--- 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 47fa0f2eb96..df7a7d2a459 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -198,7 +198,7 @@ require('./task_list'); this.refreshing = true; return $.ajax({ url: this.notes_url, - data: "last_fetched_at=" + this.last_fetched_at, + headers: { "X-Last-Fetched-At": this.last_fetched_at }, dataType: "json", success: (function(_this) { return function(data) { diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 5cf3a7f593b..d00177e7612 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -211,6 +211,11 @@ class Projects::NotesController < Projects::ApplicationController end def find_current_user_notes - @notes = NotesFinder.new(project, current_user, params).execute.inc_author + @notes = NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at)) + .execute.inc_author + end + + def last_fetched_at + request.headers['X-Last-Fetched-At'] end end diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 08c73d94a09..90a150aa74c 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -23,4 +23,4 @@ to post a comment :javascript - var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") + var notes = new Notes("#{namespace_project_noteable_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/config/routes/project.rb b/config/routes/project.rb index 84f123ff717..a9f95c09bef 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -265,7 +265,7 @@ constraints(ProjectUrlConstrainer.new) do resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ } - resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do + resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do member do delete :delete_attachment post :resolve @@ -273,6 +273,8 @@ constraints(ProjectUrlConstrainer.new) do end end + get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes' + resources :boards, only: [:index, :show] do scope module: :boards do resources :issues, only: [:index, :update] diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index dc597202050..d80780b1d90 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -200,4 +200,31 @@ describe Projects::NotesController do end end end + + describe 'GET index' do + let(:last_fetched_at) { '1487756246' } + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + target_type: 'issue', + target_id: issue.id + } + end + + before do + sign_in(user) + project.team << [user, :developer] + end + + it 'passes last_fetched_at from headers to NotesFinder' do + request.headers['X-Last-Fetched-At'] = last_fetched_at + + expect(NotesFinder).to receive(:new) + .with(anything, anything, hash_including(last_fetched_at: last_fetched_at)) + .and_call_original + + get :index, request_params + end + end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index a5bc62ef6c2..d31f1bdfb7c 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -431,12 +431,22 @@ describe 'project routing' do end end - # project_notes GET /:project_id/notes(.:format) notes#index - # POST /:project_id/notes(.:format) notes#create - # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy + # project_noteable_notes GET /:project_id/noteable/:target_type/:target_id/notes notes#index + # POST /:project_id/notes(.:format) notes#create + # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy describe Projects::NotesController, 'routing' do + it 'to #index' do + expect(get('/gitlab/gitlabhq/noteable/issue/1/notes')).to route_to( + 'projects/notes#index', + namespace_id: 'gitlab', + project_id: 'gitlabhq', + target_type: 'issue', + target_id: '1' + ) + end + it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :create, :destroy] } + let(:actions) { [:create, :destroy] } let(:controller) { 'notes' } end end From 61c9604721dbda53eb4a7111d16c1b19292f9766 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Tue, 7 Feb 2017 18:06:08 +0100 Subject: [PATCH 06/95] Add middleware for ETag caching with Redis --- changelogs/unreleased/etag-notes-polling.yml | 4 + config/initializers/etag_caching.rb | 4 + lib/gitlab/etag_caching/middleware.rb | 66 +++++++ lib/gitlab/etag_caching/store.rb | 32 ++++ .../gitlab/etag_caching/middleware_spec.rb | 163 ++++++++++++++++++ 5 files changed, 269 insertions(+) create mode 100644 changelogs/unreleased/etag-notes-polling.yml create mode 100644 config/initializers/etag_caching.rb create mode 100644 lib/gitlab/etag_caching/middleware.rb create mode 100644 lib/gitlab/etag_caching/store.rb create mode 100644 spec/lib/gitlab/etag_caching/middleware_spec.rb diff --git a/changelogs/unreleased/etag-notes-polling.yml b/changelogs/unreleased/etag-notes-polling.yml new file mode 100644 index 00000000000..53990821d25 --- /dev/null +++ b/changelogs/unreleased/etag-notes-polling.yml @@ -0,0 +1,4 @@ +--- +title: Use ETag to improve performance of issue notes polling +merge_request: 9036 +author: diff --git a/config/initializers/etag_caching.rb b/config/initializers/etag_caching.rb new file mode 100644 index 00000000000..eba88801141 --- /dev/null +++ b/config/initializers/etag_caching.rb @@ -0,0 +1,4 @@ +# This middleware has to come after Gitlab::Metrics::RackMiddleware +# in the middleware stack, because it tracks events with +# GitLab Performance Monitoring +Rails.application.config.middleware.use(Gitlab::EtagCaching::Middleware) diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb new file mode 100644 index 00000000000..0f24f9bbfde --- /dev/null +++ b/lib/gitlab/etag_caching/middleware.rb @@ -0,0 +1,66 @@ +module Gitlab + module EtagCaching + class Middleware + RESERVED_WORDS = ProjectPathValidator::RESERVED.map { |word| "/#{word}/" }.join('|') + ROUTE_REGEXP = Regexp.union( + %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z) + ) + + def initialize(app) + @app = app + end + + def call(env) + return @app.call(env) unless enabled_for_current_route?(env) + Gitlab::Metrics.add_event(:etag_caching_middleware_used) + + etag, cached_value_present = get_etag(env) + if_none_match = env['HTTP_IF_NONE_MATCH'] + + if if_none_match == etag + Gitlab::Metrics.add_event(:etag_caching_cache_hit) + [304, { 'ETag' => etag }, ['']] + else + track_cache_miss(if_none_match, cached_value_present) + + status, headers, body = @app.call(env) + headers['ETag'] = etag + [status, headers, body] + end + end + + private + + def enabled_for_current_route?(env) + ROUTE_REGEXP.match(env['PATH_INFO']) + end + + def get_etag(env) + cache_key = env['PATH_INFO'] + store = Store.new + current_value = store.get(cache_key) + cached_value_present = current_value.present? + + unless cached_value_present + current_value = store.touch(cache_key, only_if_missing: true) + end + + [weak_etag_format(current_value), cached_value_present] + end + + def weak_etag_format(value) + %Q{W/"#{value}"} + end + + def track_cache_miss(if_none_match, cached_value_present) + if if_none_match.blank? + Gitlab::Metrics.add_event(:etag_caching_header_missing) + elsif !cached_value_present + Gitlab::Metrics.add_event(:etag_caching_key_not_found) + else + Gitlab::Metrics.add_event(:etag_caching_resource_changed) + end + end + end + end +end diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb new file mode 100644 index 00000000000..9532e432f78 --- /dev/null +++ b/lib/gitlab/etag_caching/store.rb @@ -0,0 +1,32 @@ +module Gitlab + module EtagCaching + class Store + EXPIRY_TIME = 10.minutes + REDIS_NAMESPACE = 'etag:'.freeze + + def get(key) + Gitlab::Redis.with { |redis| redis.get(redis_key(key)) } + end + + def touch(key, only_if_missing: false) + etag = generate_etag + + Gitlab::Redis.with do |redis| + redis.set(redis_key(key), etag, ex: EXPIRY_TIME, nx: only_if_missing) + end + + etag + end + + private + + def generate_etag + SecureRandom.hex + end + + def redis_key(key) + "#{REDIS_NAMESPACE}#{key}" + end + end + end +end diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb new file mode 100644 index 00000000000..8b5bfc4dbb0 --- /dev/null +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -0,0 +1,163 @@ +require 'spec_helper' + +describe Gitlab::EtagCaching::Middleware do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + let(:app_status_code) { 200 } + let(:if_none_match) { nil } + let(:enabled_path) { '/gitlab-org/gitlab-ce/noteable/issue/1/notes' } + + context 'when ETag caching is not enabled for current route' do + let(:path) { '/gitlab-org/gitlab-ce/tree/master/noteable/issue/1/notes' } + + before do + mock_app_response + end + + it 'does not add ETag header' do + _, headers, _ = middleware.call(build_env(path, if_none_match)) + + expect(headers['ETag']).to be_nil + end + + it 'passes status code from app' do + status, _, _ = middleware.call(build_env(path, if_none_match)) + + expect(status).to eq app_status_code + end + end + + context 'when there is no ETag in store for given resource' do + let(:path) { enabled_path } + + before do + mock_app_response + mock_value_in_store(nil) + end + + it 'generates ETag' do + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:touch).and_return('123') + + middleware.call(build_env(path, if_none_match)) + end + + context 'when If-None-Match header was specified' do + let(:if_none_match) { 'W/"abc"' } + + it 'tracks "etag_caching_key_not_found" event' do + expect(Gitlab::Metrics).to receive(:add_event) + .with(:etag_caching_middleware_used) + expect(Gitlab::Metrics).to receive(:add_event) + .with(:etag_caching_key_not_found) + + middleware.call(build_env(path, if_none_match)) + end + end + end + + context 'when there is ETag in store for given resource' do + let(:path) { enabled_path } + + before do + mock_app_response + mock_value_in_store('123') + end + + it 'returns this value as header' do + _, headers, _ = middleware.call(build_env(path, if_none_match)) + + expect(headers['ETag']).to eq 'W/"123"' + end + end + + context 'when If-None-Match header matches ETag in store' do + let(:path) { enabled_path } + let(:if_none_match) { 'W/"123"' } + + before do + mock_value_in_store('123') + end + + it 'does not call app' do + expect(app).not_to receive(:call) + + middleware.call(build_env(path, if_none_match)) + end + + it 'returns status code 304' do + status, _, _ = middleware.call(build_env(path, if_none_match)) + + expect(status).to eq 304 + end + + it 'tracks "etag_caching_cache_hit" event' do + expect(Gitlab::Metrics).to receive(:add_event) + .with(:etag_caching_middleware_used) + expect(Gitlab::Metrics).to receive(:add_event) + .with(:etag_caching_cache_hit) + + middleware.call(build_env(path, if_none_match)) + end + end + + context 'when If-None-Match header does not match ETag in store' do + let(:path) { enabled_path } + let(:if_none_match) { 'W/"abc"' } + + before do + mock_value_in_store('123') + end + + it 'calls app' do + expect(app).to receive(:call).and_return([app_status_code, {}, ['body']]) + + middleware.call(build_env(path, if_none_match)) + end + + it 'tracks "etag_caching_resource_changed" event' do + mock_app_response + + expect(Gitlab::Metrics).to receive(:add_event) + .with(:etag_caching_middleware_used) + expect(Gitlab::Metrics).to receive(:add_event) + .with(:etag_caching_resource_changed) + + middleware.call(build_env(path, if_none_match)) + end + end + + context 'when If-None-Match header is not specified' do + let(:path) { enabled_path } + + before do + mock_value_in_store('123') + mock_app_response + end + + it 'tracks "etag_caching_header_missing" event' do + expect(Gitlab::Metrics).to receive(:add_event) + .with(:etag_caching_middleware_used) + expect(Gitlab::Metrics).to receive(:add_event) + .with(:etag_caching_header_missing) + + middleware.call(build_env(path, if_none_match)) + end + end + + def mock_app_response + allow(app).to receive(:call).and_return([app_status_code, {}, ['body']]) + end + + def mock_value_in_store(value) + allow_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:get).and_return(value) + end + + def build_env(path, if_none_match) + { + 'PATH_INFO' => path, + 'HTTP_IF_NONE_MATCH' => if_none_match + } + end +end From c661df356156240deeef7c2734670ba5f2b4b04b Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Wed, 8 Feb 2017 18:04:16 +0100 Subject: [PATCH 07/95] Invalidate ETag cache when note changes --- app/models/note.rb | 13 +++++++++++++ spec/models/note_spec.rb | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/models/note.rb b/app/models/note.rb index 4c97e4a986c..e22e96aec6f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -85,6 +85,7 @@ class Note < ActiveRecord::Base before_validation :nullify_blank_type, :nullify_blank_line_code before_validation :set_discussion_id after_save :keep_around_commit, unless: :for_personal_snippet? + after_save :expire_etag_cache class << self def model_name @@ -272,4 +273,16 @@ class Note < ActiveRecord::Base self.class.build_discussion_id(noteable_type, noteable_id || commit_id) end end + + def expire_etag_cache + return unless for_issue? + + key = Gitlab::Routing.url_helpers.namespace_project_noteable_notes_path( + noteable.project.namespace, + noteable.project, + target_type: noteable_type.underscore, + target_id: noteable.id + ) + Gitlab::EtagCaching::Store.new.touch(key) + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 1cde9e04951..33536487c41 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -387,4 +387,16 @@ describe Note, models: true do end end end + + describe 'expiring ETag cache' do + let(:note) { build(:note_on_issue) } + + it "expires cache for note's issue when note is saved" do + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:touch) + .with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes") + + note.save! + end + end end From ee318727774dbec75d5506a4b4749fc4236206d5 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Wed, 1 Mar 2017 18:15:28 +0100 Subject: [PATCH 08/95] Execute metrics initializer earlier This makes sure that Gitlab::Metrics::RackMiddleware is added before Gitlab::EtagCaching::Middleware. --- config/initializers/{metrics.rb => 8_metrics.rb} | 0 doc/development/instrumentation.md | 2 +- spec/initializers/{metrics_spec.rb => 8_metrics_spec.rb} | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename config/initializers/{metrics.rb => 8_metrics.rb} (100%) rename spec/initializers/{metrics_spec.rb => 8_metrics_spec.rb} (87%) diff --git a/config/initializers/metrics.rb b/config/initializers/8_metrics.rb similarity index 100% rename from config/initializers/metrics.rb rename to config/initializers/8_metrics.rb diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md index b8669964c84..a14c0752366 100644 --- a/doc/development/instrumentation.md +++ b/doc/development/instrumentation.md @@ -35,7 +35,7 @@ Using this method is in general preferred over directly calling the various instrumentation methods. Method instrumentation should be added in the initializer -`config/initializers/metrics.rb`. +`config/initializers/8_metrics.rb`. ### Examples diff --git a/spec/initializers/metrics_spec.rb b/spec/initializers/8_metrics_spec.rb similarity index 87% rename from spec/initializers/metrics_spec.rb rename to spec/initializers/8_metrics_spec.rb index bb595162370..570754621f3 100644 --- a/spec/initializers/metrics_spec.rb +++ b/spec/initializers/8_metrics_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -require_relative '../../config/initializers/metrics' +require_relative '../../config/initializers/8_metrics' describe 'instrument_classes', lib: true do let(:config) { double(:config) } From 4c3412c37586b624585732326714a605bb226ba9 Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Tue, 28 Feb 2017 22:44:57 +0530 Subject: [PATCH 09/95] Update profiles/account view to display new username --- app/controllers/profiles_controller.rb | 11 +++++++---- app/views/profiles/accounts/show.html.haml | 2 +- app/views/profiles/update_username.js.haml | 7 ------- ...-not-updated-after-setting-the-new-username.yml | 4 ++++ spec/features/profile_spec.rb | 14 ++++++++++++++ 5 files changed, 26 insertions(+), 12 deletions(-) delete mode 100644 app/views/profiles/update_username.js.haml create mode 100644 changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index f0c71725ea8..987b95e89b9 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -47,11 +47,14 @@ class ProfilesController < Profiles::ApplicationController end def update_username - @user.update_attributes(username: user_params[:username]) - - respond_to do |format| - format.js + if @user.update_attributes(username: user_params[:username]) + options = { notice: "Username successfully changed" } + else + message = @user.errors.full_messages.uniq.join('. ') + options = { alert: "Username change failed - #{message}" } end + + redirect_back_or_default(default: { action: 'show' }, options: options) end private diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 02fb47ec981..8a994f6d600 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -93,7 +93,7 @@ %p Changing your username will change path to all personal projects! .col-lg-9 - = form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f| + = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f| .form-group = f.label :username, "Path", class: "label-light" .input-group diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml deleted file mode 100644 index 5307e0b48cb..00000000000 --- a/app/views/profiles/update_username.js.haml +++ /dev/null @@ -1,7 +0,0 @@ -- if @user.valid? - :plain - new Flash("Username successfully changed", "notice") -- else - - error = @user.errors.full_messages.first - :plain - new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert") diff --git a/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml b/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml new file mode 100644 index 00000000000..bff996172f3 --- /dev/null +++ b/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml @@ -0,0 +1,4 @@ +--- +title: Update account view to display new username +merge_request: +author: diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 406d7cf791c..e63feb14b7e 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -61,4 +61,18 @@ describe 'Profile account page', feature: true do expect(find('#incoming-email-token').value).not_to eq(previous_token) end end + + describe 'when I change my username' do + before do + visit profile_account_path + end + + it 'changes my username' do + fill_in 'user_username', with: 'new-username' + + click_button('Update username') + + expect(page).to have_content('new-username') + end + end end From 86fe7d6aeceae5f0472adea1c2829aa45757c786 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 2 Mar 2017 10:43:48 +0000 Subject: [PATCH 10/95] Fixed spacing in mobile secondary nav Previously the spacing was spread evenly across the width which lead to some weird spacing issues, more noticeable between the user avatar & the dropdown caret --- app/assets/stylesheets/framework/header.scss | 11 +++-------- app/views/layouts/header/_default.html.haml | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 685a4847731..99651559d0d 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -261,20 +261,15 @@ header { .navbar-nav { margin: 0; - float: none !important; - - .visible-xs, - .visible-sm { - display: table-cell !important; - } + padding-right: 10px; + text-align: right; } .navbar-collapse { padding-left: 5px; .nav > li { - display: table-cell; - width: 1%; + display: inline-block; } } } diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 555ec8ad079..6f4f2dbea3a 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -19,7 +19,7 @@ %ul.nav.navbar-nav %li.hidden-sm.hidden-xs = render 'layouts/search' unless current_controller?(:search) - %li.visible-sm.visible-xs + %li.visible-sm-inline-block.visible-xs-inline-block = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') - if current_user From 53760bb836766973d3a3eb68d12782858b35adf3 Mon Sep 17 00:00:00 2001 From: Tiago Botelho Date: Wed, 15 Feb 2017 21:14:17 +0000 Subject: [PATCH 11/95] removes redundant code from gitlab database file --- lib/gitlab/database.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index d160cadc2d0..f3f417c1a63 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -24,7 +24,7 @@ module Gitlab def self.nulls_last_order(field, direction = 'ASC') order = "#{field} #{direction}" - if Gitlab::Database.postgresql? + if postgresql? order << ' NULLS LAST' else # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL @@ -38,7 +38,7 @@ module Gitlab def self.nulls_first_order(field, direction = 'ASC') order = "#{field} #{direction}" - if Gitlab::Database.postgresql? + if postgresql? order << ' NULLS FIRST' else # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL @@ -50,7 +50,7 @@ module Gitlab end def self.random - Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()" + postgresql? ? "RANDOM()" : "RAND()" end def true_value From b3971d0f213d1fe8129111009ec69076f7fb4233 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 28 Feb 2017 17:08:46 +0200 Subject: [PATCH 12/95] Change default project view for user from readme to files view Signed-off-by: Dmitriy Zaporozhets --- app/helpers/preferences_helper.rb | 4 ++-- app/models/user.rb | 1 + changelogs/unreleased/dz-change-project-view.yml | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/dz-change-project-view.yml diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index c3a08d76318..74cccb23956 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -35,9 +35,9 @@ module PreferencesHelper def project_view_choices [ - ['Readme (default)', :readme], + ['Readme', :readme], ['Activity view', :activity], - ['Files view', :files] + ['Files and Readme (default)', :files] ] end diff --git a/app/models/user.rb b/app/models/user.rb index 8443594c055..d3bb04060bf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,6 +21,7 @@ class User < ActiveRecord::Base default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false default_value_for :hide_no_password, false + default_value_for :project_view, :files attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, diff --git a/changelogs/unreleased/dz-change-project-view.yml b/changelogs/unreleased/dz-change-project-view.yml new file mode 100644 index 00000000000..47e007a80a8 --- /dev/null +++ b/changelogs/unreleased/dz-change-project-view.yml @@ -0,0 +1,4 @@ +--- +title: Change default project view for user from readme to files view +merge_request: 9584 +author: From 59e7d04bc78a69137b649dc8ac470a6c3eedbd3c Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Mon, 30 Jan 2017 12:11:58 +0100 Subject: [PATCH 13/95] Expose pipelines as PipelineBasic `projects/:id/pipelines` The `projects/:id/pipelines` exposed a lot of extra details that are superfluous and it was taking extra resources to fetch them. To get more details about a pipeline, use `projects/:id/pipelines/:pipeline_id`. --- .../26847-api-pipelines-use-basic.yml | 4 + doc/api/pipelines.md | 40 +--- doc/api/v3_to_v4.md | 1 + lib/api/api.rb | 1 + lib/api/pipelines.rb | 4 +- lib/api/v3/pipelines.rb | 36 ++++ spec/requests/api/pipelines_spec.rb | 1 + spec/requests/api/v3/pipelines_spec.rb | 203 ++++++++++++++++++ 8 files changed, 250 insertions(+), 40 deletions(-) create mode 100644 changelogs/unreleased/26847-api-pipelines-use-basic.yml create mode 100644 lib/api/v3/pipelines.rb create mode 100644 spec/requests/api/v3/pipelines_spec.rb diff --git a/changelogs/unreleased/26847-api-pipelines-use-basic.yml b/changelogs/unreleased/26847-api-pipelines-use-basic.yml new file mode 100644 index 00000000000..2034a4ba080 --- /dev/null +++ b/changelogs/unreleased/26847-api-pipelines-use-basic.yml @@ -0,0 +1,4 @@ +--- +title: Expose pipelines as PipelineBasic `api/v3/projects/:id/pipelines` +merge_request: 8875 +author: diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 9d6f3ea41d9..d809ec032f8 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -24,49 +24,13 @@ Example of response "id": 47, "status": "pending", "ref": "new-pipeline", - "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", - "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", - "tag": false, - "yaml_errors": null, - "user": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/root" - }, - "created_at": "2016-08-16T10:23:19.007Z", - "updated_at": "2016-08-16T10:23:19.216Z", - "started_at": null, - "finished_at": null, - "committed_at": null, - "duration": null, - "coverage": "30.0" + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a" }, { "id": 48, "status": "pending", "ref": "new-pipeline", - "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a", - "before_sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a", - "tag": false, - "yaml_errors": null, - "user": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/root" - }, - "created_at": "2016-08-16T10:23:21.184Z", - "updated_at": "2016-08-16T10:23:21.314Z", - "started_at": null, - "finished_at": null, - "committed_at": null, - "duration": null, - "coverage": null + "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a" } ] ``` diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index cca58894476..ba5b7e09102 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -53,3 +53,4 @@ changes are in V4: - Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505) - Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) - `projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096) +- Return basic info about pipeline in `GET /projects/:id/pipelines` [!8875](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8875) diff --git a/lib/api/api.rb b/lib/api/api.rb index 3e53ab693ab..89449ce8813 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -20,6 +20,7 @@ module API mount ::API::V3::MergeRequestDiffs mount ::API::V3::MergeRequests mount ::API::V3::Notes + mount ::API::V3::Pipelines mount ::API::V3::ProjectHooks mount ::API::V3::Milestones mount ::API::V3::Projects diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index 3afc1e385fe..0721b975ba4 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -10,7 +10,7 @@ module API resource :projects do desc 'Get all Pipelines of the project' do detail 'This feature was introduced in GitLab 8.11.' - success Entities::Pipeline + success Entities::PipelineBasic end params do use :pagination @@ -21,7 +21,7 @@ module API authorize! :read_pipeline, user_project pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope]) - present paginate(pipelines), with: Entities::Pipeline + present paginate(pipelines), with: Entities::PipelineBasic end desc 'Create a new pipeline' do diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb new file mode 100644 index 00000000000..2c26a5f7d35 --- /dev/null +++ b/lib/api/v3/pipelines.rb @@ -0,0 +1,36 @@ +module API + module V3 + class Pipelines < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The project ID' + end + resource :projects do + desc 'Get all Pipelines of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success ::API::Entities::Pipeline + end + params do + use :pagination + optional :scope, type: String, values: %w(running branches tags), + desc: 'Either running, branches, or tags' + end + get ':id/pipelines' do + authorize! :read_pipeline, user_project + + pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope]) + present paginate(pipelines), with: ::API::Entities::Pipeline + end + end + + helpers do + def pipeline + @pipeline ||= user_project.pipelines.find(params[:pipeline_id]) + end + end + end + end +end diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 98d004b572e..51af999b455 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -24,6 +24,7 @@ describe API::Pipelines, api: true do expect(json_response).to be_an Array expect(json_response.first['sha']).to match /\A\h{40}\z/ expect(json_response.first['id']).to eq pipeline.id + expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status]) end end diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb new file mode 100644 index 00000000000..3786eb06932 --- /dev/null +++ b/spec/requests/api/v3/pipelines_spec.rb @@ -0,0 +1,203 @@ +require 'spec_helper' + +describe API::V3::Pipelines, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { create(:project, :repository, creator: user) } + + let!(:pipeline) do + create(:ci_empty_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) + end + + before { project.team << [user, :master] } + + shared_examples 'a paginated resources' do + before do + # Fires the request + request + end + + it 'has pagination headers' do + expect(response).to include_pagination_headers + end + end + + describe 'GET /projects/:id/pipelines ' do + it_behaves_like 'a paginated resources' do + let(:request) { get v3_api("/projects/#{project.id}/pipelines", user) } + end + + context 'authorized user' do + it 'returns project pipelines' do + get v3_api("/projects/#{project.id}/pipelines", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['sha']).to match(/\A\h{40}\z/) + expect(json_response.first['id']).to eq pipeline.id + expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status before_sha tag yaml_errors user created_at updated_at started_at finished_at committed_at duration coverage]) + end + end + + context 'unauthorized user' do + it 'does not return project pipelines' do + get v3_api("/projects/#{project.id}/pipelines", non_member) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response).not_to be_an Array + end + end + end + + describe 'POST /projects/:id/pipeline ' do + context 'authorized user' do + context 'with gitlab-ci.yml' do + before { stub_ci_pipeline_to_return_yaml_file } + + it 'creates and returns a new pipeline' do + expect do + post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch + end.to change { Ci::Pipeline.count }.by(1) + + expect(response).to have_http_status(201) + expect(json_response).to be_a Hash + expect(json_response['sha']).to eq project.commit.id + end + + it 'fails when using an invalid ref' do + post v3_api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref' + + expect(response).to have_http_status(400) + expect(json_response['message']['base'].first).to eq 'Reference not found' + expect(json_response).not_to be_an Array + end + end + + context 'without gitlab-ci.yml' do + it 'fails to create pipeline' do + post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch + + expect(response).to have_http_status(400) + expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file' + expect(json_response).not_to be_an Array + end + end + end + + context 'unauthorized user' do + it 'does not create pipeline' do + post v3_api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response).not_to be_an Array + end + end + end + + describe 'GET /projects/:id/pipelines/:pipeline_id' do + context 'authorized user' do + it 'returns project pipelines' do + get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['sha']).to match /\A\h{40}\z/ + end + + it 'returns 404 when it does not exist' do + get v3_api("/projects/#{project.id}/pipelines/123456", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Not found' + expect(json_response['id']).to be nil + end + + context 'with coverage' do + before do + create(:ci_build, coverage: 30, pipeline: pipeline) + end + + it 'exposes the coverage' do + get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(json_response["coverage"].to_i).to eq(30) + end + end + end + + context 'unauthorized user' do + it 'should not return a project pipeline' do + get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response['id']).to be nil + end + end + end + + describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do + context 'authorized user' do + let!(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) + end + + let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } + + it 'retries failed builds' do + expect do + post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user) + end.to change { pipeline.builds.count }.from(1).to(2) + + expect(response).to have_http_status(201) + expect(build.reload.retried?).to be true + end + end + + context 'unauthorized user' do + it 'should not return a project pipeline' do + post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + expect(json_response['id']).to be nil + end + end + end + + describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do + let!(:pipeline) do + create(:ci_empty_pipeline, project: project, sha: project.commit.id, + ref: project.default_branch) + end + + let!(:build) { create(:ci_build, :running, pipeline: pipeline) } + + context 'authorized user' do + it 'retries failed builds' do + post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('canceled') + end + end + + context 'user without proper access rights' do + let!(:reporter) { create(:user) } + + before { project.team << [reporter, :reporter] } + + it 'rejects the action' do + post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) + + expect(response).to have_http_status(403) + expect(pipeline.reload.status).to eq('pending') + end + end + end +end From ed1f110499d8e2aa011e17f1ad8a038e209842fb Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 1 Mar 2017 16:17:26 +0000 Subject: [PATCH 14/95] Fixes filtering by name reseting archive filter Previously the search form just wasn't including any params that had previously been set, so when you filtered by name, it would reset all the params & therefore ignoring the archived param Closes #28007 --- app/controllers/concerns/filter_projects.rb | 2 +- app/views/admin/projects/index.html.haml | 11 ++--------- app/views/dashboard/_projects_head.html.haml | 5 +++-- app/views/groups/show.html.haml | 5 +++-- app/views/shared/projects/_dropdown.html.haml | 17 +++++++++-------- .../shared/projects/_filter_fields.html.haml | 11 +++++++++++ .../dashboard-filter-search-keep-params.yml | 4 ++++ .../dashboard/archived_projects_spec.rb | 15 +++++++++++++++ 8 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 app/views/shared/projects/_filter_fields.html.haml create mode 100644 changelogs/unreleased/dashboard-filter-search-keep-params.yml diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb index 586f97c5eb4..6014112256a 100644 --- a/app/controllers/concerns/filter_projects.rb +++ b/app/controllers/concerns/filter_projects.rb @@ -8,7 +8,7 @@ module FilterProjects extend ActiveSupport::Concern def filter_projects(projects) - projects = projects.search(params[:filter_projects]) if params[:filter_projects].present? + projects = projects.search(params[:name]) if params[:name].present? projects = projects.non_archived if params[:archived].blank? projects = projects.personal(current_user) if params[:personal].present? && current_user diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index c35945c5a35..121662a2ea6 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -8,6 +8,7 @@ .top-area .prepend-top-default = form_tag admin_projects_path, method: :get do |f| + = render "shared/projects/filter_fields" .search-holder .search-field-holder = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' @@ -15,20 +16,12 @@ - if params[:visibility_level].present? = hidden_field_tag 'visibility_level', params[:visibility_level] - - if params[:sort].present? - = hidden_field_tag 'sort', params[:sort] - - - if params[:personal].present? - = hidden_field_tag 'visibility_level', 'true' - - - if params[:archived].present? - = hidden_field_tag 'archived', 'true' - = icon("search", class: "search-icon") .dropdown - toggle_text = 'Namespace' - if params[:namespace_id].present? + = hidden_field_tag :namespace_id, params[:namespace_id] - namespace = Namespace.find(params[:namespace_id]) - toggle_text = "#{namespace.kind}: #{namespace.full_path}" = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 48b0fd504f4..795ad9f33c1 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -13,8 +13,9 @@ Explore projects .nav-controls - = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| - = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2" + = form_tag request.fullpath, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| + = render "shared/projects/filter_fields" + = search_field_tag :name, params[:name], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2" = render 'shared/projects/dropdown' - if current_user.can_create_project? = link_to new_project_path, class: 'btn btn-new' do diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 8f0f2708194..5a7b0c1534b 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -11,8 +11,9 @@ .top-area = render 'groups/show_nav' .nav-controls - = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false + = form_tag request.fullpath, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| + = render "shared/projects/filter_fields" + = search_field_tag :name, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false = render 'shared/projects/dropdown' - if can? current_user, :create_projects, @group = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index c19697802ce..7e5c00e5608 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,4 +1,5 @@ - @sort ||= sort_value_recently_updated +- name = params[:name] - personal = params[:personal] - archived = params[:archived] - shared = params[:shared] @@ -11,32 +12,32 @@ Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal, name: name), class: ("is-active" if @sort == value) do = title %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil, name: name), class: ("is-active" unless params[:archived].present?) do Hide archived projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true, name: name), class: ("is-active" if params[:archived].present?) do Show archived projects - if current_user %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil), class: ("is-active" unless personal.present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil, name: name), class: ("is-active" unless personal.present?) do Owned by anyone %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true, name: name), class: ("is-active" if personal.present?) do Owned by me - if @group && @group.shared_projects.present? %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil), class: ("is-active" unless shared.present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil, name: name), class: ("is-active" unless shared.present?) do All projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0), class: ("is-active" if shared == '0') do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0, name: name), class: ("is-active" if shared == '0') do Hide shared projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1), class: ("is-active" if shared == '1') do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1, name: name), class: ("is-active" if shared == '1') do Hide group projects diff --git a/app/views/shared/projects/_filter_fields.html.haml b/app/views/shared/projects/_filter_fields.html.haml new file mode 100644 index 00000000000..26362ad1eb7 --- /dev/null +++ b/app/views/shared/projects/_filter_fields.html.haml @@ -0,0 +1,11 @@ +- if params[:sort].present? + = hidden_field_tag :sort, params[:sort] + +- if params[:personal].present? + = hidden_field_tag :personal, params[:personal] + +- if params[:archived].present? + = hidden_field_tag :archived, params[:archived] + +- if params[:visibility_level].present? + = hidden_field_tag :visibility_level, params[:visibility_level] diff --git a/changelogs/unreleased/dashboard-filter-search-keep-params.yml b/changelogs/unreleased/dashboard-filter-search-keep-params.yml new file mode 100644 index 00000000000..a140715b7a2 --- /dev/null +++ b/changelogs/unreleased/dashboard-filter-search-keep-params.yml @@ -0,0 +1,4 @@ +--- +title: Dashboard project search keeps selected sort & filters +merge_request: +author: diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb index 038c1641be9..f33bcbb5318 100644 --- a/spec/features/dashboard/archived_projects_spec.rb +++ b/spec/features/dashboard/archived_projects_spec.rb @@ -25,4 +25,19 @@ RSpec.describe 'Dashboard Archived Project', feature: true do expect(page).to have_link(project.name) expect(page).to have_link(archived_project.name) end + + it 'searchs archived projects', :js do + click_button 'Last updated' + click_link 'Show archived projects' + + expect(page).to have_link(project.name) + expect(page).to have_link(archived_project.name) + + fill_in 'project-filter-form-field', with: archived_project.name + + find('#project-filter-form-field').native.send_keys :return + + expect(page).not_to have_link(project.name) + expect(page).to have_link(archived_project.name) + end end From 14873c837b1baefe90df8cf38e117c8336f66040 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 1 Mar 2017 16:54:13 +0000 Subject: [PATCH 15/95] Fixed incorrect empty state showing --- app/views/dashboard/projects/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index f0adbad8412..4b095f52643 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -6,13 +6,13 @@ .user-callout{ 'callout-svg' => custom_icon('icon_customization') } -- if @projects.any? || params[:filter_projects] +- if @projects.any? || params[:name] = render 'dashboard/projects_head' - if @last_push = render "events/event_last_push", event: @last_push -- if @projects.any? || params[:filter_projects] +- if @projects.any? || params[:name] = render 'projects' - else = render "zero_authorized_projects" From 8fd5aeee7fee7506c44674d61f794ff8685b90e6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 1 Mar 2017 16:57:11 +0000 Subject: [PATCH 16/95] Fixed filter_projects param still used in group controller --- app/controllers/groups_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7ed54479599..15db5b7762d 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -108,7 +108,7 @@ class GroupsController < Groups::ApplicationController @projects = @projects.sorted_by_activity @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.page(params[:page]) if params[:filter_projects].blank? + @projects = @projects.page(params[:page]) if params[:name].blank? end def authorize_create_group! From 62b2d6735e6615628e9bfc80a14647f6825b4e4e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 2 Mar 2017 09:38:31 -0500 Subject: [PATCH 17/95] Set max for Zen mode textarea to screen height --- app/assets/stylesheets/framework/zen.scss | 3 ++- changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 97ade638db6..0c226ff7598 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -20,8 +20,9 @@ outline: none; resize: none; height: 100vh; + max-height: calc(100vh - 10px); max-width: 900px; - margin: 0 auto; + margin: 0 auto 10px; } .zen-control-leave { diff --git a/changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml b/changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml new file mode 100644 index 00000000000..b8dba0b5993 --- /dev/null +++ b/changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml @@ -0,0 +1,4 @@ +--- +title: Set max height to screen height for Zen mode +merge_request: 9667 +author: From ba4a2c546982b1b615be5ed9c8b96361de3f3545 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Mon, 20 Feb 2017 21:02:24 +0000 Subject: [PATCH 18/95] Add KUBE_CA_PEM_FILE, deprecate KUBE_CA_PEM --- app/models/project_services/kubernetes_service.rb | 7 ++++++- .../add-kube-ca-pem-file-deprecate-kube-ca-pem.yml | 4 ++++ doc/user/project/integrations/kubernetes.md | 3 ++- spec/models/project_services/kubernetes_service_spec.rb | 6 ++++++ 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 9819e723fe8..f2e1c906dac 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -94,7 +94,12 @@ class KubernetesService < DeploymentService { key: 'KUBE_TOKEN', value: token, public: false }, { key: 'KUBE_NAMESPACE', value: namespace, public: true } ] - variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present? + + if ca_pem.present? + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + end + variables end diff --git a/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml b/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml new file mode 100644 index 00000000000..1ae1e3c7a7a --- /dev/null +++ b/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml @@ -0,0 +1,4 @@ +--- +title: Add KUBE_CA_PEM_FILE, deprecate KUBE_CA_PEM +merge_request: 9398 +author: diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md index cc67e667472..2a890acde4d 100644 --- a/doc/user/project/integrations/kubernetes.md +++ b/doc/user/project/integrations/kubernetes.md @@ -49,7 +49,8 @@ GitLab CI build environment: - `KUBE_URL` - equal to the API URL - `KUBE_TOKEN` - `KUBE_NAMESPACE` -- `KUBE_CA_PEM` - only if a custom CA bundle was specified +- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path to a file containing PEM data. +- `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data. ## Web terminals diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 24356447fed..585c899cdf9 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -163,6 +163,12 @@ describe KubernetesService, models: true, caching: true do { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true } ) end + + it 'sets KUBE_CA_PEM_FILE' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true } + ) + end end describe '#terminals' do From 5bb6a85b902c6096970d6c82bb63cae7985e55e8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 3 Mar 2017 12:13:03 +0200 Subject: [PATCH 19/95] Refactor projects filtering by name Reuse same search form and behavior for dashboard#projects, group#projects and admin#projects. Repsect all other options like sorting, personal filter when search projects by name. Create FilterableList JS class to handle identical behaviour of projects and groups lists. This change also makes filtering and sorting availabe on explore#projects and explore#groups no matter if you are logged in or not. Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/dispatcher.js.es6 | 13 +++- app/assets/javascripts/filterable_list.js | 47 +++++++++++ app/assets/javascripts/groups_list.js | 44 ++--------- app/assets/javascripts/projects_list.js | 63 ++++----------- app/assets/stylesheets/pages/search.scss | 6 +- app/controllers/admin/projects_controller.rb | 9 +++ app/helpers/explore_helper.rb | 7 +- app/views/admin/projects/_projects.html.haml | 32 ++++++++ app/views/admin/projects/index.html.haml | 78 +++++-------------- app/views/dashboard/_groups_head.html.haml | 3 +- app/views/dashboard/_projects_head.html.haml | 4 +- app/views/dashboard/projects/index.html.haml | 1 - app/views/explore/groups/_nav.html.haml | 8 ++ app/views/explore/groups/index.html.haml | 2 +- app/views/explore/projects/_nav.html.haml | 27 ++++--- app/views/explore/projects/index.html.haml | 7 +- app/views/groups/show.html.haml | 4 +- .../shared/groups/_search_form.html.haml | 2 + app/views/shared/projects/_dropdown.html.haml | 21 ++--- .../shared/projects/_filter_fields.html.haml | 11 --- app/views/shared/projects/_list.html.haml | 5 +- .../shared/projects/_search_form.html.haml | 23 ++++++ features/steps/project/fork.rb | 2 +- 23 files changed, 212 insertions(+), 207 deletions(-) create mode 100644 app/assets/javascripts/filterable_list.js create mode 100644 app/views/admin/projects/_projects.html.haml create mode 100644 app/views/explore/groups/_nav.html.haml create mode 100644 app/views/shared/groups/_search_form.html.haml delete mode 100644 app/views/shared/projects/_filter_fields.html.haml create mode 100644 app/views/shared/projects/_search_form.html.haml diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index fc25122aedc..ef5785b5532 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -36,6 +36,7 @@ /* global Shortcuts */ import GroupsList from './groups_list'; +import ProjectsList from './projects_list'; const ShortcutsBlob = require('./shortcuts_blob'); const UserCallout = require('./user_callout'); @@ -98,6 +99,14 @@ const UserCallout = require('./user_callout'); case 'dashboard:todos:index': new gl.Todos(); break; + case 'dashboard:projects:index': + case 'dashboard:projects:starred': + case 'explore:projects:index': + case 'explore:projects:trending': + case 'explore:projects:starred': + case 'admin:projects:index': + new ProjectsList(); + break; case 'dashboard:groups:index': case 'explore:groups:index': new GroupsList(); @@ -163,9 +172,6 @@ const UserCallout = require('./user_callout'); case 'dashboard:activity': new gl.Activities(); break; - case 'dashboard:projects:starred': - new gl.Activities(); - break; case 'projects:commit:show': new Commit(); new gl.Diff(); @@ -208,6 +214,7 @@ const UserCallout = require('./user_callout'); shortcut_handler = new ShortcutsNavigation(); new NotificationsForm(); new NotificationsDropdown(); + new ProjectsList(); break; case 'groups:group_members:index': new gl.MemberExpirationDate(); diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js new file mode 100644 index 00000000000..f498c3ea973 --- /dev/null +++ b/app/assets/javascripts/filterable_list.js @@ -0,0 +1,47 @@ +/** + * Makes search request for content when user types a value in the search input. + * Updates the html content of the page with the received one. + */ +export default class FilterableList { + constructor(form, filter, holder) { + this.filterForm = form; + this.listFilterElement = filter; + this.listHolderElement = holder; + + this.initSearch(); + } + + initSearch() { + this.debounceFilter = _.debounce(this.filterResults.bind(this), 500); + + this.listFilterElement.removeEventListener('input', this.debounceFilter); + this.listFilterElement.addEventListener('input', this.debounceFilter); + } + + filterResults() { + const form = this.filterForm; + const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`; + + $(this.listHolderElement).fadeTo(250, 0.5); + + return $.ajax({ + url: form.getAttribute('action'), + data: $(form).serialize(), + type: 'GET', + dataType: 'json', + context: this, + complete() { + $(this.listHolderElement).fadeTo(250, 1); + }, + success(data) { + this.listHolderElement.innerHTML = data.html; + + // Change url so if user reload a page - search results are saved + return window.history.replaceState({ + page: filterUrl, + + }, document.title, filterUrl); + }, + }); + } +} diff --git a/app/assets/javascripts/groups_list.js b/app/assets/javascripts/groups_list.js index 0ef81e49444..49b29affaa5 100644 --- a/app/assets/javascripts/groups_list.js +++ b/app/assets/javascripts/groups_list.js @@ -1,47 +1,15 @@ +import FilterableList from './filterable_list'; + /** - * Based on project list search. * Makes search request for groups when user types a value in the search input. * Updates the html content of the page with the received one. */ export default class GroupsList { constructor() { - this.groupsListFilterElement = document.querySelector('.js-groups-list-filter'); - this.groupsListHolderElement = document.querySelector('.js-groups-list-holder'); + var form = document.querySelector('form#group-filter-form'); + var filter = document.querySelector('.js-groups-list-filter'); + var holder = document.querySelector('.js-groups-list-holder'); - this.initSearch(); - } - - initSearch() { - this.debounceFilter = _.debounce(this.filterResults.bind(this), 500); - - this.groupsListFilterElement.removeEventListener('input', this.debounceFilter); - this.groupsListFilterElement.addEventListener('input', this.debounceFilter); - } - - filterResults() { - const form = document.querySelector('form#group-filter-form'); - const groupFilterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`; - - $(this.groupsListHolderElement).fadeTo(250, 0.5); - - return $.ajax({ - url: form.getAttribute('action'), - data: $(form).serialize(), - type: 'GET', - dataType: 'json', - context: this, - complete() { - $(this.groupsListHolderElement).fadeTo(250, 1); - }, - success(data) { - this.groupsListHolderElement.innerHTML = data.html; - - // Change url so if user reload a page - search results are saved - return window.history.replaceState({ - page: groupFilterUrl, - - }, document.title, groupFilterUrl); - }, - }); + new FilterableList(form, filter, holder); } } diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index acdf9b7eb5a..383c2815457 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -1,50 +1,15 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, max-len */ +import FilterableList from './filterable_list'; -(function() { - window.ProjectsList = { - init: function() { - $(".projects-list-filter").off('keyup'); - this.initSearch(); - return this.initPagination(); - }, - initSearch: function() { - var debounceFilter, projectsListFilter; - projectsListFilter = $('.projects-list-filter'); - debounceFilter = _.debounce(window.ProjectsList.filterResults, 500); - return projectsListFilter.on('keyup', function(e) { - if (projectsListFilter.val() !== '') { - return debounceFilter(); - } - }); - }, - filterResults: function() { - var form, project_filter_url, search; - $('.projects-list-holder').fadeTo(250, 0.5); - form = null; - form = $("form#project-filter-form"); - search = $(".projects-list-filter").val(); - project_filter_url = form.attr('action') + '?' + form.serialize(); - return $.ajax({ - type: "GET", - url: form.attr('action'), - data: form.serialize(), - complete: function() { - return $('.projects-list-holder').fadeTo(250, 1); - }, - success: function(data) { - $('.projects-list-holder').replaceWith(data.html); - return history.replaceState({ - page: project_filter_url - // Change url so if user reload a page - search results are saved - }, document.title, project_filter_url); - }, - dataType: "json" - }); - }, - initPagination: function() { - return $('.projects-list-holder .pagination').on('ajax:success', function(e, data) { - return $('.projects-list-holder').replaceWith(data.html); - }); - } - }; -}).call(window); +/** + * Makes search request for projects when user types a value in the search input. + * Updates the html content of the page with the received one. + */ +export default class ProjectsList { + constructor() { + var form = document.querySelector('form#project-filter-form'); + var filter = document.querySelector('.js-projects-list-filter'); + var holder = document.querySelector('.js-projects-list-holder'); + + new FilterableList(form, filter, holder); + } +} diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 88ea92c5afb..543d2ece3df 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -182,7 +182,8 @@ input[type="checkbox"]:hover { display: flex; } - .search-field-holder { + .search-field-holder, + .project-filter-form { -webkit-flex: 1 0 auto; flex: 1 0 auto; position: relative; @@ -201,7 +202,8 @@ input[type="checkbox"]:hover { pointer-events: none; } - .search-text-input { + .search-text-input, + .project-filter-form-field { padding-left: $gl-padding + 15px; padding-right: $gl-padding + 15px; } diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 39c8c6d8a0c..daecfc832bf 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -14,6 +14,15 @@ class Admin::ProjectsController < Admin::ApplicationController @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("admin/projects/_projects", locals: { projects: @projects }) + } + end + end end def show diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index bbcb52f7eaf..cb9bb33f492 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -1,11 +1,16 @@ module ExploreHelper def filter_projects_path(options = {}) exist_opts = { - sort: params[:sort], + sort: params[:sort] || @sort, scope: params[:scope], group: params[:group], tag: params[:tag], visibility_level: params[:visibility_level], + name: params[:name], + personal: params[:personal], + archived: params[:archived], + shared: params[:shared], + namespace_id: params[:namespace_id], } options = exist_opts.merge(options) diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml new file mode 100644 index 00000000000..c1a9f8d6ddd --- /dev/null +++ b/app/views/admin/projects/_projects.html.haml @@ -0,0 +1,32 @@ +.js-projects-list-holder + - if @projects.any? + %ul.projects-list.content-list + - @projects.each_with_index do |project| + %li.project-row + .controls + - if project.archived + %span.label.label-warning archived + %span.badge + = storage_counter(project.statistics.storage_size) + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" + = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" + .title + = link_to [:admin, project.namespace.becomes(Namespace), project] do + .dash-project-avatar + .avatar-container.s40 + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + %span.project-full-name + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name + + - if project.description.present? + .description + = markdown_field(project, :description) + + = paginate @projects, theme: 'gitlab' + - else + .nothing-here-block No projects found diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 121662a2ea6..3301f55b8a8 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -7,33 +7,24 @@ %div{ class: container_class } .top-area .prepend-top-default - = form_tag admin_projects_path, method: :get do |f| - = render "shared/projects/filter_fields" - .search-holder - .search-field-holder - = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' - - - if params[:visibility_level].present? - = hidden_field_tag 'visibility_level', params[:visibility_level] - - = icon("search", class: "search-icon") - - .dropdown - - toggle_text = 'Namespace' - - if params[:namespace_id].present? - = hidden_field_tag :namespace_id, params[:namespace_id] - - namespace = Namespace.find(params[:namespace_id]) - - toggle_text = "#{namespace.kind}: #{namespace.full_path}" - = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) - .dropdown-menu.dropdown-select.dropdown-menu-align-right - = dropdown_title('Namespaces') - = dropdown_filter("Search for Namespace") - = dropdown_content - = dropdown_loading - = render 'shared/projects/dropdown' - = link_to new_project_path, class: 'btn btn-new' do - New Project - = button_tag "Search", class: "btn btn-primary btn-search hide" + .search-holder + = render 'shared/projects/search_form', autofocus: true, icon: true + .dropdown + - toggle_text = 'Namespace' + - if params[:namespace_id].present? + = hidden_field_tag :namespace_id, params[:namespace_id] + - namespace = Namespace.find(params[:namespace_id]) + - toggle_text = "#{namespace.kind}: #{namespace.full_path}" + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) + .dropdown-menu.dropdown-select.dropdown-menu-align-right + = dropdown_title('Namespaces') + = dropdown_filter("Search for Namespace") + = dropdown_content + = dropdown_loading + = render 'shared/projects/dropdown' + = link_to new_project_path, class: 'btn btn-new' do + New Project + = button_tag "Search", class: "btn btn-primary btn-search hide" %ul.nav-links - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } @@ -51,35 +42,4 @@ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do Public - .projects-list-holder - - if @projects.any? - %ul.projects-list.content-list - - @projects.each_with_index do |project| - %li.project-row - .controls - - if project.archived - %span.label.label-warning archived - %span.badge - = storage_counter(project.statistics.storage_size) - = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" - = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" - .title - = link_to [:admin, project.namespace.becomes(Namespace), project] do - .dash-project-avatar - .avatar-container.s40 - = project_icon(project, alt: '', class: 'avatar project-avatar s40') - %span.project-full-name - %span.namespace-name - - if project.namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name - - - if project.description.present? - .description - = markdown_field(project, :description) - - = paginate @projects, theme: 'gitlab' - - else - .nothing-here-block No projects found + = render 'projects' diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index c6d5937a3c3..13eaba41f4c 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -7,8 +7,7 @@ = link_to explore_groups_path, title: 'Explore groups' do Explore Groups .nav-controls - = form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f| - = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" + = render 'shared/groups/search_form' = render 'shared/groups/dropdown' - if current_user.can_create_group? = link_to new_group_path, class: "btn btn-new" do diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 795ad9f33c1..600ee63a5c0 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -13,9 +13,7 @@ Explore projects .nav-controls - = form_tag request.fullpath, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| - = render "shared/projects/filter_fields" - = search_field_tag :name, params[:name], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2" + = render 'shared/projects/search_form' = render 'shared/projects/dropdown' - if current_user.can_create_project? = link_to new_project_path, class: 'btn btn-new' do diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 4b095f52643..eef794dbd51 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -5,7 +5,6 @@ - header_title "Projects", dashboard_projects_path .user-callout{ 'callout-svg' => custom_icon('icon_customization') } - - if @projects.any? || params[:name] = render 'dashboard/projects_head' diff --git a/app/views/explore/groups/_nav.html.haml b/app/views/explore/groups/_nav.html.haml new file mode 100644 index 00000000000..c8d95b52156 --- /dev/null +++ b/app/views/explore/groups/_nav.html.haml @@ -0,0 +1,8 @@ +.top-area + %ul.nav-links + = nav_link(page: explore_groups_path) do + = link_to explore_groups_path do + Explore Groups + .nav-controls + = render 'shared/groups/search_form' + = render 'shared/groups/dropdown' diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 7f1bacc91cb..8374f5a009f 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -5,7 +5,7 @@ = render 'dashboard/groups_head' - else = render 'explore/head' - + = render 'nav' - if @groups.present? = render 'groups' diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml index 614b5431779..e0a2a1e9c96 100644 --- a/app/views/explore/projects/_nav.html.haml +++ b/app/views/explore/projects/_nav.html.haml @@ -1,10 +1,17 @@ -%ul.nav-links - = nav_link(page: [trending_explore_projects_path, explore_root_path]) do - = link_to trending_explore_projects_path do - Trending - = nav_link(page: starred_explore_projects_path) do - = link_to starred_explore_projects_path do - Most stars - = nav_link(page: explore_projects_path) do - = link_to explore_projects_path do - All +.top-area + %ul.nav-links + = nav_link(page: [trending_explore_projects_path, explore_root_path]) do + = link_to trending_explore_projects_path do + Trending + = nav_link(page: starred_explore_projects_path) do + = link_to starred_explore_projects_path do + Most stars + = nav_link(page: explore_projects_path) do + = link_to explore_projects_path do + All + + .nav-controls + - unless current_user + = render 'shared/projects/search_form' + = render 'shared/projects/dropdown' + = render 'filter' diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index 42b50481b9d..ec461755103 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -6,10 +6,5 @@ - else = render 'explore/head' -.top-area - = render 'explore/projects/nav' - - .nav-controls - = render 'filter' - += render 'explore/projects/nav' = render 'projects', projects: @projects diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 5a7b0c1534b..18997baa998 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -11,9 +11,7 @@ .top-area = render 'groups/show_nav' .nav-controls - = form_tag request.fullpath, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| - = render "shared/projects/filter_fields" - = search_field_tag :name, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false + = render 'shared/projects/search_form' = render 'shared/projects/dropdown' - if can? current_user, :create_projects, @group = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do diff --git a/app/views/shared/groups/_search_form.html.haml b/app/views/shared/groups/_search_form.html.haml new file mode 100644 index 00000000000..ad7a7faedf1 --- /dev/null +++ b/app/views/shared/groups/_search_form.html.haml @@ -0,0 +1,2 @@ += form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f| + = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 7e5c00e5608..2d25b8aad62 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,9 +1,4 @@ - @sort ||= sort_value_recently_updated -- name = params[:name] -- personal = params[:personal] -- archived = params[:archived] -- shared = params[:shared] -- namespace_id = params[:namespace_id] .dropdown - toggle_text = projects_sort_options_hash[@sort] = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) @@ -12,32 +7,32 @@ Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal, name: name), class: ("is-active" if @sort == value) do + = link_to filter_projects_path(sort: value), class: ("is-active" if @sort == value) do = title %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil, name: name), class: ("is-active" unless params[:archived].present?) do + = link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do Hide archived projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true, name: name), class: ("is-active" if params[:archived].present?) do + = link_to filter_projects_path(archived: true), class: ("is-active" if params[:archived].present?) do Show archived projects - if current_user %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil, name: name), class: ("is-active" unless personal.present?) do + = link_to filter_projects_path(personal: nil), class: ("is-active" unless params[:personal].present?) do Owned by anyone %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true, name: name), class: ("is-active" if personal.present?) do + = link_to filter_projects_path(personal: true), class: ("is-active" if params[:personal].present?) do Owned by me - if @group && @group.shared_projects.present? %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil, name: name), class: ("is-active" unless shared.present?) do + = link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do All projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0, name: name), class: ("is-active" if shared == '0') do + = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do Hide shared projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1, name: name), class: ("is-active" if shared == '1') do + = link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do Hide group projects diff --git a/app/views/shared/projects/_filter_fields.html.haml b/app/views/shared/projects/_filter_fields.html.haml deleted file mode 100644 index 26362ad1eb7..00000000000 --- a/app/views/shared/projects/_filter_fields.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- if params[:sort].present? - = hidden_field_tag :sort, params[:sort] - -- if params[:personal].present? - = hidden_field_tag :personal, params[:personal] - -- if params[:archived].present? - = hidden_field_tag :archived, params[:archived] - -- if params[:visibility_level].present? - = hidden_field_tag :visibility_level, params[:visibility_level] diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 3a9dd37dc7d..c57282c5742 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -8,7 +8,7 @@ - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - remote = false unless local_assigns[:remote] == true -.projects-list-holder +.js-projects-list-holder - if projects.any? %ul.projects-list.content-list - projects.each_with_index do |project, i| @@ -25,6 +25,3 @@ = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages - else .nothing-here-block No projects found - -:javascript - ProjectsList.init(); diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml new file mode 100644 index 00000000000..b89194bcc67 --- /dev/null +++ b/app/views/shared/projects/_search_form.html.haml @@ -0,0 +1,23 @@ += form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| + = search_field_tag :name, params[:name], + placeholder: 'Filter by name...', + class: 'project-filter-form-field form-control input-short js-projects-list-filter', + spellcheck: false, + id: 'project-filter-form-field', + tabindex: "2", + autofocus: local_assigns[:autofocus] + + - if local_assigns[:icon] + = icon("search", class: "search-icon") + + - if params[:sort].present? + = hidden_field_tag :sort, params[:sort] + + - if params[:personal].present? + = hidden_field_tag :personal, params[:personal] + + - if params[:archived].present? + = hidden_field_tag :archived, params[:archived] + + - if params[:visibility_level].present? + = hidden_field_tag :visibility_level, params[:visibility_level] diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 9a6c04fba7a..79db9728227 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -56,7 +56,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I should see my fork on the list' do - page.within('.projects-list-holder') do + page.within('.js-projects-list-holder') do project = @user.fork_of(@project) expect(page).to have_content("#{project.namespace.human_name} / #{project.name}") end From d13669c98b5b2c15cfff8b65e12ce1ef0911f1cd Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 24 Feb 2017 13:18:07 +0100 Subject: [PATCH 20/95] Remove remnants of git annex --- app/controllers/profiles/keys_controller.rb | 5 ----- ...8-remove-remnants-of-git-annex-from-ce.yml | 4 ++++ config/routes/profile.rb | 2 +- doc/ci/variables/README.md | 4 ++-- doc/downgrade_ee_to_ce/README.md | 7 ------- doc/university/README.md | 2 +- doc/university/support/README.md | 1 - .../lfs/manage_large_binaries_with_git_lfs.md | 7 ------- lib/backup/repository.rb | 5 ++--- .../profiles/keys_controller_spec.rb | 10 --------- spec/tasks/gitlab/backup_rake_spec.rb | 21 +------------------ 11 files changed, 11 insertions(+), 57 deletions(-) create mode 100644 changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index c8663a3c38e..e4452f46056 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -10,11 +10,6 @@ class Profiles::KeysController < Profiles::ApplicationController @key = current_user.keys.find(params[:id]) end - # Back-compat: We need to support this URL since git-annex webapp points to it - def new - redirect_to profile_keys_path - end - def create @key = current_user.keys.new(key_params) diff --git a/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml b/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml new file mode 100644 index 00000000000..f247fe35439 --- /dev/null +++ b/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml @@ -0,0 +1,4 @@ +--- +title: Remove remnants of git annex support. +merge_request: +author: diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 6b91485da9e..07c341999ea 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -21,7 +21,7 @@ resource :profile, only: [:show, :update] do end end resource :preferences, only: [:show, :update] - resources :keys, only: [:index, :show, :new, :create, :destroy] + resources :keys, only: [:index, :show, :create, :destroy] resources :emails, only: [:index, :create, :destroy] resources :chat_names, only: [:index, :new, :create, :destroy] do collection do diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 04c0af44237..a9e25187b88 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -308,8 +308,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach ++ CI_RUNNER_ID=1337 ++ export CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com ++ CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com -++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo, git-annex' -++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo, git-annex' +++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo' +++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo' ++ export CI_REGISTRY=registry.example.com ++ CI_REGISTRY=registry.example.com ++ export CI_DEBUG_TRACE=true diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md index a6d22e5a04a..fe4b6d73771 100644 --- a/doc/downgrade_ee_to_ce/README.md +++ b/doc/downgrade_ee_to_ce/README.md @@ -15,13 +15,6 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so you should disable these mechanisms before downgrading and you should provide alternative authentication methods to your users. -### Git Annex - -Git Annex is also only available on the Enterprise Edition. This means that if -you have repositories that use Git Annex to store large files, these files will -no longer be easily available via Git. You should consider migrating these -repositories to use Git LFS before downgrading to the Community Edition. - ### Remove Jenkins CI Service entries from the database The `JenkinsService` class is only available on the Enterprise Edition codebase, diff --git a/doc/university/README.md b/doc/university/README.md index 8d4e7eff115..c1661f0b52b 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -165,7 +165,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project #### 3.4. Large Files -1. [Big files in Git (Git LFS, Annex) - Video](https://www.youtube.com/watch?v=DawznUxYDe4) +1. [Big files in Git (Git LFS) - Video](https://www.youtube.com/watch?v=DawznUxYDe4) #### 3.5. LDAP and Active Directory diff --git a/doc/university/support/README.md b/doc/university/support/README.md index ca538ef6dc3..567dadb3b47 100644 --- a/doc/university/support/README.md +++ b/doc/university/support/README.md @@ -167,7 +167,6 @@ Some tickets need specific knowledge or a deep understanding of a particular com Move on to understanding some of GitLab's more advanced features. You can make use of GitLab.com to understand the features from an end-user perspective and then use your own instance to understand setup and configuration of the feature from an Administrative perspective -- Set up and try [Git Annex](https://docs.gitlab.com/ee/workflow/git_annex.html) - Set up and try [Git LFS](https://docs.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html) - Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings - Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html) diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 9cc45065eb2..6adde447975 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -4,13 +4,6 @@ Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. The general recommendation is to not have Git repositories larger than 1GB to preserve performance. -GitLab already supports [managing large files with git annex](http://docs.gitlab.com/ee/workflow/git_annex.html) -(EE only), however in certain environments it is not always convenient to use -different commands to differentiate between the large files and regular ones. - -Git LFS makes this simpler for the end user by removing the requirement to -learn new commands. - ## How it works Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index d16d5ba4960..3c4ba5d50e6 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -180,9 +180,8 @@ module Backup return unless Dir.exist?(path) dir_entries = Dir.entries(path) - %w[annex custom_hooks].each do |entry| - yield(entry) if dir_entries.include?(entry) - end + + yield('custom_hooks') if dir_entries.include?('custom_hooks') end def prepare diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index f7219690722..61e4fae46fb 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -3,16 +3,6 @@ require 'spec_helper' describe Profiles::KeysController do let(:user) { create(:user) } - describe '#new' do - before { sign_in(user) } - - it 'redirects to #index' do - get :new - - expect(response).to redirect_to(profile_keys_path) - end - end - describe "#get_keys" do describe "non existant user" do it "does not generally work" do diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index df8a47893f9..dfbfbd05f43 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -108,7 +108,7 @@ describe 'gitlab:app namespace rake task' do $stdout = orig_stdout end - describe 'backup creation and deletion using annex and custom_hooks' do + describe 'backup creation and deletion using custom_hooks' do let(:project) { create(:project) } let(:user_backup_path) { "repositories/#{project.path_with_namespace}" } @@ -132,25 +132,6 @@ describe 'gitlab:app namespace rake task' do Dir.chdir(@origin_cd) end - context 'project uses git-annex and successfully creates backup' do - let(:filename) { "annex" } - - it 'creates annex.tar and project bundle' do - tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{@backup_tar}}) - - expect(exit_status).to eq(0) - expect(tar_contents).to match(user_backup_path) - expect(tar_contents).to match("#{user_backup_path}/annex.tar") - expect(tar_contents).to match("#{user_backup_path}.bundle") - end - - it 'restores files correctly' do - restore_backup - - expect(Dir.entries(File.join(project.repository.path, "annex"))).to include("dummy.txt") - end - end - context 'project uses custom_hooks and successfully creates backup' do let(:filename) { "custom_hooks" } From 1d49d065ed168fcc3653d0d4681485edd524043e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 3 Mar 2017 18:03:58 +0000 Subject: [PATCH 21/95] Removed empty options from URL --- app/helpers/explore_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index cb9bb33f492..7bd212a3ef9 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -13,7 +13,7 @@ module ExploreHelper namespace_id: params[:namespace_id], } - options = exist_opts.merge(options) + options = exist_opts.merge(options).delete_if { |key, value| value.blank? } request_path_with_options(options) end From 0fcb8ce7d66f2d0a923a2de6568116f8e8088028 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 3 Mar 2017 15:44:08 -0300 Subject: [PATCH 22/95] Bump health_check gem to version 2.6.0 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/controllers/admin/health_check_controller.rb | 2 +- spec/controllers/health_check_controller_spec.rb | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index b21f563940d..a267030eb4c 100644 --- a/Gemfile +++ b/Gemfile @@ -344,7 +344,7 @@ gem 'oauth2', '~> 1.2.0' gem 'paranoia', '~> 2.2' # Health check -gem 'health_check', '~> 2.2.0' +gem 'health_check', '~> 2.6.0' # System information gem 'vmstat', '~> 2.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 2d7e6f6f0bf..65120df205c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -336,7 +336,7 @@ GEM thor tilt hashie (3.5.5) - health_check (2.2.1) + health_check (2.6.0) rails (>= 4.0) hipchat (1.5.2) httparty @@ -895,7 +895,7 @@ DEPENDENCIES grape-entity (~> 0.6.0) haml_lint (~> 0.21.0) hamlit (~> 2.6.1) - health_check (~> 2.2.0) + health_check (~> 2.6.0) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) html2text diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb index 241c7be0ea1..caf4c138da8 100644 --- a/app/controllers/admin/health_check_controller.rb +++ b/app/controllers/admin/health_check_controller.rb @@ -1,5 +1,5 @@ class Admin::HealthCheckController < Admin::ApplicationController def show - @errors = HealthCheck::Utils.process_checks('standard') + @errors = HealthCheck::Utils.process_checks(['standard']) end end diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb index cfe18dd4b6c..58c16cc57e6 100644 --- a/spec/controllers/health_check_controller_spec.rb +++ b/spec/controllers/health_check_controller_spec.rb @@ -64,8 +64,8 @@ describe HealthCheckController do context 'when a service is down and an access token is provided' do before do - allow(HealthCheck::Utils).to receive(:process_checks).with('standard').and_return('The server is on fire') - allow(HealthCheck::Utils).to receive(:process_checks).with('email').and_return('Email is on fire') + allow(HealthCheck::Utils).to receive(:process_checks).with(['standard']).and_return('The server is on fire') + allow(HealthCheck::Utils).to receive(:process_checks).with(['email']).and_return('Email is on fire') end it 'supports passing the token in the header' do From f13fbf62bc54301e969951920833cbdb67df3ca5 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 3 Mar 2017 20:17:37 +0000 Subject: [PATCH 23/95] Revert "Merge branch 'tooltip-hide-on-scroll' into 'master'" This reverts merge request !9653 --- app/assets/javascripts/application.js | 4 ---- changelogs/unreleased/tooltip-hide-on-scroll.yml | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 changelogs/unreleased/tooltip-hide-on-scroll.yml diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e0ee698a8ff..4c24d35b5bb 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -231,10 +231,6 @@ require('es6-promise').polyfill(); var bootstrapBreakpoint = bp.getBreakpointSize(); var fitSidebarForSize; - $(document).on('scroll', function() { - $('.has-tooltip').tooltip('hide'); - }); - // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; diff --git a/changelogs/unreleased/tooltip-hide-on-scroll.yml b/changelogs/unreleased/tooltip-hide-on-scroll.yml deleted file mode 100644 index cd81d303330..00000000000 --- a/changelogs/unreleased/tooltip-hide-on-scroll.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed tooltip remaining after scrolling the page -merge_request: -author: From 02504f2f9c8f0316b9235121f9677a998136bfc4 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 10 Feb 2017 00:45:42 -0600 Subject: [PATCH 24/95] use deterministic module IDs in production and development --- config/webpack.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/webpack.config.js b/config/webpack.config.js index 13273902b0e..d06f7733945 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -83,6 +83,10 @@ var config = { assets: true }), new webpack.IgnorePlugin(/moment/, /pikaday/), + // use deterministic module ids in all environments + IS_PRODUCTION ? + new webpack.HashedModuleIdsPlugin() : + new webpack.NamedModulesPlugin(), ], resolve: { From e80fa69895dadfbd5cdc95f7feb9593cfe52e9b6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 1 Mar 2017 15:47:52 -0600 Subject: [PATCH 25/95] update plugin formatting --- config/webpack.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/webpack.config.js b/config/webpack.config.js index d06f7733945..cbf59276dce 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -82,7 +82,10 @@ var config = { modules: false, assets: true }), + + // prevent pikaday from including moment.js new webpack.IgnorePlugin(/moment/, /pikaday/), + // use deterministic module ids in all environments IS_PRODUCTION ? new webpack.HashedModuleIdsPlugin() : From cb6c036d8942ab24048b9ecebcc6c3fa408c9a3e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 1 Mar 2017 16:01:33 -0600 Subject: [PATCH 26/95] create a common.js bundle and include all jQuery plugins --- app/assets/javascripts/application.js | 18 +----------------- app/assets/javascripts/commons/bootstrap.js | 10 ++++++++++ app/assets/javascripts/commons/index.js | 2 ++ app/assets/javascripts/commons/jquery.js | 12 ++++++++++++ app/views/layouts/_head.html.haml | 1 + config/webpack.config.js | 8 +++++++- spec/javascripts/test_bundle.js | 13 +------------ 7 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 app/assets/javascripts/commons/bootstrap.js create mode 100644 app/assets/javascripts/commons/index.js create mode 100644 app/assets/javascripts/commons/jquery.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4c24d35b5bb..8441a335ac0 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -6,23 +6,8 @@ /* global AwardsHandler */ /* global Aside */ -window.$ = window.jQuery = require('jquery'); -require('jquery-ujs'); -require('vendor/jquery.endless-scroll'); -require('vendor/jquery.waitforimages'); -require('vendor/jquery.caret'); -require('vendor/jquery.atwho'); -require('vendor/jquery.scrollTo'); +// common libraries window.Cookies = require('js-cookie'); -require('./autosave'); -require('bootstrap/js/affix'); -require('bootstrap/js/alert'); -require('bootstrap/js/dropdown'); -require('bootstrap/js/modal'); -require('bootstrap/js/tab'); -require('bootstrap/js/transition'); -require('bootstrap/js/tooltip'); -require('select2/select2.js'); window.Pikaday = require('pikaday'); window._ = require('underscore'); window.Dropzone = require('dropzone'); @@ -34,7 +19,6 @@ require('./shortcuts_navigation'); require('./shortcuts_dashboard_navigation'); require('./shortcuts_issuable'); require('./shortcuts_network'); -require('vendor/jquery.nicescroll'); // behaviors require('./behaviors/autosize'); diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js new file mode 100644 index 00000000000..72e43d34a74 --- /dev/null +++ b/app/assets/javascripts/commons/bootstrap.js @@ -0,0 +1,10 @@ +require('./jquery'); + +// twitter bootstrap plugins +require('bootstrap-sass/assets/javascripts/bootstrap/affix'); +require('bootstrap-sass/assets/javascripts/bootstrap/alert'); +require('bootstrap-sass/assets/javascripts/bootstrap/dropdown'); +require('bootstrap-sass/assets/javascripts/bootstrap/modal'); +require('bootstrap-sass/assets/javascripts/bootstrap/tab'); +require('bootstrap-sass/assets/javascripts/bootstrap/transition'); +require('bootstrap-sass/assets/javascripts/bootstrap/tooltip'); diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js new file mode 100644 index 00000000000..a9226bc1325 --- /dev/null +++ b/app/assets/javascripts/commons/index.js @@ -0,0 +1,2 @@ +require('./jquery'); +require('./bootstrap'); diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js new file mode 100644 index 00000000000..9ef415d6a95 --- /dev/null +++ b/app/assets/javascripts/commons/jquery.js @@ -0,0 +1,12 @@ +// jQuery library +window.$ = window.jQuery = require('jquery'); + +// jQuery plugins +require('jquery-ujs'); +require('vendor/jquery.endless-scroll'); +require('vendor/jquery.caret'); +require('vendor/jquery.atwho'); +require('vendor/jquery.scrollTo'); +require('vendor/jquery.nicescroll'); +require('vendor/jquery.waitforimages'); +require('select2/select2.js'); diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 302c1794628..29844b1027b 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,6 +28,7 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" + = javascript_include_tag(*webpack_asset_paths("common")) = javascript_include_tag(*webpack_asset_paths("application")) - if content_for?(:page_specific_javascripts) diff --git a/config/webpack.config.js b/config/webpack.config.js index cbf59276dce..c2e0e86fb78 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -17,6 +17,7 @@ var WEBPACK_REPORT = process.env.WEBPACK_REPORT; var config = { context: path.join(ROOT_PATH, 'app/assets/javascripts'), entry: { + common: './commons/index.js', application: './application.js', blob_edit: './blob_edit/blob_edit_bundle.js', boards: './boards/boards_bundle.js', @@ -90,13 +91,18 @@ var config = { IS_PRODUCTION ? new webpack.HashedModuleIdsPlugin() : new webpack.NamedModulesPlugin(), + + // create a common.js bundle to be loaded on every page + new webpack.optimize.CommonsChunkPlugin({ + name: 'common', + minChunks: Infinity, + }), ], resolve: { extensions: ['.js', '.es6', '.js.es6'], alias: { '~': path.join(ROOT_PATH, 'app/assets/javascripts'), - 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index ca707d872a4..fae462561e9 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -5,23 +5,12 @@ jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; // include common libraries +require('~/commons/index.js'); window.$ = window.jQuery = require('jquery'); window._ = require('underscore'); window.Cookies = require('js-cookie'); window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -require('jquery-ujs'); -require('bootstrap/js/affix'); -require('bootstrap/js/alert'); -require('bootstrap/js/button'); -require('bootstrap/js/collapse'); -require('bootstrap/js/dropdown'); -require('bootstrap/js/modal'); -require('bootstrap/js/scrollspy'); -require('bootstrap/js/tab'); -require('bootstrap/js/transition'); -require('bootstrap/js/tooltip'); -require('bootstrap/js/popover'); // stub expected globals window.gl = window.gl || {}; From 7371f6cd8c168df41e4197f80f53f3c04185dee7 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 1 Mar 2017 17:02:01 -0600 Subject: [PATCH 27/95] refactor common bundle to ES module syntax and move global exports to application.js --- app/assets/javascripts/application.js | 32 ++++++++++++++------- app/assets/javascripts/commons/bootstrap.js | 18 ++++++------ app/assets/javascripts/commons/index.js | 4 +-- app/assets/javascripts/commons/jquery.js | 21 +++++++------- config/webpack.config.js | 6 ++++ 5 files changed, 49 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 8441a335ac0..798553c16ac 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -6,14 +6,29 @@ /* global AwardsHandler */ /* global Aside */ -// common libraries -window.Cookies = require('js-cookie'); -window.Pikaday = require('pikaday'); -window._ = require('underscore'); -window.Dropzone = require('dropzone'); -window.Sortable = require('vendor/Sortable'); +import jQuery from 'jquery'; +import _ from 'underscore'; +import Cookies from 'js-cookie'; +import Pikaday from 'pikaday'; +import Dropzone from 'dropzone'; +import Sortable from 'vendor/Sortable'; + +// libraries with import side-effects require('mousetrap'); require('mousetrap/plugins/pause/mousetrap-pause'); +require('vendor/fuzzaldrin-plus'); +require('es6-promise').polyfill(); + +// expose common libraries as globals (TODO: remove these) +window.jQuery = jQuery; +window.$ = jQuery; +window._ = _; +window.Cookies = Cookies; +window.Pikaday = Pikaday; +window.Dropzone = Dropzone; +window.Sortable = Sortable; + +// shortcuts require('./shortcuts'); require('./shortcuts_navigation'); require('./shortcuts_dashboard_navigation'); @@ -189,9 +204,6 @@ require('./visibility_select'); require('./wikis'); require('./zen_mode'); -require('vendor/fuzzaldrin-plus'); -require('es6-promise').polyfill(); - (function () { document.addEventListener('beforeunload', function () { // Unbind scroll events @@ -269,7 +281,7 @@ require('es6-promise').polyfill(); $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', - placement: function (_, el) { + placement: function (tip, el) { return $(el).data('placement') || 'bottom'; } }); diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index 72e43d34a74..db0cbfd87c3 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -1,10 +1,10 @@ -require('./jquery'); +import 'jquery'; -// twitter bootstrap plugins -require('bootstrap-sass/assets/javascripts/bootstrap/affix'); -require('bootstrap-sass/assets/javascripts/bootstrap/alert'); -require('bootstrap-sass/assets/javascripts/bootstrap/dropdown'); -require('bootstrap-sass/assets/javascripts/bootstrap/modal'); -require('bootstrap-sass/assets/javascripts/bootstrap/tab'); -require('bootstrap-sass/assets/javascripts/bootstrap/transition'); -require('bootstrap-sass/assets/javascripts/bootstrap/tooltip'); +// bootstrap jQuery plugins +import 'bootstrap-sass/assets/javascripts/bootstrap/affix'; +import 'bootstrap-sass/assets/javascripts/bootstrap/alert'; +import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown'; +import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; +import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; +import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; +import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index a9226bc1325..72ede1d621a 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -1,2 +1,2 @@ -require('./jquery'); -require('./bootstrap'); +import './jquery'; +import './bootstrap'; diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js index 9ef415d6a95..b53f6284afc 100644 --- a/app/assets/javascripts/commons/jquery.js +++ b/app/assets/javascripts/commons/jquery.js @@ -1,12 +1,11 @@ -// jQuery library -window.$ = window.jQuery = require('jquery'); +import 'jquery'; -// jQuery plugins -require('jquery-ujs'); -require('vendor/jquery.endless-scroll'); -require('vendor/jquery.caret'); -require('vendor/jquery.atwho'); -require('vendor/jquery.scrollTo'); -require('vendor/jquery.nicescroll'); -require('vendor/jquery.waitforimages'); -require('select2/select2.js'); +// common jQuery plugins +import 'jquery-ujs'; +import 'vendor/jquery.endless-scroll'; +import 'vendor/jquery.caret'; +import 'vendor/jquery.atwho'; +import 'vendor/jquery.scrollTo'; +import 'vendor/jquery.nicescroll'; +import 'vendor/jquery.waitforimages'; +import 'select2/select2'; diff --git a/config/webpack.config.js b/config/webpack.config.js index c2e0e86fb78..a76daa2ba95 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -87,6 +87,12 @@ var config = { // prevent pikaday from including moment.js new webpack.IgnorePlugin(/moment/, /pikaday/), + // fix legacy jQuery plugins which depend on globals + new webpack.ProvidePlugin({ + $: 'jquery', + jQuery: 'jquery', + }), + // use deterministic module ids in all environments IS_PRODUCTION ? new webpack.HashedModuleIdsPlugin() : From ba865db378b12265442c008a6fc167f2c3c910fb Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 2 Mar 2017 00:35:44 -0600 Subject: [PATCH 28/95] separate webpack runtime into its own chunk to maintain cacheability of common chunk --- app/views/layouts/_head.html.haml | 1 + config/webpack.config.js | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 29844b1027b..8457d3fd0e2 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,6 +28,7 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" + = javascript_include_tag(*webpack_asset_paths("manifest")) = javascript_include_tag(*webpack_asset_paths("common")) = javascript_include_tag(*webpack_asset_paths("application")) diff --git a/config/webpack.config.js b/config/webpack.config.js index a76daa2ba95..f262286f5f8 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -98,10 +98,9 @@ var config = { new webpack.HashedModuleIdsPlugin() : new webpack.NamedModulesPlugin(), - // create a common.js bundle to be loaded on every page + // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ - name: 'common', - minChunks: Infinity, + names: ['application', 'common', 'manifest'], }), ], From 14517f977a55fbe0a2371ebb7d33df7c35415e8d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 2 Mar 2017 00:37:07 -0600 Subject: [PATCH 29/95] don't rely on global spaced Vue library for issuable bundle --- .../issuable/time_tracking/time_tracking_bundle.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 index 958a0cc6d50..0134b7cb6f3 100644 --- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 @@ -1,5 +1,7 @@ /* global Vue */ +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); require('./components/time_tracker'); require('../../smart_interval'); require('../../subbable_resource'); From 7f22c39a25b5424ab1fd7905654667a697c82aeb Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 1 Mar 2017 20:28:34 -0600 Subject: [PATCH 30/95] create a cacheable commons bundle for our Vue bundles --- .../javascripts/lib/vue_resource.js.es6 | 2 -- app/views/projects/boards/_show.html.haml | 1 + .../projects/commit/_pipelines_list.haml | 1 + .../projects/cycle_analytics/show.html.haml | 1 + .../projects/environments/folder.html.haml | 1 + .../projects/environments/index.html.haml | 1 + app/views/projects/issues/show.html.haml | 2 +- .../projects/merge_requests/_show.html.haml | 1 + .../merge_requests/conflicts.html.haml | 2 +- app/views/projects/pipelines/index.html.haml | 1 + app/views/shared/issuable/_sidebar.html.haml | 1 + config/webpack.config.js | 19 ++++++++++++++++++- 12 files changed, 28 insertions(+), 5 deletions(-) delete mode 100644 app/assets/javascripts/lib/vue_resource.js.es6 diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6 deleted file mode 100644 index 49babdea2e1..00000000000 --- a/app/assets/javascripts/lib/vue_resource.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index b3bc6010efb..3ae78387938 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -3,6 +3,7 @@ - page_title "Boards" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('boards') = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test? diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 6792b3f7a83..da5a676274f 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -4,4 +4,5 @@ } } - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('commit_pipelines') diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index be17f2c764e..dd3fa814716 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,6 +1,7 @@ - @no_container = true - page_title "Cycle Analytics" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('cycle_analytics') = render "projects/head" diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index d9cb7bc0331..4b101447bc0 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -3,6 +3,7 @@ = render "projects/pipelines/head" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag("environments_folder") #environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index d6366b57957..80d2b6f5d95 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,6 +3,7 @@ = render "projects/pipelines/head" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag("environments") #environments-list-view{ data: { environments_data: environments_list_data, diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 069f3d97943..394f1999f2c 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -3,7 +3,7 @@ - page_description @issue.description - page_card_attributes @issue.card_attributes - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_vue') + = page_specific_javascript_bundle_tag('common_vue') .clearfix.detail-page-header .issuable-header diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 521b0694ca9..03d618327d4 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -3,6 +3,7 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('diff_notes') .merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) } diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index 1ecd9924d88..51d59280be8 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,6 +1,6 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_vue') + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('merge_conflicts') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index acb61aa2490..5d59ce06612 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -50,4 +50,5 @@ .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } .vue-pipelines-index += page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('vue_pipelines') diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 37a0c63e514..048fc488207 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,5 +1,6 @@ - todo = issuable_todo(issuable) - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('issuable') %aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } diff --git a/config/webpack.config.js b/config/webpack.config.js index f262286f5f8..cdae80550dc 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -18,6 +18,7 @@ var config = { context: path.join(ROOT_PATH, 'app/assets/javascripts'), entry: { common: './commons/index.js', + common_vue: ['vue', 'vue-resource'], application: './application.js', blob_edit: './blob_edit/blob_edit_bundle.js', boards: './boards/boards_bundle.js', @@ -41,7 +42,6 @@ var config = { users: './users/users_bundle.js', lib_chart: './lib/chart.js', lib_d3: './lib/d3.js', - lib_vue: './lib/vue_resource.js', vue_pipelines: './vue_pipelines_index/index.js', }, @@ -98,6 +98,23 @@ var config = { new webpack.HashedModuleIdsPlugin() : new webpack.NamedModulesPlugin(), + // create cacheable common library bundle for all vue chunks + new webpack.optimize.CommonsChunkPlugin({ + name: 'common_vue', + chunks: [ + 'boards', + 'commit_pipelines', + 'cycle_analytics', + 'diff_notes', + 'environments', + 'environments_folder', + 'issuable', + 'merge_conflicts', + 'vue_pipelines', + ], + minChunks: Infinity, + }), + // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ names: ['application', 'common', 'manifest'], From 5f464f98f1ff86caac02eca207704c1961b07be1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 2 Mar 2017 00:58:47 -0600 Subject: [PATCH 31/95] include vue_shared scripts within common_vue chunk --- config/webpack.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index cdae80550dc..904ac36df4c 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -112,7 +112,9 @@ var config = { 'merge_conflicts', 'vue_pipelines', ], - minChunks: Infinity, + minChunks: function(module, count) { + return module.resource && (/vue_shared/).test(module.resource); + }, }), // create cacheable common library bundles From 1f9743310054273b98e423b6dc5e99a5bc9fd7a2 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 2 Mar 2017 01:27:52 -0600 Subject: [PATCH 32/95] merge lib_chart into graphs bundle --- app/assets/javascripts/graphs/graphs_bundle.js | 2 ++ app/assets/javascripts/lib/chart.js | 3 --- app/views/projects/graphs/charts.html.haml | 1 - app/views/projects/graphs/show.html.haml | 1 - app/views/projects/pipelines/charts.html.haml | 1 - config/webpack.config.js | 1 - 6 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 app/assets/javascripts/lib/chart.js diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index ea5afbd9d29..a433c7ba8f0 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,4 +1,6 @@ +import Chart from 'vendor/Chart'; import ContributorsStatGraph from './stat_graph_contributors'; // export to global scope +window.Chart = Chart; window.ContributorsStatGraph = ContributorsStatGraph; diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js deleted file mode 100644 index 9b011d89e93..00000000000 --- a/app/assets/javascripts/lib/chart.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren */ - -window.Chart = require('vendor/Chart'); diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index d3bce45e974..fa28db6c71f 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Charts" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_chart') = page_specific_javascript_bundle_tag('graphs') = render "projects/commits/head" diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index d89dfe31e47..05291c65b50 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Contributors" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_chart') = page_specific_javascript_bundle_tag('graphs') = render 'projects/commits/head' diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index 8ffdfa1a2cf..fbf783afe44 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Charts", "Pipelines" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_chart') = page_specific_javascript_bundle_tag('graphs') = render 'head' diff --git a/config/webpack.config.js b/config/webpack.config.js index 904ac36df4c..243007be4b7 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -40,7 +40,6 @@ var config = { snippet: './snippet/snippet_bundle.js', terminal: './terminal/terminal_bundle.js', users: './users/users_bundle.js', - lib_chart: './lib/chart.js', lib_d3: './lib/d3.js', vue_pipelines: './vue_pipelines_index/index.js', }, From ec9180719ef3d8a3d2aa6bc67b2ec31f91c57613 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 2 Mar 2017 01:34:36 -0600 Subject: [PATCH 33/95] create a cacheable commons bundle for d3 library --- app/assets/javascripts/lib/d3.js | 3 --- app/assets/javascripts/users/calendar.js | 3 ++- app/views/projects/graphs/charts.html.haml | 1 + app/views/projects/graphs/show.html.haml | 1 + app/views/projects/pipelines/charts.html.haml | 1 + app/views/users/show.html.haml | 2 +- config/webpack.config.js | 8 +++++++- 7 files changed, 13 insertions(+), 6 deletions(-) delete mode 100644 app/assets/javascripts/lib/d3.js diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js deleted file mode 100644 index a9dd32edbed..00000000000 --- a/app/assets/javascripts/lib/d3.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren */ - -window.d3 = require('d3'); diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 5111b260e1c..754d448564f 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */ -/* global d3 */ + +import d3 from 'd3'; (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index fa28db6c71f..464ac34d961 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -1,6 +1,7 @@ - @no_container = true - page_title "Charts" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('graphs') = render "projects/commits/head" diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 05291c65b50..680f8ae6c8f 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,6 +1,7 @@ - @no_container = true - page_title "Contributors" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('graphs') = render 'projects/commits/head' diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index fbf783afe44..4a5043aac3c 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -1,6 +1,7 @@ - @no_container = true - page_title "Charts", "Pipelines" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('graphs') = render 'head' diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index af091f9ab88..76cd330e80a 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,7 +1,7 @@ - page_title @user.name - page_description @user.bio - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_d3') + = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('users') - header_title @user.name, user_path(@user) - @no_container = true diff --git a/config/webpack.config.js b/config/webpack.config.js index 243007be4b7..eb453b5997b 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -19,6 +19,7 @@ var config = { entry: { common: './commons/index.js', common_vue: ['vue', 'vue-resource'], + common_d3: ['d3'], application: './application.js', blob_edit: './blob_edit/blob_edit_bundle.js', boards: './boards/boards_bundle.js', @@ -40,7 +41,6 @@ var config = { snippet: './snippet/snippet_bundle.js', terminal: './terminal/terminal_bundle.js', users: './users/users_bundle.js', - lib_d3: './lib/d3.js', vue_pipelines: './vue_pipelines_index/index.js', }, @@ -116,6 +116,12 @@ var config = { }, }), + // create cacheable common library bundle for all d3 chunks + new webpack.optimize.CommonsChunkPlugin({ + name: 'common_d3', + chunks: ['graphs', 'users'], + }), + // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ names: ['application', 'common', 'manifest'], From 59f51cbffd61d798179da60d62406a61f8658957 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 2 Mar 2017 02:11:23 -0600 Subject: [PATCH 34/95] add CHANGELOG.md entry for !9647 --- changelogs/unreleased/commons-chunk-plugin.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/commons-chunk-plugin.yml diff --git a/changelogs/unreleased/commons-chunk-plugin.yml b/changelogs/unreleased/commons-chunk-plugin.yml new file mode 100644 index 00000000000..5c11ea3bbb2 --- /dev/null +++ b/changelogs/unreleased/commons-chunk-plugin.yml @@ -0,0 +1,5 @@ +--- +title: Use webpack CommonsChunkPlugin to place common javascript libraries in their + own bundles +merge_request: 9647 +author: From f5f0be534aafd4a2bf1acd9b9154537343fff66e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 2 Mar 2017 16:35:36 -0600 Subject: [PATCH 35/95] remove problematic plugins from karma's webpack config --- config/karma.config.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/config/karma.config.js b/config/karma.config.js index 2f3cc932413..a23e62f5022 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -1,9 +1,10 @@ var path = require('path'); +var webpack = require('webpack'); var webpackConfig = require('./webpack.config.js'); var ROOT_PATH = path.resolve(__dirname, '..'); // add coverage instrumentation to babel config -if (webpackConfig && webpackConfig.module && webpackConfig.module.rules) { +if (webpackConfig.module && webpackConfig.module.rules) { var babelConfig = webpackConfig.module.rules.find(function (rule) { return rule.loader === 'babel-loader'; }); @@ -13,6 +14,16 @@ if (webpackConfig && webpackConfig.module && webpackConfig.module.rules) { babelConfig.options.plugins.push('istanbul'); } +// remove problematic plugins +if (webpackConfig.plugins) { + webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) { + return !( + plugin instanceof webpack.optimize.CommonsChunkPlugin || + plugin instanceof webpack.DefinePlugin + ); + }); +} + // Karma configuration module.exports = function(config) { var progressReporter = process.env.CI ? 'mocha' : 'progress'; From d0d34786e9bdda51dfecc59a419082db8c523ddc Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 2 Mar 2017 17:00:48 -0600 Subject: [PATCH 36/95] rename application entry point and change manifest to runtime --- app/assets/javascripts/{application.js => main.js} | 0 app/views/layouts/_head.html.haml | 4 ++-- config/webpack.config.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename app/assets/javascripts/{application.js => main.js} (100%) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/main.js similarity index 100% rename from app/assets/javascripts/application.js rename to app/assets/javascripts/main.js diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 8457d3fd0e2..f6d8bb08a64 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,9 +28,9 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" - = javascript_include_tag(*webpack_asset_paths("manifest")) + = javascript_include_tag(*webpack_asset_paths("runtime")) = javascript_include_tag(*webpack_asset_paths("common")) - = javascript_include_tag(*webpack_asset_paths("application")) + = javascript_include_tag(*webpack_asset_paths("main")) - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/config/webpack.config.js b/config/webpack.config.js index eb453b5997b..d9fa70c29fb 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -20,7 +20,7 @@ var config = { common: './commons/index.js', common_vue: ['vue', 'vue-resource'], common_d3: ['d3'], - application: './application.js', + main: './main.js', blob_edit: './blob_edit/blob_edit_bundle.js', boards: './boards/boards_bundle.js', simulate_drag: './test_utils/simulate_drag.js', @@ -47,7 +47,7 @@ var config = { output: { path: path.join(ROOT_PATH, 'public/assets/webpack'), publicPath: '/assets/webpack/', - filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js' + filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js' }, devtool: 'inline-source-map', @@ -124,7 +124,7 @@ var config = { // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ - names: ['application', 'common', 'manifest'], + names: ['main', 'common', 'runtime'], }), ], From ed3e3317de6e386364c1949b939c087b7aed6258 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 2 Mar 2017 17:05:06 -0600 Subject: [PATCH 37/95] remove isolated common_vue inclusion --- app/views/projects/issues/show.html.haml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 394f1999f2c..d39f36e94c7 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -2,8 +2,6 @@ - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') .clearfix.detail-page-header .issuable-header From 982dd5040b3bb2b74534ecddf6fc6c3668ae08c0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 3 Mar 2017 16:54:20 -0600 Subject: [PATCH 38/95] merge cropper library into profile_bundle --- app/assets/javascripts/lib/cropper.js | 7 ------- app/assets/javascripts/profile/gl_crop.js.es6 | 2 ++ app/views/profiles/_head.html.haml | 1 - config/application.rb | 1 - 4 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 app/assets/javascripts/lib/cropper.js diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js deleted file mode 100644 index 7862c6797c3..00000000000 --- a/app/assets/javascripts/lib/cropper.js +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren */ - -/*= require cropper */ - -(function() { - -}).call(window); diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6 index 192b1192d07..cf1566eeb87 100644 --- a/app/assets/javascripts/profile/gl_crop.js.es6 +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -1,5 +1,7 @@ /* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */ +import 'vendor/cropper'; + ((global) => { // Matches everything but the file name const FILENAMEREGEX = /^.*[\\\/]/; diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml index 1df04ea614e..83ae9129807 100644 --- a/app/views/profiles/_head.html.haml +++ b/app/views/profiles/_head.html.haml @@ -1,3 +1,2 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/cropper.js') = page_specific_javascript_bundle_tag('profile') diff --git a/config/application.rb b/config/application.rb index 45f3b20d214..f1a986d1731 100644 --- a/config/application.rb +++ b/config/application.rb @@ -100,7 +100,6 @@ module Gitlab config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" config.assets.precompile << "lib/ace.js" - config.assets.precompile << "lib/cropper.js" config.assets.precompile << "lib/raphael.js" config.assets.precompile << "u2f.js" config.assets.precompile << "vendor/assets/fonts/*" From ffd970d97dc9faf82752e6494aab8ba6fdce759a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 3 Mar 2017 14:12:35 -0800 Subject: [PATCH 39/95] Make SidekiqStatus able to count number of jobs completed/running --- lib/gitlab/sidekiq_status.rb | 35 +++++++++++++++++++++----- spec/lib/gitlab/sidekiq_status_spec.rb | 26 +++++++++++++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb index aadc401ff8d..11e5f1b645c 100644 --- a/lib/gitlab/sidekiq_status.rb +++ b/lib/gitlab/sidekiq_status.rb @@ -44,19 +44,42 @@ module Gitlab # Returns true if all the given job have been completed. # - # jids - The Sidekiq job IDs to check. + # job_ids - The Sidekiq job IDs to check. # # Returns true or false. - def self.all_completed?(jids) - keys = jids.map { |jid| key_for(jid) } + def self.all_completed?(job_ids) + self.num_running(job_ids).zero? + end - responses = Sidekiq.redis do |redis| + # Returns the number of jobs that are running. + # + # job_ids - The Sidekiq job IDs to check. + def self.num_running(job_ids) + responses = self.job_status(job_ids) + + responses.select(&:present?).count + end + + # Returns the number of jobs that have completed. + # + # job_ids - The Sidekiq job IDs to check. + def self.num_completed(job_ids) + job_ids.size - self.num_running(job_ids) + end + + # Returns the job status for each of the given job IDs. + # + # job_ids - The Sidekiq job IDs to check. + # + # Returns an array of true or false indicating job completion. + def self.job_status(job_ids) + keys = job_ids.map { |jid| key_for(jid) } + + Sidekiq.redis do |redis| redis.pipelined do keys.each { |key| redis.exists(key) } end end - - responses.all? { |value| !value } end def self.key_for(jid) diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb index 0aa36a3416b..56f06b61afb 100644 --- a/spec/lib/gitlab/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/sidekiq_status_spec.rb @@ -39,6 +39,32 @@ describe Gitlab::SidekiqStatus do end end + describe '.num_running', :redis do + it 'returns 0 if all jobs have been completed' do + expect(described_class.num_running(%w(123))).to eq(0) + end + + it 'returns 2 if two jobs are still running' do + described_class.set('123') + described_class.set('456') + + expect(described_class.num_running(%w(123 456 789))).to eq(2) + end + end + + describe '.num_completed', :redis do + it 'returns 1 if all jobs have been completed' do + expect(described_class.num_completed(%w(123))).to eq(1) + end + + it 'returns 1 if a job has not yet been completed' do + described_class.set('123') + described_class.set('456') + + expect(described_class.num_completed(%w(123 456 789))).to eq(1) + end + end + describe '.key_for' do it 'returns the key for a job ID' do key = described_class.key_for('123') From 305f19ca266d3494567a885b67dfc13d602cf860 Mon Sep 17 00:00:00 2001 From: Vignesh Ravichandran Date: Fri, 3 Mar 2017 23:28:34 -0800 Subject: [PATCH 40/95] Be able to list issues with no labels using API --- doc/api/issues.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/api/issues.md b/doc/api/issues.md index 51ce08c873e..0f22d0b7f94 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -23,6 +23,7 @@ GET /issues?state=closed GET /issues?labels=foo GET /issues?labels=foo,bar GET /issues?labels=foo,bar&state=opened +GET /projects/:id/issues?labels_name=No+Label GET /issues?milestone=1.0.0 GET /issues?milestone=1.0.0&state=opened GET /issues?iids[]=42&iids[]=43 @@ -32,6 +33,7 @@ GET /issues?iids[]=42&iids[]=43 | --------- | ---- | -------- | ----------- | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | +| `labels_name` | string | no | Return all issues with the mentioned label. `No+Label` lists all issues with no labels | | `milestone` | string| no | The milestone title | | `iids` | Array[integer] | no | Return only the issues having the given `iid` | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | @@ -102,6 +104,7 @@ GET /groups/:id/issues?state=closed GET /groups/:id/issues?labels=foo GET /groups/:id/issues?labels=foo,bar GET /groups/:id/issues?labels=foo,bar&state=opened +GET /projects/:id/issues?labels_name=No+Label GET /groups/:id/issues?milestone=1.0.0 GET /groups/:id/issues?milestone=1.0.0&state=opened GET /groups/:id/issues?iids[]=42&iids[]=43 @@ -112,6 +115,7 @@ GET /groups/:id/issues?iids[]=42&iids[]=43 | `id` | integer | yes | The ID of a group | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | +| `labels_name` | string | no | Return all issues with the mentioned label. `No+Label` lists all issues with no labels | | `iids` | Array[integer] | no | Return only the issues having the given `iid` | | `milestone` | string| no | The milestone title | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | @@ -183,6 +187,7 @@ GET /projects/:id/issues?state=closed GET /projects/:id/issues?labels=foo GET /projects/:id/issues?labels=foo,bar GET /projects/:id/issues?labels=foo,bar&state=opened +GET /projects/:id/issues?labels_name=No+Label GET /projects/:id/issues?milestone=1.0.0 GET /projects/:id/issues?milestone=1.0.0&state=opened GET /projects/:id/issues?iids[]=42&iids[]=43 @@ -194,6 +199,7 @@ GET /projects/:id/issues?iids[]=42&iids[]=43 | `iids` | Array[integer] | no | Return only the milestone having the given `iid` | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | +| `labels_name` | string | no | Return all issues with the mentioned label. `No+Label` lists all issues with no labels | | `milestone` | string| no | The milestone title | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | From 9c01ae091d918b77bcd911477ec5b02945add4fd Mon Sep 17 00:00:00 2001 From: Vignesh Ravichandran Date: Sat, 4 Mar 2017 00:25:15 -0800 Subject: [PATCH 41/95] Adds changelog entry --- changelogs/unreleased/list_issues_with_no_labels.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/list_issues_with_no_labels.yml diff --git a/changelogs/unreleased/list_issues_with_no_labels.yml b/changelogs/unreleased/list_issues_with_no_labels.yml new file mode 100644 index 00000000000..ab44841631b --- /dev/null +++ b/changelogs/unreleased/list_issues_with_no_labels.yml @@ -0,0 +1,4 @@ +--- +title: Document ability to list issues with no labels using API +merge_request: 9697 +author: Vignesh Ravichandran From 8a910ba297b229171a64794b8958401431354b4a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 4 Mar 2017 13:04:00 +0200 Subject: [PATCH 42/95] Improve projects/groups list js code Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/filterable_list.js | 2 -- app/assets/javascripts/groups_list.js | 11 +++++++---- app/assets/javascripts/projects_list.js | 11 +++++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index f498c3ea973..47a40e28461 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -7,8 +7,6 @@ export default class FilterableList { this.filterForm = form; this.listFilterElement = filter; this.listHolderElement = holder; - - this.initSearch(); } initSearch() { diff --git a/app/assets/javascripts/groups_list.js b/app/assets/javascripts/groups_list.js index 49b29affaa5..56a8cbf6d03 100644 --- a/app/assets/javascripts/groups_list.js +++ b/app/assets/javascripts/groups_list.js @@ -6,10 +6,13 @@ import FilterableList from './filterable_list'; */ export default class GroupsList { constructor() { - var form = document.querySelector('form#group-filter-form'); - var filter = document.querySelector('.js-groups-list-filter'); - var holder = document.querySelector('.js-groups-list-holder'); + const form = document.querySelector('form#group-filter-form'); + const filter = document.querySelector('.js-groups-list-filter'); + const holder = document.querySelector('.js-groups-list-holder'); - new FilterableList(form, filter, holder); + if (form && filter && holder) { + const list = new FilterableList(form, filter, holder); + list.initSearch(); + } } } diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index 383c2815457..c67d59d2be5 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -6,10 +6,13 @@ import FilterableList from './filterable_list'; */ export default class ProjectsList { constructor() { - var form = document.querySelector('form#project-filter-form'); - var filter = document.querySelector('.js-projects-list-filter'); - var holder = document.querySelector('.js-projects-list-holder'); + const form = document.querySelector('form#project-filter-form'); + const filter = document.querySelector('.js-projects-list-filter'); + const holder = document.querySelector('.js-projects-list-holder'); - new FilterableList(form, filter, holder); + if (form && filter && holder) { + const list = new FilterableList(form, filter, holder); + list.initSearch(); + } } } From c13c1e1b166edd5520057814dc4698450b578c9a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 4 Mar 2017 15:30:12 +0200 Subject: [PATCH 43/95] Improve explore projects spinach test Project name "Internal" is too generic and can lead to false positive/negative when there is a visibility filter on the page. So we ensure we check for project inside list holder css class. Signed-off-by: Dmitriy Zaporozhets --- features/steps/shared/project.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index dae248b8b7e..345a28f27dc 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -166,11 +166,15 @@ module SharedProject end step 'I should see project "Internal"' do - expect(page).to have_content "Internal" + page.within '.js-projects-list-holder' do + expect(page).to have_content "Internal" + end end step 'I should not see project "Internal"' do - expect(page).not_to have_content "Internal" + page.within '.js-projects-list-holder' do + expect(page).not_to have_content "Internal" + end end step 'public project "Community"' do From 51481534cae759c1bd893e311effc8c346b77aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=83=20Winnie=20=F0=9F=92=83?= Date: Sat, 4 Mar 2017 20:55:01 +0000 Subject: [PATCH 44/95] Re-enable GitLab pages job for GitLab EE --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9006fef6a27..c0eed0f84b0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -421,6 +421,7 @@ pages: - public only: - master@gitlab-org/gitlab-ce + - master@gitlab-org/gitlab-ee # Insurance in case a gem needed by one of our releases gets yanked from # rubygems.org in the future. From c33f09d2549d2228a5ac7ceb7cb099774fbd826e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 18:49:30 +0100 Subject: [PATCH 45/95] Update triggers API --- lib/api/entities.rb | 4 - lib/api/triggers.rb | 68 ++++++++-- lib/api/v3/entities.rb | 8 ++ lib/api/v3/triggers.rb | 77 +++++++++++- spec/requests/api/v3/triggers_spec.rb | 171 ++++++++++++++++++++++++++ 5 files changed, 311 insertions(+), 17 deletions(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d2d21f5d03a..82d05d85ead 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -592,10 +592,6 @@ module API end end - class TriggerRequest < Grape::Entity - expose :id, :variables - end - class Runner < Grape::Entity expose :id expose :description diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index b7c9c5f2b7f..c324708a16d 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -6,7 +6,7 @@ module API requires :id, type: String, desc: 'The ID of a project' end resource :projects do - desc 'Trigger a GitLab project build' do + desc 'Trigger a GitLab project pipeline' do success Entities::TriggerRequest end params do @@ -14,7 +14,7 @@ module API requires :token, type: String, desc: 'The unique token of trigger' optional :variables, type: Hash, desc: 'The list of variables to be injected into build' end - post ":id/(ref/:ref/)trigger/builds" do + post ":id/(ref/:ref/)trigger/pipeline" do project = find_project(params[:id]) trigger = Ci::Trigger.find_by_token(params[:token].to_s) not_found! unless project && trigger @@ -29,9 +29,9 @@ module API # create request and trigger builds trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) if trigger_request - present trigger_request, with: Entities::TriggerRequest + present trigger_request.pipeline, with: Entities::Pipeline else - errors = 'No builds created' + errors = 'No pipeline create' render_api_error!(errors, 400) end end @@ -55,13 +55,13 @@ module API success Entities::Trigger end params do - requires :token, type: String, desc: 'The unique token of trigger' + requires :trigger_id, type: Integer, desc: 'The trigger ID' end - get ':id/triggers/:token' do + get ':id/triggers/:trigger_id' do authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.find_by(token: params[:token].to_s) + trigger = user_project.triggers.find(params[:trigger_id]) return not_found!('Trigger') unless trigger present trigger, with: Entities::Trigger @@ -70,26 +70,72 @@ module API desc 'Create a trigger' do success Entities::Trigger end + params do + requires :description, type: String, desc: 'The trigger description' + end post ':id/triggers' do authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.create + trigger = user_project.triggers.create( + declared_params(include_missing: false).merge(owner: current_user)) + if trigger.valid? + present trigger, with: Entities::Trigger + else + render_validation_error!(trigger) + end + end + + desc 'Update a trigger' do + success Entities::Trigger + end + params do + requires :trigger_id, type: Integer, desc: 'The trigger ID' + optional :description, type: String, desc: 'The trigger description' + end + delete ':id/triggers/:trigger_id' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find(params[:trigger_id]) + return not_found!('Trigger') unless trigger + + trigger = trigger.update(declared_params(include_missing: false)) present trigger, with: Entities::Trigger end + desc 'Take ownership of trigger' do + success Entities::Trigger + end + params do + requires :trigger_id, type: Integer, desc: 'The trigger ID' + end + post ':id/triggers/:trigger_id/take' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find(params[:trigger_id]) + return not_found!('Trigger') unless trigger + + if trigger.update(owner: current_user) + present trigger, with: Entities::Trigger + else + render_validation_error!(trigger) + end + end + desc 'Delete a trigger' do success Entities::Trigger end params do - requires :token, type: String, desc: 'The unique token of trigger' + requires :trigger_id, type: Integer, desc: 'The trigger ID' end - delete ':id/triggers/:token' do + delete ':id/triggers/:trigger_id' do authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.find_by(token: params[:token].to_s) + trigger = user_project.triggers.find(params[:trigger_id]) return not_found!('Trigger') unless trigger trigger.destroy diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 270d99a2348..29a44d4c7e5 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -186,6 +186,14 @@ module API class Environment < ::API::Entities::EnvironmentBasic expose :project, using: Entities::Project end + + class Trigger < Grape::Entity + expose :token, :created_at, :updated_at, :deleted_at, :last_used + end + + class TriggerRequest < Grape::Entity + expose :id, :variables + end end end end diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb index 4051d4bca8d..1dfdb6a5956 100644 --- a/lib/api/v3/triggers.rb +++ b/lib/api/v3/triggers.rb @@ -7,8 +7,81 @@ module API requires :id, type: String, desc: 'The ID of a project' end resource :projects do + desc 'Trigger a GitLab project build' do + success ::API::V3::Entities::TriggerRequest + end + params do + requires :ref, type: String, desc: 'The commit sha or name of a branch or tag' + requires :token, type: String, desc: 'The unique token of trigger' + optional :variables, type: Hash, desc: 'The list of variables to be injected into build' + end + post ":id/(ref/:ref/)trigger/builds" do + project = find_project(params[:id]) + trigger = Ci::Trigger.find_by_token(params[:token].to_s) + not_found! unless project && trigger + unauthorized! unless trigger.project == project + + # validate variables + variables = params[:variables].to_h + unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } + render_api_error!('variables needs to be a map of key-valued strings', 400) + end + + # create request and trigger builds + trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) + if trigger_request + present trigger_request, with: ::API::V3::Entities::TriggerRequest + else + errors = 'No builds created' + render_api_error!(errors, 400) + end + end + + desc 'Get triggers list' do + success ::API::V3::Entities::Trigger + end + params do + use :pagination + end + get ':id/triggers' do + authenticate! + authorize! :admin_build, user_project + + triggers = user_project.triggers.includes(:trigger_requests) + + present paginate(triggers), with: ::API::V3::Entities::Trigger + end + + desc 'Get specific trigger of a project' do + success ::API::V3::Entities::Trigger + end + params do + requires :token, type: String, desc: 'The unique token of trigger' + end + get ':id/triggers/:token' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find_by(token: params[:token].to_s) + return not_found!('Trigger') unless trigger + + present trigger, with: ::API::V3::Entities::Trigger + end + + desc 'Create a trigger' do + success ::API::V3::Entities::Trigger + end + post ':id/triggers' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.create + + present trigger, with: ::API::V3::Entities::Trigger + end + desc 'Delete a trigger' do - success ::API::Entities::Trigger + success ::API::V3::Entities::Trigger end params do requires :token, type: String, desc: 'The unique token of trigger' @@ -22,7 +95,7 @@ module API trigger.destroy - present trigger, with: ::API::Entities::Trigger + present trigger, with: ::API::V3::Entities::Trigger end end end diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb index 721ce4a361b..4819269d69f 100644 --- a/spec/requests/api/v3/triggers_spec.rb +++ b/spec/requests/api/v3/triggers_spec.rb @@ -11,6 +11,177 @@ describe API::V3::Triggers do let!(:developer) { create(:project_member, :developer, user: user2, project: project) } let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) } + describe 'POST /projects/:project_id/trigger' do + let!(:project2) { create(:project) } + let(:options) do + { + token: trigger_token + } + end + + before do + stub_ci_pipeline_to_return_yaml_file + end + + context 'Handles errors' do + it 'returns bad request if token is missing' do + post v3_api("/projects/#{project.id}/trigger/builds"), ref: 'master' + expect(response).to have_http_status(400) + end + + it 'returns not found if project is not found' do + post v3_api('/projects/0/trigger/builds'), options.merge(ref: 'master') + expect(response).to have_http_status(404) + end + + it 'returns unauthorized if token is for different project' do + post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master') + expect(response).to have_http_status(401) + end + end + + context 'Have a commit' do + let(:pipeline) { project.pipelines.last } + + it 'creates builds' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') + expect(response).to have_http_status(201) + pipeline.builds.reload + expect(pipeline.builds.pending.size).to eq(2) + expect(pipeline.builds.size).to eq(5) + end + + it 'creates builds on webhook from other gitlab repository and branch' do + expect do + post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(5) + expect(response).to have_http_status(201) + end + + it 'returns bad request with no builds created if there\'s no commit for that ref' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('No builds created') + end + + context 'Validates variables' do + let(:variables) do + { 'TRIGGER_KEY' => 'TRIGGER_VALUE' } + end + + it 'validates variables to be a hash' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master') + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('variables is invalid') + end + + it 'validates variables needs to be a map of key-valued strings' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master') + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') + end + + it 'creates trigger request with variables' do + post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') + expect(response).to have_http_status(201) + pipeline.builds.reload + expect(pipeline.builds.first.trigger_request.variables).to eq(variables) + end + end + end + end + + describe 'GET /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'returns list of triggers' do + get v3_api("/projects/#{project.id}/triggers", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_a(Array) + expect(json_response[0]).to have_key('token') + end + end + + context 'authenticated user with invalid permissions' do + it 'does not return triggers list' do + get v3_api("/projects/#{project.id}/triggers", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not return triggers list' do + get v3_api("/projects/#{project.id}/triggers") + + expect(response).to have_http_status(401) + end + end + end + + describe 'GET /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'returns trigger details' do + get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a(Hash) + end + + it 'responds with 404 Not Found if requesting non-existing trigger' do + get v3_api("/projects/#{project.id}/triggers/abcdef012345", user) + + expect(response).to have_http_status(404) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not return triggers list' do + get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not return triggers list' do + get v3_api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'creates trigger' do + expect do + post v3_api("/projects/#{project.id}/triggers", user) + end.to change{project.triggers.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response).to be_a(Hash) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not create trigger' do + post v3_api("/projects/#{project.id}/triggers", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not create trigger' do + post v3_api("/projects/#{project.id}/triggers") + + expect(response).to have_http_status(401) + end + end + end + describe 'DELETE /projects/:id/triggers/:token' do context 'authenticated user with valid permissions' do it 'deletes trigger' do From 0a75de2909152351483b317024fcab2e9bb08e16 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 15 Feb 2017 19:36:31 +0100 Subject: [PATCH 46/95] Make Pipeline Triggers to be user aware - they can have owner, - they can be edited, - they have description, - you can take ownership of them --- app/models/ci/trigger.rb | 12 +++++++++--- .../20170215164610_add_owner_id_to_triggers.rb | 10 ++++++++++ .../20170215165036_add_description_to_triggers.rb | 9 +++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20170215164610_add_owner_id_to_triggers.rb create mode 100644 db/migrate/20170215165036_add_description_to_triggers.rb diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 39a1dd86241..f76f06eb9c6 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -5,10 +5,12 @@ module Ci acts_as_paranoid belongs_to :project, foreign_key: :gl_project_id + belongs_to :owner, class_name: "User" + has_many :trigger_requests, dependent: :destroy - validates :token, presence: true - validates :token, uniqueness: true + validates :token, presence: true, uniqueness: true + validates :owner, presence: true before_validation :set_default_values @@ -25,7 +27,11 @@ module Ci end def short_token - token[0...10] + token[0...4] + end + + def can_show_token?(user) + owner.blank? || owner == user end end end diff --git a/db/migrate/20170215164610_add_owner_id_to_triggers.rb b/db/migrate/20170215164610_add_owner_id_to_triggers.rb new file mode 100644 index 00000000000..02c77ee4b5e --- /dev/null +++ b/db/migrate/20170215164610_add_owner_id_to_triggers.rb @@ -0,0 +1,10 @@ +class AddOwnerIdToTriggers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :ci_triggers, :owner_id, :integer + add_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :nullify + end +end diff --git a/db/migrate/20170215165036_add_description_to_triggers.rb b/db/migrate/20170215165036_add_description_to_triggers.rb new file mode 100644 index 00000000000..1dca0e37412 --- /dev/null +++ b/db/migrate/20170215165036_add_description_to_triggers.rb @@ -0,0 +1,9 @@ +class AddDescriptionToTriggers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :ci_triggers, :description, :string + end +end From 91ce04678ea78f2042026e8584e5d1069853dbc4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 18:23:42 +0100 Subject: [PATCH 47/95] Make triggers to be user aware --- app/models/user.rb | 1 + .../ci/create_trigger_request_service.rb | 2 +- .../20170215164610_add_owner_id_to_triggers.rb | 2 +- spec/models/ci/trigger_spec.rb | 14 +++++++++++--- spec/models/user_spec.rb | 1 + .../ci/create_trigger_request_service_spec.rb | 18 ++++++++++++++++-- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index d3bb04060bf..dfba51d3b00 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -95,6 +95,7 @@ class User < ActiveRecord::Base has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy has_many :award_emoji, dependent: :destroy + has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 6af3c1ca5b1..dca5aa9f5d7 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -3,7 +3,7 @@ module Ci def execute(project, trigger, ref, variables = nil) trigger_request = trigger.trigger_requests.create(variables: variables) - pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref). + pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref). execute(ignore_skip_ci: true, trigger_request: trigger_request) if pipeline.persisted? trigger_request diff --git a/db/migrate/20170215164610_add_owner_id_to_triggers.rb b/db/migrate/20170215164610_add_owner_id_to_triggers.rb index 02c77ee4b5e..845c89c703e 100644 --- a/db/migrate/20170215164610_add_owner_id_to_triggers.rb +++ b/db/migrate/20170215164610_add_owner_id_to_triggers.rb @@ -5,6 +5,6 @@ class AddOwnerIdToTriggers < ActiveRecord::Migration def change add_column :ci_triggers, :owner_id, :integer - add_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :nullify + add_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade end end diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index 3ca9231f58e..074cf1a0bd7 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -1,16 +1,24 @@ require 'spec_helper' describe Ci::Trigger, models: true do - let(:project) { FactoryGirl.create :empty_project } + let(:project) { create :empty_project } + + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:owner) } + it { is_expected.to have_many(:trigger_requests) } + end describe 'before_validation' do it 'sets an random token if none provided' do - trigger = FactoryGirl.create :ci_trigger_without_token, project: project + trigger = create(:ci_trigger_without_token, project: project) + expect(trigger.token).not_to be_nil end it 'does not set an random token if one provided' do - trigger = FactoryGirl.create :ci_trigger, project: project + trigger = create(:ci_trigger, project: project) + expect(trigger.token).to eq('token') end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e86b4a761d9..b99cde64675 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -32,6 +32,7 @@ describe User, models: true do it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:award_emoji).dependent(:destroy) } + it { is_expected.to have_many(:triggers).dependent(:destroy) } it { is_expected.to have_many(:builds).dependent(:nullify) } it { is_expected.to have_many(:pipelines).dependent(:nullify) } it { is_expected.to have_many(:chat_names).dependent(:destroy) } diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index d8c443d29d5..5e68343784d 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -13,8 +13,22 @@ describe Ci::CreateTriggerRequestService, services: true do context 'valid params' do subject { service.execute(project, trigger, 'master') } - it { expect(subject).to be_kind_of(Ci::TriggerRequest) } - it { expect(subject.builds.first).to be_kind_of(Ci::Build) } + context 'without owner' do + it { expect(subject).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(subject.builds.first).to be_kind_of(Ci::Build) } + end + + context 'with owner' do + let(:owner) { create(:user) } + let(:trigger) { create(:ci_trigger, project: project, owner: owner) } + + it { expect(subject).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(subject.pipeline.user).to eq(owner) } + it { expect(subject.builds.first).to be_kind_of(Ci::Build) } + it { expect(subject.builds.first.user).to eq(owner) } + end end context 'no commit for ref' do From 4ac1467c5ce969e082d96e7b2a60ae8e1654c881 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 18:28:32 +0100 Subject: [PATCH 48/95] Update db/schema --- ...ers.rb => 20170217151948_add_owner_id_to_triggers.rb} | 0 ....rb => 20170217151949_add_description_to_triggers.rb} | 0 db/schema.rb | 9 ++++++--- 3 files changed, 6 insertions(+), 3 deletions(-) rename db/migrate/{20170215164610_add_owner_id_to_triggers.rb => 20170217151948_add_owner_id_to_triggers.rb} (100%) rename db/migrate/{20170215165036_add_description_to_triggers.rb => 20170217151949_add_description_to_triggers.rb} (100%) diff --git a/db/migrate/20170215164610_add_owner_id_to_triggers.rb b/db/migrate/20170217151948_add_owner_id_to_triggers.rb similarity index 100% rename from db/migrate/20170215164610_add_owner_id_to_triggers.rb rename to db/migrate/20170217151948_add_owner_id_to_triggers.rb diff --git a/db/migrate/20170215165036_add_description_to_triggers.rb b/db/migrate/20170217151949_add_description_to_triggers.rb similarity index 100% rename from db/migrate/20170215165036_add_description_to_triggers.rb rename to db/migrate/20170217151949_add_description_to_triggers.rb diff --git a/db/schema.rb b/db/schema.rb index cd5aa339269..75daae86a4a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170217151947) do +ActiveRecord::Schema.define(version: 20170217151949) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -111,7 +111,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do t.boolean "plantuml_enabled" t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false - t.string "default_artifacts_expire_in", default: '0', null: false + t.string "default_artifacts_expire_in", default: "0", null: false end create_table "audit_events", force: :cascade do |t| @@ -377,6 +377,8 @@ ActiveRecord::Schema.define(version: 20170217151947) do t.datetime "created_at" t.datetime "updated_at" t.integer "gl_project_id" + t.integer "owner_id" + t.string "description" end add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree @@ -581,9 +583,9 @@ ActiveRecord::Schema.define(version: 20170217151947) do end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree - add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["title"], name: "index_labels_on_title", using: :btree + add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false @@ -1333,6 +1335,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "boards", "projects" + add_foreign_key "ci_triggers", "users", column: "owner_id", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade From ab972295bc0ec6db90ae451904a867788d66f49d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 18:30:58 +0100 Subject: [PATCH 49/95] Fix trigger model --- app/models/ci/trigger.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index f76f06eb9c6..8aa45b2f02e 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -10,7 +10,6 @@ module Ci has_many :trigger_requests, dependent: :destroy validates :token, presence: true, uniqueness: true - validates :owner, presence: true before_validation :set_default_values From 4cd2ab52548e89cd7259cfb7ce320fdfa203fe84 Mon Sep 17 00:00:00 2001 From: winniehell Date: Tue, 14 Feb 2017 20:10:57 +0100 Subject: [PATCH 50/95] Adjust ESLint rule for file names --- .eslintrc | 2 +- changelogs/unreleased/remove-es6-extension.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/remove-es6-extension.yml diff --git a/.eslintrc b/.eslintrc index 0fcd866778f..b0ae2a31919 100644 --- a/.eslintrc +++ b/.eslintrc @@ -23,7 +23,7 @@ } }, "rules": { - "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"], + "filenames/match-regex": [2, "^[a-z0-9_]+$"], "no-multiple-empty-lines": ["error", { "max": 1 }] } } diff --git a/changelogs/unreleased/remove-es6-extension.yml b/changelogs/unreleased/remove-es6-extension.yml new file mode 100644 index 00000000000..65f4a7a7867 --- /dev/null +++ b/changelogs/unreleased/remove-es6-extension.yml @@ -0,0 +1,4 @@ +--- +title: Remove es6 file extension from JavaScript files +merge_request: 9241 +author: winniehell From 140b51ce980bc519f3478bf4321dfd4a35d6bd3c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 20:58:08 +0100 Subject: [PATCH 51/95] Introduce tests for pipeline triggers --- app/helpers/triggers_helper.rb | 4 +- .../introduce-pipeline-triggers.yml | 4 + doc/ci/triggers/README.md | 16 +- lib/api/entities.rb | 4 +- lib/api/triggers.rb | 22 +-- spec/requests/api/triggers_spec.rb | 149 ++++++++++++++---- 6 files changed, 146 insertions(+), 53 deletions(-) create mode 100644 changelogs/unreleased/introduce-pipeline-triggers.yml diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb index b0135ea2e95..a48d4475e97 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/triggers_helper.rb @@ -1,9 +1,9 @@ module TriggersHelper def builds_trigger_url(project_id, ref: nil) if ref.nil? - "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds" + "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/trigger/pipeline" else - "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds" + "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/ref/#{ref}/trigger/pipeline" end end diff --git a/changelogs/unreleased/introduce-pipeline-triggers.yml b/changelogs/unreleased/introduce-pipeline-triggers.yml new file mode 100644 index 00000000000..ce5a230d48f --- /dev/null +++ b/changelogs/unreleased/introduce-pipeline-triggers.yml @@ -0,0 +1,4 @@ +--- +title: Introduce Pipeline Triggers that are user-aware +merge_request: +author: diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 1ad9621c8a0..ccaee33dc92 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -36,7 +36,7 @@ it will not trigger a job. To trigger a job you need to send a `POST` request to GitLab's API endpoint: ``` -POST /projects/:id/trigger/builds +POST /projects/:id/trigger/pipeline ``` The required parameters are the trigger's `token` and the Git `ref` on which @@ -71,7 +71,7 @@ To trigger a job from webhook of another project you need to add the following webhook url for Push and Tag push events: ``` -https://gitlab.example.com/api/v4/projects/:id/ref/:ref/trigger/builds?token=TOKEN +https://gitlab.example.com/api/v4/projects/:id/ref/:ref/trigger/pipeline?token=TOKEN ``` > **Note**: @@ -105,7 +105,7 @@ Using cURL you can trigger a rebuild with minimal effort, for example: curl --request POST \ --form token=TOKEN \ --form ref=master \ - https://gitlab.example.com/api/v4/projects/9/trigger/builds + https://gitlab.example.com/api/v4/projects/9/trigger/pipeline ``` In this case, the project with ID `9` will get rebuilt on `master` branch. @@ -114,7 +114,7 @@ Alternatively, you can pass the `token` and `ref` arguments in the query string: ```bash curl --request POST \ - "https://gitlab.example.com/api/v4/projects/9/trigger/builds?token=TOKEN&ref=master" + "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline?token=TOKEN&ref=master" ``` ### Triggering a job within `.gitlab-ci.yml` @@ -128,7 +128,7 @@ need to add in project's A `.gitlab-ci.yml`: build_docs: stage: deploy script: - - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/builds" + - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline" only: - tags ``` @@ -187,7 +187,7 @@ curl --request POST \ --form token=TOKEN \ --form ref=master \ --form "variables[UPLOAD_TO_S3]=true" \ - https://gitlab.example.com/api/v4/projects/9/trigger/builds + https://gitlab.example.com/api/v4/projects/9/trigger/pipeline ``` ### Using webhook to trigger job @@ -195,7 +195,7 @@ curl --request POST \ You can add the following webhook to another project in order to trigger a job: ``` -https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true +https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN&variables[UPLOAD_TO_S3]=true ``` ### Using cron to trigger nightly jobs @@ -205,7 +205,7 @@ in conjunction with cron. The example below triggers a job on the `master` branch of project with ID `9` every night at `00:30`: ```bash -30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/builds +30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline ``` [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229 diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 82d05d85ead..c0c94044ced 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -639,7 +639,9 @@ module API end class Trigger < Grape::Entity - expose :token, :created_at, :updated_at, :deleted_at, :last_used + expose :token, :description + expose :created_at, :updated_at, :deleted_at, :last_used + expose :owner, using: Entities::UserBasic end class Variable < Grape::Entity diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index c324708a16d..157f3cef1fd 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -7,7 +7,7 @@ module API end resource :projects do desc 'Trigger a GitLab project pipeline' do - success Entities::TriggerRequest + success Entities::Pipeline end params do requires :ref, type: String, desc: 'The commit sha or name of a branch or tag' @@ -31,7 +31,7 @@ module API if trigger_request present trigger_request.pipeline, with: Entities::Pipeline else - errors = 'No pipeline create' + errors = 'No pipeline created' render_api_error!(errors, 400) end end @@ -61,7 +61,7 @@ module API authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.find(params[:trigger_id]) + trigger = user_project.triggers.find(params.delete(:trigger_id)) return not_found!('Trigger') unless trigger present trigger, with: Entities::Trigger @@ -94,15 +94,18 @@ module API requires :trigger_id, type: Integer, desc: 'The trigger ID' optional :description, type: String, desc: 'The trigger description' end - delete ':id/triggers/:trigger_id' do + put ':id/triggers/:trigger_id' do authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.find(params[:trigger_id]) + trigger = user_project.triggers.find(params.delete(:trigger_id)) return not_found!('Trigger') unless trigger - trigger = trigger.update(declared_params(include_missing: false)) - present trigger, with: Entities::Trigger + if trigger.update(declared_params(include_missing: false)) + present trigger, with: Entities::Trigger + else + render_validation_error!(trigger) + end end desc 'Take ownership of trigger' do @@ -115,10 +118,11 @@ module API authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.find(params[:trigger_id]) + trigger = user_project.triggers.find(params.delete(:trigger_id)) return not_found!('Trigger') unless trigger if trigger.update(owner: current_user) + status :ok present trigger, with: Entities::Trigger else render_validation_error!(trigger) @@ -135,7 +139,7 @@ module API authenticate! authorize! :admin_build, user_project - trigger = user_project.triggers.find(params[:trigger_id]) + trigger = user_project.triggers.find(params.delete(:trigger_id)) return not_found!('Trigger') unless trigger trigger.destroy diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 153e2791cbe..f2effd71755 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -14,7 +14,7 @@ describe API::Triggers do let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) } let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') } - describe 'POST /projects/:project_id/trigger' do + describe 'POST /projects/:project_id/trigger/pipeline' do let!(:project2) { create(:project) } let(:options) do { @@ -28,17 +28,20 @@ describe API::Triggers do context 'Handles errors' do it 'returns bad request if token is missing' do - post api("/projects/#{project.id}/trigger/builds"), ref: 'master' + post api("/projects/#{project.id}/trigger/pipeline"), ref: 'master' + expect(response).to have_http_status(400) end it 'returns not found if project is not found' do - post api('/projects/0/trigger/builds'), options.merge(ref: 'master') + post api('/projects/0/trigger/pipeline'), options.merge(ref: 'master') + expect(response).to have_http_status(404) end it 'returns unauthorized if token is for different project' do - post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master') + post api("/projects/#{project2.id}/trigger/pipeline"), options.merge(ref: 'master') + expect(response).to have_http_status(401) end end @@ -46,9 +49,11 @@ describe API::Triggers do context 'Have a commit' do let(:pipeline) { project.pipelines.last } - it 'creates builds' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') + it 'creates pipeline' do + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'master') + expect(response).to have_http_status(201) + expect(json_response).to include('id' => pipeline.id) pipeline.builds.reload expect(pipeline.builds.pending.size).to eq(2) expect(pipeline.builds.size).to eq(5) @@ -56,15 +61,17 @@ describe API::Triggers do it 'creates builds on webhook from other gitlab repository and branch' do expect do - post api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } end.to change(project.builds, :count).by(5) + expect(response).to have_http_status(201) end - it 'returns bad request with no builds created if there\'s no commit for that ref' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') + it 'returns bad request with no pipeline created if there\'s no commit for that ref' do + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch') + expect(response).to have_http_status(400) - expect(json_response['message']).to eq('No builds created') + expect(json_response['message']).to eq('No pipeline created') end context 'Validates variables' do @@ -73,22 +80,24 @@ describe API::Triggers do end it 'validates variables to be a hash' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master') + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: 'value', ref: 'master') + expect(response).to have_http_status(400) expect(json_response['error']).to eq('variables is invalid') end it 'validates variables needs to be a map of key-valued strings' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master') + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: { key: %w(1 2) }, ref: 'master') + expect(response).to have_http_status(400) expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') end it 'creates trigger request with variables' do - post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') + post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: variables, ref: 'master') + expect(response).to have_http_status(201) - pipeline.builds.reload - expect(pipeline.builds.first.trigger_request.variables).to eq(variables) + expect(pipeline.builds.reload.first.trigger_request.variables).to eq(variables) end end end @@ -123,17 +132,17 @@ describe API::Triggers do end end - describe 'GET /projects/:id/triggers/:token' do + describe 'GET /projects/:id/triggers/:trigger_id' do context 'authenticated user with valid permissions' do it 'returns trigger details' do - get api("/projects/#{project.id}/triggers/#{trigger.token}", user) + get api("/projects/#{project.id}/triggers/#{trigger.id}", user) expect(response).to have_http_status(200) expect(json_response).to be_a(Hash) end it 'responds with 404 Not Found if requesting non-existing trigger' do - get api("/projects/#{project.id}/triggers/abcdef012345", user) + get api("/projects/#{project.id}/triggers/-5", user) expect(response).to have_http_status(404) end @@ -141,7 +150,7 @@ describe API::Triggers do context 'authenticated user with invalid permissions' do it 'does not return triggers list' do - get api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + get api("/projects/#{project.id}/triggers/#{trigger.id}", user2) expect(response).to have_http_status(403) end @@ -149,7 +158,7 @@ describe API::Triggers do context 'unauthenticated user' do it 'does not return triggers list' do - get api("/projects/#{project.id}/triggers/#{trigger.token}") + get api("/projects/#{project.id}/triggers/#{trigger.id}") expect(response).to have_http_status(401) end @@ -158,19 +167,31 @@ describe API::Triggers do describe 'POST /projects/:id/triggers' do context 'authenticated user with valid permissions' do - it 'creates trigger' do - expect do - post api("/projects/#{project.id}/triggers", user) - end.to change{project.triggers.count}.by(1) + context 'with required parameters' do + it 'creates trigger' do + expect do + post api("/projects/#{project.id}/triggers", user), + description: 'trigger' + end.to change{project.triggers.count}.by(1) - expect(response).to have_http_status(201) - expect(json_response).to be_a(Hash) + expect(response).to have_http_status(201) + expect(json_response).to include('description' => 'trigger') + end + end + + context 'without required parameters' do + it 'creates trigger' do + post api("/projects/#{project.id}/triggers", user) + + expect(response).to have_http_status(:bad_request) + end end end context 'authenticated user with invalid permissions' do it 'does not create trigger' do - post api("/projects/#{project.id}/triggers", user2) + post api("/projects/#{project.id}/triggers", user2), + description: 'trigger' expect(response).to have_http_status(403) end @@ -178,25 +199,87 @@ describe API::Triggers do context 'unauthenticated user' do it 'does not create trigger' do - post api("/projects/#{project.id}/triggers") + post api("/projects/#{project.id}/triggers"), + description: 'trigger' expect(response).to have_http_status(401) end end end - describe 'DELETE /projects/:id/triggers/:token' do + describe 'PUT /projects/:id/triggers/:trigger_id' do + context 'authenticated user with valid permissions' do + let(:new_description) { 'new description' } + + it 'updates description' do + put api("/projects/#{project.id}/triggers/#{trigger.id}", user), + description: new_description + + expect(response).to have_http_status(200) + expect(json_response).to include('description' => new_description) + expect(trigger.reload.description).to eq(new_description) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update trigger' do + put api("/projects/#{project.id}/triggers/#{trigger.id}", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not update trigger' do + put api("/projects/#{project.id}/triggers/#{trigger.id}") + + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/triggers/:trigger_id/take' do + context 'authenticated user with valid permissions' do + it 'updates owner' do + expect(trigger.owner).to be_nil + + post api("/projects/#{project.id}/triggers/#{trigger.id}/take", user) + + expect(response).to have_http_status(200) + expect(json_response).to include('owner') + expect(trigger.reload.owner).to eq(user) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update owner' do + post api("/projects/#{project.id}/triggers/#{trigger.id}/take", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not update owner' do + post api("/projects/#{project.id}/triggers/#{trigger.id}/take") + + expect(response).to have_http_status(401) + end + end + end + + describe 'DELETE /projects/:id/triggers/:trigger_id' do context 'authenticated user with valid permissions' do it 'deletes trigger' do expect do - delete api("/projects/#{project.id}/triggers/#{trigger.token}", user) + delete api("/projects/#{project.id}/triggers/#{trigger.id}", user) expect(response).to have_http_status(204) end.to change{project.triggers.count}.by(-1) end it 'responds with 404 Not Found if requesting non-existing trigger' do - delete api("/projects/#{project.id}/triggers/abcdef012345", user) + delete api("/projects/#{project.id}/triggers/-5", user) expect(response).to have_http_status(404) end @@ -204,7 +287,7 @@ describe API::Triggers do context 'authenticated user with invalid permissions' do it 'does not delete trigger' do - delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + delete api("/projects/#{project.id}/triggers/#{trigger.id}", user2) expect(response).to have_http_status(403) end @@ -212,7 +295,7 @@ describe API::Triggers do context 'unauthenticated user' do it 'does not delete trigger' do - delete api("/projects/#{project.id}/triggers/#{trigger.token}") + delete api("/projects/#{project.id}/triggers/#{trigger.id}") expect(response).to have_http_status(401) end From 307d12526bf7209569411995f9b4422a1ee8fff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=92=83=20Winnie=20=F0=9F=92=83?= Date: Sun, 5 Mar 2017 20:08:10 +0000 Subject: [PATCH 52/95] Enable build job for caching Gems of GitLab EE --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c0eed0f84b0..deeb01f9a3c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -438,3 +438,4 @@ cache gems: - vendor/cache only: - master@gitlab-org/gitlab-ce + - master@gitlab-org/gitlab-ee From b565ee4912d742ef01d10e9a6fae64fe79d6b7bf Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 21:18:00 +0100 Subject: [PATCH 53/95] Update documentation and expose ID --- doc/api/README.md | 2 +- doc/api/build_triggers.md | 108 ---------------------- doc/api/pipeline_triggers.md | 170 +++++++++++++++++++++++++++++++++++ lib/api/entities.rb | 1 + 4 files changed, 172 insertions(+), 109 deletions(-) delete mode 100644 doc/api/build_triggers.md create mode 100644 doc/api/pipeline_triggers.md diff --git a/doc/api/README.md b/doc/api/README.md index 3399e2bb5f6..285cd2435ac 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -12,7 +12,6 @@ following locations: - [Branches](branches.md) - [Broadcast Messages](broadcast_messages.md) - [Builds](builds.md) -- [Build Triggers](build_triggers.md) - [Build Variables](build_variables.md) - [Commits](commits.md) - [Deployments](deployments.md) @@ -33,6 +32,7 @@ following locations: - [Notes](notes.md) (comments) - [Notification settings](notification_settings.md) - [Pipelines](pipelines.md) +- [Pipeline Triggers](pipeline_triggers.md) - [Projects](projects.md) including setting Webhooks - [Project Access Requests](access_requests.md) - [Project Members](members.md) diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md deleted file mode 100644 index 28befba69d6..00000000000 --- a/doc/api/build_triggers.md +++ /dev/null @@ -1,108 +0,0 @@ -# Build triggers - -You can read more about [triggering builds through the API](../ci/triggers/README.md). - -## List project triggers - -Get a list of project's build triggers. - -``` -GET /projects/:id/triggers -``` - -| Attribute | Type | required | Description | -|-----------|---------|----------|---------------------| -| `id` | integer | yes | The ID of a project | - -``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers" -``` - -```json -[ - { - "created_at": "2015-12-23T16:24:34.716Z", - "deleted_at": null, - "last_used": "2016-01-04T15:41:21.986Z", - "token": "fbdb730c2fbdb095a0862dbd8ab88b", - "updated_at": "2015-12-23T16:24:34.716Z" - }, - { - "created_at": "2015-12-23T16:25:56.760Z", - "deleted_at": null, - "last_used": null, - "token": "7b9148c158980bbd9bcea92c17522d", - "updated_at": "2015-12-23T16:25:56.760Z" - } -] -``` - -## Get trigger details - -Get details of project's build trigger. - -``` -GET /projects/:id/triggers/:token -``` - -| Attribute | Type | required | Description | -|-----------|---------|----------|--------------------------| -| `id` | integer | yes | The ID of a project | -| `token` | string | yes | The `token` of a trigger | - -``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" -``` - -```json -{ - "created_at": "2015-12-23T16:25:56.760Z", - "deleted_at": null, - "last_used": null, - "token": "7b9148c158980bbd9bcea92c17522d", - "updated_at": "2015-12-23T16:25:56.760Z" -} -``` - -## Create a project trigger - -Create a build trigger for a project. - -``` -POST /projects/:id/triggers -``` - -| Attribute | Type | required | Description | -|-----------|---------|----------|--------------------------| -| `id` | integer | yes | The ID of a project | - -``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers" -``` - -```json -{ - "created_at": "2016-01-07T09:53:58.235Z", - "deleted_at": null, - "last_used": null, - "token": "6d056f63e50fe6f8c5f8f4aa10edb7", - "updated_at": "2016-01-07T09:53:58.235Z" -} -``` - -## Remove a project trigger - -Remove a project's build trigger. - -``` -DELETE /projects/:id/triggers/:token -``` - -| Attribute | Type | required | Description | -|-----------|---------|----------|--------------------------| -| `id` | integer | yes | The ID of a project | -| `token` | string | yes | The `token` of a trigger | - -``` -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" -``` diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md new file mode 100644 index 00000000000..aebc84b9a66 --- /dev/null +++ b/doc/api/pipeline_triggers.md @@ -0,0 +1,170 @@ +# Pipeline triggers + +You can read more about [triggering pipelines through the API](../ci/triggers/README.md). + +## List project triggers + +Get a list of project's build triggers. + +``` +GET /projects/:id/triggers +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | + +``` +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers" +``` + +```json +[ + { + "id": 10, + "description": "my trigger", + "created_at": "2016-01-07T09:53:58.235Z", + "deleted_at": null, + "last_used": null, + "token": "6d056f63e50fe6f8c5f8f4aa10edb7", + "updated_at": "2016-01-07T09:53:58.235Z", + "owner": null + } +] +``` + +## Get trigger details + +Get details of project's build trigger. + +``` +GET /projects/:id/triggers/:trigger_id +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|--------------------------| +| `id` | integer | yes | The ID of a project | +| `token` | string | yes | The `token` of a trigger | + +``` +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5" +``` + +```json +{ + "id": 10, + "description": "my trigger", + "created_at": "2016-01-07T09:53:58.235Z", + "deleted_at": null, + "last_used": null, + "token": "6d056f63e50fe6f8c5f8f4aa10edb7", + "updated_at": "2016-01-07T09:53:58.235Z", + "owner": null +} +``` + +## Create a project trigger + +Create a trigger for a project. + +``` +POST /projects/:id/triggers +``` + +| Attribute | Type | required | Description | +|---------------|---------|----------|--------------------------| +| `id` | integer | yes | The ID of a project | +| `description` | string | yes | The trigger name | + +``` +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers" +``` + +```json +{ + "id": 10, + "description": "my trigger", + "created_at": "2016-01-07T09:53:58.235Z", + "deleted_at": null, + "last_used": null, + "token": "6d056f63e50fe6f8c5f8f4aa10edb7", + "updated_at": "2016-01-07T09:53:58.235Z", + "owner": null +} +``` + +## Update a project trigger + +Update a trigger for a project. + +``` +PUT /projects/:id/triggers/:trigger_id +``` + +| Attribute | Type | required | Description | +|---------------|---------|----------|--------------------------| +| `trigger_id` | integer | yes | The trigger id | +| `description` | string | no | The trigger name | + +``` +curl --request PUT -F description="my description" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/10" +``` + +```json +{ + "id": 10, + "description": "my trigger", + "created_at": "2016-01-07T09:53:58.235Z", + "deleted_at": null, + "last_used": null, + "token": "6d056f63e50fe6f8c5f8f4aa10edb7", + "updated_at": "2016-01-07T09:53:58.235Z", + "owner": null +} +``` + +## Take ownership of a project trigger + +Update an owner of a project trigger. + +``` +POST /projects/:id/triggers/:trigger_id/take +``` + +| Attribute | Type | required | Description | +|---------------|---------|----------|--------------------------| +| `trigger_id` | integer | yes | The trigger id | + +``` +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/10/take" +``` + +```json +{ + "id": 10, + "description": "my trigger", + "created_at": "2016-01-07T09:53:58.235Z", + "deleted_at": null, + "last_used": null, + "token": "6d056f63e50fe6f8c5f8f4aa10edb7", + "updated_at": "2016-01-07T09:53:58.235Z", + "owner": null +} +``` + +## Remove a project trigger + +Remove a project's build trigger. + +``` +DELETE /projects/:id/triggers/:trigger_id +``` + +| Attribute | Type | required | Description | +|----------------|---------|----------|--------------------------| +| `id` | integer | yes | The ID of a project | +| `trigger_id` | integer | yes | The trigger id | + +``` +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5" +``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c0c94044ced..98ef9d4118e 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -639,6 +639,7 @@ module API end class Trigger < Grape::Entity + expose :id expose :token, :description expose :created_at, :updated_at, :deleted_at, :last_used expose :owner, using: Entities::UserBasic From 572f9782d5e8d6307784b61db0dfce48f5118445 Mon Sep 17 00:00:00 2001 From: winniehell Date: Sun, 5 Mar 2017 20:43:05 +0100 Subject: [PATCH 54/95] Remove .es6 from file extensions (!9241) --- app/assets/javascripts/{abuse_reports.js.es6 => abuse_reports.js} | 0 app/assets/javascripts/{activities.js.es6 => activities.js} | 0 .../javascripts/blob/{blob_ci_yaml.js.es6 => blob_ci_yaml.js} | 0 ...lob_dockerfile_selector.js.es6 => blob_dockerfile_selector.js} | 0 ...b_dockerfile_selectors.js.es6 => blob_dockerfile_selectors.js} | 0 .../{blob_license_selectors.js.es6 => blob_license_selectors.js} | 0 .../blob/{template_selector.js.es6 => template_selector.js} | 0 .../javascripts/boards/{boards_bundle.js.es6 => boards_bundle.js} | 0 .../javascripts/boards/components/{board.js.es6 => board.js} | 0 .../components/{board_blank_state.js.es6 => board_blank_state.js} | 0 .../boards/components/{board_delete.js.es6 => board_delete.js} | 0 .../boards/components/{board_list.js.es6 => board_list.js} | 0 .../boards/components/{board_sidebar.js.es6 => board_sidebar.js} | 0 .../components/{issue_card_inner.js.es6 => issue_card_inner.js} | 0 .../components/modal/{empty_state.js.es6 => empty_state.js} | 0 .../boards/components/modal/{filters.js.es6 => filters.js} | 0 .../boards/components/modal/filters/{label.js.es6 => label.js} | 0 .../components/modal/filters/{milestone.js.es6 => milestone.js} | 0 .../boards/components/modal/filters/{user.js.es6 => user.js} | 0 .../boards/components/modal/{footer.js.es6 => footer.js} | 0 .../boards/components/modal/{header.js.es6 => header.js} | 0 .../boards/components/modal/{index.js.es6 => index.js} | 0 .../javascripts/boards/components/modal/{list.js.es6 => list.js} | 0 .../components/modal/{lists_dropdown.js.es6 => lists_dropdown.js} | 0 .../javascripts/boards/components/modal/{tabs.js.es6 => tabs.js} | 0 .../components/{new_list_dropdown.js.es6 => new_list_dropdown.js} | 0 .../components/sidebar/{remove_issue.js.es6 => remove_issue.js} | 0 .../filters/{due_date_filters.js.es6 => due_date_filters.js} | 0 .../boards/mixins/{modal_mixins.js.es6 => modal_mixins.js} | 0 ...ortable_default_options.js.es6 => sortable_default_options.js} | 0 app/assets/javascripts/boards/models/{issue.js.es6 => issue.js} | 0 app/assets/javascripts/boards/models/{label.js.es6 => label.js} | 0 app/assets/javascripts/boards/models/{list.js.es6 => list.js} | 0 .../javascripts/boards/models/{milestone.js.es6 => milestone.js} | 0 app/assets/javascripts/boards/models/{user.js.es6 => user.js} | 0 .../boards/services/{board_service.js.es6 => board_service.js} | 0 .../boards/stores/{boards_store.js.es6 => boards_store.js} | 0 .../boards/stores/{modal_store.js.es6 => modal_store.js} | 0 .../javascripts/{build_variables.js.es6 => build_variables.js} | 0 .../javascripts/{ci_lint_editor.js.es6 => ci_lint_editor.js} | 0 .../pipelines/{pipelines_bundle.js.es6 => pipelines_bundle.js} | 0 .../pipelines/{pipelines_service.js.es6 => pipelines_service.js} | 0 .../pipelines/{pipelines_store.js.es6 => pipelines_store.js} | 0 .../pipelines/{pipelines_table.js.es6 => pipelines_table.js} | 0 .../{compare_autocomplete.js.es6 => compare_autocomplete.js} | 0 app/assets/javascripts/{copy_as_gfm.js.es6 => copy_as_gfm.js} | 0 app/assets/javascripts/{create_label.js.es6 => create_label.js} | 0 .../{stage_code_component.js.es6 => stage_code_component.js} | 0 .../{stage_issue_component.js.es6 => stage_issue_component.js} | 0 .../{stage_plan_component.js.es6 => stage_plan_component.js} | 0 ..._production_component.js.es6 => stage_production_component.js} | 0 .../{stage_review_component.js.es6 => stage_review_component.js} | 0 ...{stage_staging_component.js.es6 => stage_staging_component.js} | 0 .../{stage_test_component.js.es6 => stage_test_component.js} | 0 .../{total_time_component.js.es6 => total_time_component.js} | 0 .../{cycle_analytics_bundle.js.es6 => cycle_analytics_bundle.js} | 0 ...{cycle_analytics_service.js.es6 => cycle_analytics_service.js} | 0 .../{cycle_analytics_store.js.es6 => cycle_analytics_store.js} | 0 .../{default_event_objects.js.es6 => default_event_objects.js} | 0 app/assets/javascripts/{diff.js.es6 => diff.js} | 0 .../{comment_resolve_btn.js.es6 => comment_resolve_btn.js} | 0 .../{jump_to_discussion.js.es6 => jump_to_discussion.js} | 0 .../diff_notes/components/{resolve_btn.js.es6 => resolve_btn.js} | 0 .../components/{resolve_count.js.es6 => resolve_count.js} | 0 .../{resolve_discussion_btn.js.es6 => resolve_discussion_btn.js} | 0 .../diff_notes/{diff_notes_bundle.js.es6 => diff_notes_bundle.js} | 0 .../diff_notes/mixins/{discussion.js.es6 => discussion.js} | 0 .../diff_notes/models/{discussion.js.es6 => discussion.js} | 0 app/assets/javascripts/diff_notes/models/{note.js.es6 => note.js} | 0 .../diff_notes/services/{resolve.js.es6 => resolve.js} | 0 .../diff_notes/stores/{comments.js.es6 => comments.js} | 0 app/assets/javascripts/{dispatcher.js.es6 => dispatcher.js} | 0 .../javascripts/{due_date_select.js.es6 => due_date_select.js} | 0 .../components/{environment.js.es6 => environment.js} | 0 .../{environment_actions.js.es6 => environment_actions.js} | 0 ...nvironment_external_url.js.es6 => environment_external_url.js} | 0 .../components/{environment_item.js.es6 => environment_item.js} | 0 .../{environment_rollback.js.es6 => environment_rollback.js} | 0 .../components/{environment_stop.js.es6 => environment_stop.js} | 0 ...ment_terminal_button.js.es6 => environment_terminal_button.js} | 0 .../{environments_table.js.es6 => environments_table.js} | 0 .../{environments_bundle.js.es6 => environments_bundle.js} | 0 ...onments_folder_bundle.js.es6 => environments_folder_bundle.js} | 0 ...nvironments_folder_view.js.es6 => environments_folder_view.js} | 0 .../{environments_service.js.es6 => environments_service.js} | 0 .../stores/{environments_store.js.es6 => environments_store.js} | 0 app/assets/javascripts/extensions/{array.js.es6 => array.js} | 0 .../extensions/{custom_event.js.es6 => custom_event.js} | 0 app/assets/javascripts/extensions/{element.js.es6 => element.js} | 0 app/assets/javascripts/extensions/{object.js.es6 => object.js} | 0 .../filtered_search/{dropdown_hint.js.es6 => dropdown_hint.js} | 0 .../{dropdown_non_user.js.es6 => dropdown_non_user.js} | 0 .../filtered_search/{dropdown_user.js.es6 => dropdown_user.js} | 0 .../filtered_search/{dropdown_utils.js.es6 => dropdown_utils.js} | 0 ...iltered_search_dropdown.js.es6 => filtered_search_dropdown.js} | 0 ...ropdown_manager.js.es6 => filtered_search_dropdown_manager.js} | 0 ...{filtered_search_manager.js.es6 => filtered_search_manager.js} | 0 ...red_search_token_keys.js.es6 => filtered_search_token_keys.js} | 0 ...tered_search_tokenizer.js.es6 => filtered_search_tokenizer.js} | 0 .../{gfm_auto_complete.js.es6 => gfm_auto_complete.js} | 0 .../javascripts/{gl_field_error.js.es6 => gl_field_error.js} | 0 .../javascripts/{gl_field_errors.js.es6 => gl_field_errors.js} | 0 app/assets/javascripts/{gl_form.js.es6 => gl_form.js} | 0 ...roup_label_subscription.js.es6 => group_label_subscription.js} | 0 app/assets/javascripts/{issuable.js.es6 => issuable.js} | 0 .../issuable/{issuable_bundle.js.es6 => issuable_bundle.js} | 0 .../components/{collapsed_state.js.es6 => collapsed_state.js} | 0 .../components/{comparison_pane.js.es6 => comparison_pane.js} | 0 .../{estimate_only_pane.js.es6 => estimate_only_pane.js} | 0 .../time_tracking/components/{help_state.js.es6 => help_state.js} | 0 .../components/{no_tracking_pane.js.es6 => no_tracking_pane.js} | 0 .../components/{spent_only_pane.js.es6 => spent_only_pane.js} | 0 .../components/{time_tracker.js.es6 => time_tracker.js} | 0 .../{time_tracking_bundle.js.es6 => time_tracking_bundle.js} | 0 .../{issues_bulk_assignment.js.es6 => issues_bulk_assignment.js} | 0 app/assets/javascripts/{label_manager.js.es6 => label_manager.js} | 0 .../{bootstrap_linked_tabs.js.es6 => bootstrap_linked_tabs.js} | 0 .../lib/utils/{common_utils.js.es6 => common_utils.js} | 0 .../lib/utils/{datetime_utility.js.es6 => datetime_utility.js} | 0 .../javascripts/lib/utils/{pretty_time.js.es6 => pretty_time.js} | 0 .../javascripts/lib/utils/{url_utility.js.es6 => url_utility.js} | 0 .../{member_expiration_date.js.es6 => member_expiration_date.js} | 0 app/assets/javascripts/{members.js.es6 => members.js} | 0 .../components/{diff_file_editor.js.es6 => diff_file_editor.js} | 0 .../{inline_conflict_lines.js.es6 => inline_conflict_lines.js} | 0 ...{parallel_conflict_lines.js.es6 => parallel_conflict_lines.js} | 0 .../{merge_conflict_service.js.es6 => merge_conflict_service.js} | 0 .../{merge_conflict_store.js.es6 => merge_conflict_store.js} | 0 .../{merge_conflicts_bundle.js.es6 => merge_conflicts_bundle.js} | 0 .../{line_conflict_actions.js.es6 => line_conflict_actions.js} | 0 .../mixins/{line_conflict_utils.js.es6 => line_conflict_utils.js} | 0 .../{merge_request_tabs.js.es6 => merge_request_tabs.js} | 0 .../{merge_request_widget.js.es6 => merge_request_widget.js} | 0 .../merge_request_widget/{ci_bundle.js.es6 => ci_bundle.js} | 0 ...line_graph_dropdown.js.es6 => mini_pipeline_graph_dropdown.js} | 0 app/assets/javascripts/{pager.js.es6 => pager.js} | 0 app/assets/javascripts/{pipelines.js.es6 => pipelines.js} | 0 app/assets/javascripts/profile/{gl_crop.js.es6 => gl_crop.js} | 0 app/assets/javascripts/profile/{profile.js.es6 => profile.js} | 0 ...ct_label_subscription.js.es6 => project_label_subscription.js} | 0 .../{project_variables.js.es6 => project_variables.js} | 0 ...access_dropdown.js.es6 => protected_branch_access_dropdown.js} | 0 ...{protected_branch_create.js.es6 => protected_branch_create.js} | 0 ...tected_branch_dropdown.js.es6 => protected_branch_dropdown.js} | 0 .../{protected_branch_edit.js.es6 => protected_branch_edit.js} | 0 ...cted_branch_edit_list.js.es6 => protected_branch_edit_list.js} | 0 .../{search_autocomplete.js.es6 => search_autocomplete.js} | 0 .../javascripts/{shortcuts_blob.js.es6 => shortcuts_blob.js} | 0 .../{signin_tabs_memoizer.js.es6 => signin_tabs_memoizer.js} | 0 .../javascripts/{smart_interval.js.es6 => smart_interval.js} | 0 app/assets/javascripts/{snippets_list.js.es6 => snippets_list.js} | 0 .../{subbable_resource.js.es6 => subbable_resource.js} | 0 app/assets/javascripts/{subscription.js.es6 => subscription.js} | 0 ...ble_template_selector.js.es6 => issuable_template_selector.js} | 0 ...e_template_selectors.js.es6 => issuable_template_selectors.js} | 0 app/assets/javascripts/terminal/{terminal.js.es6 => terminal.js} | 0 .../terminal/{terminal_bundle.js.es6 => terminal_bundle.js} | 0 app/assets/javascripts/{todos.js.es6 => todos.js} | 0 .../javascripts/u2f/{authenticate.js.es6 => authenticate.js} | 0 app/assets/javascripts/{user.js.es6 => user.js} | 0 app/assets/javascripts/{user_tabs.js.es6 => user_tabs.js} | 0 .../{username_validator.js.es6 => username_validator.js} | 0 .../{version_check_image.js.es6 => version_check_image.js} | 0 .../{visibility_select.js.es6 => visibility_select.js} | 0 .../javascripts/vue_pipelines_index/{index.js.es6 => index.js} | 0 .../{pipeline_actions.js.es6 => pipeline_actions.js} | 0 .../vue_pipelines_index/{pipeline_url.js.es6 => pipeline_url.js} | 0 .../vue_pipelines_index/{pipelines.js.es6 => pipelines.js} | 0 .../javascripts/vue_pipelines_index/{stage.js.es6 => stage.js} | 0 .../javascripts/vue_pipelines_index/{status.js.es6 => status.js} | 0 .../javascripts/vue_pipelines_index/{store.js.es6 => store.js} | 0 .../vue_pipelines_index/{time_ago.js.es6 => time_ago.js} | 0 .../javascripts/vue_realtime_listener/{index.js.es6 => index.js} | 0 .../vue_shared/components/{commit.js.es6 => commit.js} | 0 .../components/{pipelines_table.js.es6 => pipelines_table.js} | 0 .../{pipelines_table_row.js.es6 => pipelines_table_row.js} | 0 .../components/{table_pagination.js.es6 => table_pagination.js} | 0 ...ue_resource_interceptor.js.es6 => vue_resource_interceptor.js} | 0 app/assets/javascripts/{wikis.js.es6 => wikis.js} | 0 .../{abuse_reports_spec.js.es6 => abuse_reports_spec.js} | 0 spec/javascripts/{activities_spec.js.es6 => activities_spec.js} | 0 .../boards/{boards_store_spec.js.es6 => boards_store_spec.js} | 0 .../boards/{issue_card_spec.js.es6 => issue_card_spec.js} | 0 spec/javascripts/boards/{issue_spec.js.es6 => issue_spec.js} | 0 spec/javascripts/boards/{list_spec.js.es6 => list_spec.js} | 0 spec/javascripts/boards/{mock_data.js.es6 => mock_data.js} | 0 .../boards/{modal_store_spec.js.es6 => modal_store_spec.js} | 0 ...trap_linked_tabs_spec.js.es6 => bootstrap_linked_tabs_spec.js} | 0 spec/javascripts/{build_spec.js.es6 => build_spec.js} | 0 .../commit/pipelines/{mock_data.js.es6 => mock_data.js} | 0 .../commit/pipelines/{pipelines_spec.js.es6 => pipelines_spec.js} | 0 .../{pipelines_store_spec.js.es6 => pipelines_store_spec.js} | 0 spec/javascripts/{commits_spec.js.es6 => commits_spec.js} | 0 .../{datetime_utility_spec.js.es6 => datetime_utility_spec.js} | 0 ...iff_comments_store_spec.js.es6 => diff_comments_store_spec.js} | 0 ...nvironment_actions_spec.js.es6 => environment_actions_spec.js} | 0 ..._external_url_spec.js.es6 => environment_external_url_spec.js} | 0 .../{environment_item_spec.js.es6 => environment_item_spec.js} | 0 ...ironment_rollback_spec.js.es6 => environment_rollback_spec.js} | 0 .../environments/{environment_spec.js.es6 => environment_spec.js} | 0 .../{environment_stop_spec.js.es6 => environment_stop_spec.js} | 0 .../{environment_table_spec.js.es6 => environment_table_spec.js} | 0 ...{environments_store_spec.js.es6 => environments_store_spec.js} | 0 ...s_folder_view_spec.js.es6 => environments_folder_view_spec.js} | 0 spec/javascripts/environments/{mock_data.js.es6 => mock_data.js} | 0 spec/javascripts/extensions/{array_spec.js.es6 => array_spec.js} | 0 .../extensions/{element_spec.js.es6 => element_spec.js} | 0 .../javascripts/extensions/{object_spec.js.es6 => object_spec.js} | 0 .../{dropdown_user_spec.js.es6 => dropdown_user_spec.js} | 0 .../{dropdown_utils_spec.js.es6 => dropdown_utils_spec.js} | 0 ...nager_spec.js.es6 => filtered_search_dropdown_manager_spec.js} | 0 ...search_manager_spec.js.es6 => filtered_search_manager_spec.js} | 0 ..._token_keys_spec.js.es6 => filtered_search_token_keys_spec.js} | 0 ...ch_tokenizer_spec.js.es6 => filtered_search_tokenizer_spec.js} | 0 .../{gfm_auto_complete_spec.js.es6 => gfm_auto_complete_spec.js} | 0 spec/javascripts/{gl_dropdown_spec.js.es6 => gl_dropdown_spec.js} | 0 .../{gl_field_errors_spec.js.es6 => gl_field_errors_spec.js} | 0 spec/javascripts/{gl_form_spec.js.es6 => gl_form_spec.js} | 0 .../helpers/{class_spec_helper.js.es6 => class_spec_helper.js} | 0 .../{class_spec_helper_spec.js.es6 => class_spec_helper_spec.js} | 0 spec/javascripts/{issuable_spec.js.es6 => issuable_spec.js} | 0 ...ble_time_tracker_spec.js.es6 => issuable_time_tracker_spec.js} | 0 ...els_issue_sidebar_spec.js.es6 => labels_issue_sidebar_spec.js} | 0 .../lib/utils/{common_utils_spec.js.es6 => common_utils_spec.js} | 0 .../lib/utils/{text_utility_spec.js.es6 => text_utility_spec.js} | 0 ..._dropdown_spec.js.es6 => mini_pipeline_graph_dropdown_spec.js} | 0 spec/javascripts/{pipelines_spec.js.es6 => pipelines_spec.js} | 0 spec/javascripts/{pretty_time_spec.js.es6 => pretty_time_spec.js} | 0 ...nin_tabs_memoizer_spec.js.es6 => signin_tabs_memoizer_spec.js} | 0 .../{smart_interval_spec.js.es6 => smart_interval_spec.js} | 0 .../{subbable_resource_spec.js.es6 => subbable_resource_spec.js} | 0 .../{user_callout_spec.js.es6 => user_callout_spec.js} | 0 ...ersion_check_image_spec.js.es6 => version_check_image_spec.js} | 0 .../{visibility_select_spec.js.es6 => visibility_select_spec.js} | 0 .../vue_shared/components/{commit_spec.js.es6 => commit_spec.js} | 0 ...ipelines_table_row_spec.js.es6 => pipelines_table_row_spec.js} | 0 .../{pipelines_table_spec.js.es6 => pipelines_table_spec.js} | 0 .../{table_pagination_spec.js.es6 => table_pagination_spec.js} | 0 238 files changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/{abuse_reports.js.es6 => abuse_reports.js} (100%) rename app/assets/javascripts/{activities.js.es6 => activities.js} (100%) rename app/assets/javascripts/blob/{blob_ci_yaml.js.es6 => blob_ci_yaml.js} (100%) rename app/assets/javascripts/blob/{blob_dockerfile_selector.js.es6 => blob_dockerfile_selector.js} (100%) rename app/assets/javascripts/blob/{blob_dockerfile_selectors.js.es6 => blob_dockerfile_selectors.js} (100%) rename app/assets/javascripts/blob/{blob_license_selectors.js.es6 => blob_license_selectors.js} (100%) rename app/assets/javascripts/blob/{template_selector.js.es6 => template_selector.js} (100%) rename app/assets/javascripts/boards/{boards_bundle.js.es6 => boards_bundle.js} (100%) rename app/assets/javascripts/boards/components/{board.js.es6 => board.js} (100%) rename app/assets/javascripts/boards/components/{board_blank_state.js.es6 => board_blank_state.js} (100%) rename app/assets/javascripts/boards/components/{board_delete.js.es6 => board_delete.js} (100%) rename app/assets/javascripts/boards/components/{board_list.js.es6 => board_list.js} (100%) rename app/assets/javascripts/boards/components/{board_sidebar.js.es6 => board_sidebar.js} (100%) rename app/assets/javascripts/boards/components/{issue_card_inner.js.es6 => issue_card_inner.js} (100%) rename app/assets/javascripts/boards/components/modal/{empty_state.js.es6 => empty_state.js} (100%) rename app/assets/javascripts/boards/components/modal/{filters.js.es6 => filters.js} (100%) rename app/assets/javascripts/boards/components/modal/filters/{label.js.es6 => label.js} (100%) rename app/assets/javascripts/boards/components/modal/filters/{milestone.js.es6 => milestone.js} (100%) rename app/assets/javascripts/boards/components/modal/filters/{user.js.es6 => user.js} (100%) rename app/assets/javascripts/boards/components/modal/{footer.js.es6 => footer.js} (100%) rename app/assets/javascripts/boards/components/modal/{header.js.es6 => header.js} (100%) rename app/assets/javascripts/boards/components/modal/{index.js.es6 => index.js} (100%) rename app/assets/javascripts/boards/components/modal/{list.js.es6 => list.js} (100%) rename app/assets/javascripts/boards/components/modal/{lists_dropdown.js.es6 => lists_dropdown.js} (100%) rename app/assets/javascripts/boards/components/modal/{tabs.js.es6 => tabs.js} (100%) rename app/assets/javascripts/boards/components/{new_list_dropdown.js.es6 => new_list_dropdown.js} (100%) rename app/assets/javascripts/boards/components/sidebar/{remove_issue.js.es6 => remove_issue.js} (100%) rename app/assets/javascripts/boards/filters/{due_date_filters.js.es6 => due_date_filters.js} (100%) rename app/assets/javascripts/boards/mixins/{modal_mixins.js.es6 => modal_mixins.js} (100%) rename app/assets/javascripts/boards/mixins/{sortable_default_options.js.es6 => sortable_default_options.js} (100%) rename app/assets/javascripts/boards/models/{issue.js.es6 => issue.js} (100%) rename app/assets/javascripts/boards/models/{label.js.es6 => label.js} (100%) rename app/assets/javascripts/boards/models/{list.js.es6 => list.js} (100%) rename app/assets/javascripts/boards/models/{milestone.js.es6 => milestone.js} (100%) rename app/assets/javascripts/boards/models/{user.js.es6 => user.js} (100%) rename app/assets/javascripts/boards/services/{board_service.js.es6 => board_service.js} (100%) rename app/assets/javascripts/boards/stores/{boards_store.js.es6 => boards_store.js} (100%) rename app/assets/javascripts/boards/stores/{modal_store.js.es6 => modal_store.js} (100%) rename app/assets/javascripts/{build_variables.js.es6 => build_variables.js} (100%) rename app/assets/javascripts/{ci_lint_editor.js.es6 => ci_lint_editor.js} (100%) rename app/assets/javascripts/commit/pipelines/{pipelines_bundle.js.es6 => pipelines_bundle.js} (100%) rename app/assets/javascripts/commit/pipelines/{pipelines_service.js.es6 => pipelines_service.js} (100%) rename app/assets/javascripts/commit/pipelines/{pipelines_store.js.es6 => pipelines_store.js} (100%) rename app/assets/javascripts/commit/pipelines/{pipelines_table.js.es6 => pipelines_table.js} (100%) rename app/assets/javascripts/{compare_autocomplete.js.es6 => compare_autocomplete.js} (100%) rename app/assets/javascripts/{copy_as_gfm.js.es6 => copy_as_gfm.js} (100%) rename app/assets/javascripts/{create_label.js.es6 => create_label.js} (100%) rename app/assets/javascripts/cycle_analytics/components/{stage_code_component.js.es6 => stage_code_component.js} (100%) rename app/assets/javascripts/cycle_analytics/components/{stage_issue_component.js.es6 => stage_issue_component.js} (100%) rename app/assets/javascripts/cycle_analytics/components/{stage_plan_component.js.es6 => stage_plan_component.js} (100%) rename app/assets/javascripts/cycle_analytics/components/{stage_production_component.js.es6 => stage_production_component.js} (100%) rename app/assets/javascripts/cycle_analytics/components/{stage_review_component.js.es6 => stage_review_component.js} (100%) rename app/assets/javascripts/cycle_analytics/components/{stage_staging_component.js.es6 => stage_staging_component.js} (100%) rename app/assets/javascripts/cycle_analytics/components/{stage_test_component.js.es6 => stage_test_component.js} (100%) rename app/assets/javascripts/cycle_analytics/components/{total_time_component.js.es6 => total_time_component.js} (100%) rename app/assets/javascripts/cycle_analytics/{cycle_analytics_bundle.js.es6 => cycle_analytics_bundle.js} (100%) rename app/assets/javascripts/cycle_analytics/{cycle_analytics_service.js.es6 => cycle_analytics_service.js} (100%) rename app/assets/javascripts/cycle_analytics/{cycle_analytics_store.js.es6 => cycle_analytics_store.js} (100%) rename app/assets/javascripts/cycle_analytics/{default_event_objects.js.es6 => default_event_objects.js} (100%) rename app/assets/javascripts/{diff.js.es6 => diff.js} (100%) rename app/assets/javascripts/diff_notes/components/{comment_resolve_btn.js.es6 => comment_resolve_btn.js} (100%) rename app/assets/javascripts/diff_notes/components/{jump_to_discussion.js.es6 => jump_to_discussion.js} (100%) rename app/assets/javascripts/diff_notes/components/{resolve_btn.js.es6 => resolve_btn.js} (100%) rename app/assets/javascripts/diff_notes/components/{resolve_count.js.es6 => resolve_count.js} (100%) rename app/assets/javascripts/diff_notes/components/{resolve_discussion_btn.js.es6 => resolve_discussion_btn.js} (100%) rename app/assets/javascripts/diff_notes/{diff_notes_bundle.js.es6 => diff_notes_bundle.js} (100%) rename app/assets/javascripts/diff_notes/mixins/{discussion.js.es6 => discussion.js} (100%) rename app/assets/javascripts/diff_notes/models/{discussion.js.es6 => discussion.js} (100%) rename app/assets/javascripts/diff_notes/models/{note.js.es6 => note.js} (100%) rename app/assets/javascripts/diff_notes/services/{resolve.js.es6 => resolve.js} (100%) rename app/assets/javascripts/diff_notes/stores/{comments.js.es6 => comments.js} (100%) rename app/assets/javascripts/{dispatcher.js.es6 => dispatcher.js} (100%) rename app/assets/javascripts/{due_date_select.js.es6 => due_date_select.js} (100%) rename app/assets/javascripts/environments/components/{environment.js.es6 => environment.js} (100%) rename app/assets/javascripts/environments/components/{environment_actions.js.es6 => environment_actions.js} (100%) rename app/assets/javascripts/environments/components/{environment_external_url.js.es6 => environment_external_url.js} (100%) rename app/assets/javascripts/environments/components/{environment_item.js.es6 => environment_item.js} (100%) rename app/assets/javascripts/environments/components/{environment_rollback.js.es6 => environment_rollback.js} (100%) rename app/assets/javascripts/environments/components/{environment_stop.js.es6 => environment_stop.js} (100%) rename app/assets/javascripts/environments/components/{environment_terminal_button.js.es6 => environment_terminal_button.js} (100%) rename app/assets/javascripts/environments/components/{environments_table.js.es6 => environments_table.js} (100%) rename app/assets/javascripts/environments/{environments_bundle.js.es6 => environments_bundle.js} (100%) rename app/assets/javascripts/environments/folder/{environments_folder_bundle.js.es6 => environments_folder_bundle.js} (100%) rename app/assets/javascripts/environments/folder/{environments_folder_view.js.es6 => environments_folder_view.js} (100%) rename app/assets/javascripts/environments/services/{environments_service.js.es6 => environments_service.js} (100%) rename app/assets/javascripts/environments/stores/{environments_store.js.es6 => environments_store.js} (100%) rename app/assets/javascripts/extensions/{array.js.es6 => array.js} (100%) rename app/assets/javascripts/extensions/{custom_event.js.es6 => custom_event.js} (100%) rename app/assets/javascripts/extensions/{element.js.es6 => element.js} (100%) rename app/assets/javascripts/extensions/{object.js.es6 => object.js} (100%) rename app/assets/javascripts/filtered_search/{dropdown_hint.js.es6 => dropdown_hint.js} (100%) rename app/assets/javascripts/filtered_search/{dropdown_non_user.js.es6 => dropdown_non_user.js} (100%) rename app/assets/javascripts/filtered_search/{dropdown_user.js.es6 => dropdown_user.js} (100%) rename app/assets/javascripts/filtered_search/{dropdown_utils.js.es6 => dropdown_utils.js} (100%) rename app/assets/javascripts/filtered_search/{filtered_search_dropdown.js.es6 => filtered_search_dropdown.js} (100%) rename app/assets/javascripts/filtered_search/{filtered_search_dropdown_manager.js.es6 => filtered_search_dropdown_manager.js} (100%) rename app/assets/javascripts/filtered_search/{filtered_search_manager.js.es6 => filtered_search_manager.js} (100%) rename app/assets/javascripts/filtered_search/{filtered_search_token_keys.js.es6 => filtered_search_token_keys.js} (100%) rename app/assets/javascripts/filtered_search/{filtered_search_tokenizer.js.es6 => filtered_search_tokenizer.js} (100%) rename app/assets/javascripts/{gfm_auto_complete.js.es6 => gfm_auto_complete.js} (100%) rename app/assets/javascripts/{gl_field_error.js.es6 => gl_field_error.js} (100%) rename app/assets/javascripts/{gl_field_errors.js.es6 => gl_field_errors.js} (100%) rename app/assets/javascripts/{gl_form.js.es6 => gl_form.js} (100%) rename app/assets/javascripts/{group_label_subscription.js.es6 => group_label_subscription.js} (100%) rename app/assets/javascripts/{issuable.js.es6 => issuable.js} (100%) rename app/assets/javascripts/issuable/{issuable_bundle.js.es6 => issuable_bundle.js} (100%) rename app/assets/javascripts/issuable/time_tracking/components/{collapsed_state.js.es6 => collapsed_state.js} (100%) rename app/assets/javascripts/issuable/time_tracking/components/{comparison_pane.js.es6 => comparison_pane.js} (100%) rename app/assets/javascripts/issuable/time_tracking/components/{estimate_only_pane.js.es6 => estimate_only_pane.js} (100%) rename app/assets/javascripts/issuable/time_tracking/components/{help_state.js.es6 => help_state.js} (100%) rename app/assets/javascripts/issuable/time_tracking/components/{no_tracking_pane.js.es6 => no_tracking_pane.js} (100%) rename app/assets/javascripts/issuable/time_tracking/components/{spent_only_pane.js.es6 => spent_only_pane.js} (100%) rename app/assets/javascripts/issuable/time_tracking/components/{time_tracker.js.es6 => time_tracker.js} (100%) rename app/assets/javascripts/issuable/time_tracking/{time_tracking_bundle.js.es6 => time_tracking_bundle.js} (100%) rename app/assets/javascripts/{issues_bulk_assignment.js.es6 => issues_bulk_assignment.js} (100%) rename app/assets/javascripts/{label_manager.js.es6 => label_manager.js} (100%) rename app/assets/javascripts/lib/utils/{bootstrap_linked_tabs.js.es6 => bootstrap_linked_tabs.js} (100%) rename app/assets/javascripts/lib/utils/{common_utils.js.es6 => common_utils.js} (100%) rename app/assets/javascripts/lib/utils/{datetime_utility.js.es6 => datetime_utility.js} (100%) rename app/assets/javascripts/lib/utils/{pretty_time.js.es6 => pretty_time.js} (100%) rename app/assets/javascripts/lib/utils/{url_utility.js.es6 => url_utility.js} (100%) rename app/assets/javascripts/{member_expiration_date.js.es6 => member_expiration_date.js} (100%) rename app/assets/javascripts/{members.js.es6 => members.js} (100%) rename app/assets/javascripts/merge_conflicts/components/{diff_file_editor.js.es6 => diff_file_editor.js} (100%) rename app/assets/javascripts/merge_conflicts/components/{inline_conflict_lines.js.es6 => inline_conflict_lines.js} (100%) rename app/assets/javascripts/merge_conflicts/components/{parallel_conflict_lines.js.es6 => parallel_conflict_lines.js} (100%) rename app/assets/javascripts/merge_conflicts/{merge_conflict_service.js.es6 => merge_conflict_service.js} (100%) rename app/assets/javascripts/merge_conflicts/{merge_conflict_store.js.es6 => merge_conflict_store.js} (100%) rename app/assets/javascripts/merge_conflicts/{merge_conflicts_bundle.js.es6 => merge_conflicts_bundle.js} (100%) rename app/assets/javascripts/merge_conflicts/mixins/{line_conflict_actions.js.es6 => line_conflict_actions.js} (100%) rename app/assets/javascripts/merge_conflicts/mixins/{line_conflict_utils.js.es6 => line_conflict_utils.js} (100%) rename app/assets/javascripts/{merge_request_tabs.js.es6 => merge_request_tabs.js} (100%) rename app/assets/javascripts/{merge_request_widget.js.es6 => merge_request_widget.js} (100%) rename app/assets/javascripts/merge_request_widget/{ci_bundle.js.es6 => ci_bundle.js} (100%) rename app/assets/javascripts/{mini_pipeline_graph_dropdown.js.es6 => mini_pipeline_graph_dropdown.js} (100%) rename app/assets/javascripts/{pager.js.es6 => pager.js} (100%) rename app/assets/javascripts/{pipelines.js.es6 => pipelines.js} (100%) rename app/assets/javascripts/profile/{gl_crop.js.es6 => gl_crop.js} (100%) rename app/assets/javascripts/profile/{profile.js.es6 => profile.js} (100%) rename app/assets/javascripts/{project_label_subscription.js.es6 => project_label_subscription.js} (100%) rename app/assets/javascripts/{project_variables.js.es6 => project_variables.js} (100%) rename app/assets/javascripts/protected_branches/{protected_branch_access_dropdown.js.es6 => protected_branch_access_dropdown.js} (100%) rename app/assets/javascripts/protected_branches/{protected_branch_create.js.es6 => protected_branch_create.js} (100%) rename app/assets/javascripts/protected_branches/{protected_branch_dropdown.js.es6 => protected_branch_dropdown.js} (100%) rename app/assets/javascripts/protected_branches/{protected_branch_edit.js.es6 => protected_branch_edit.js} (100%) rename app/assets/javascripts/protected_branches/{protected_branch_edit_list.js.es6 => protected_branch_edit_list.js} (100%) rename app/assets/javascripts/{search_autocomplete.js.es6 => search_autocomplete.js} (100%) rename app/assets/javascripts/{shortcuts_blob.js.es6 => shortcuts_blob.js} (100%) rename app/assets/javascripts/{signin_tabs_memoizer.js.es6 => signin_tabs_memoizer.js} (100%) rename app/assets/javascripts/{smart_interval.js.es6 => smart_interval.js} (100%) rename app/assets/javascripts/{snippets_list.js.es6 => snippets_list.js} (100%) rename app/assets/javascripts/{subbable_resource.js.es6 => subbable_resource.js} (100%) rename app/assets/javascripts/{subscription.js.es6 => subscription.js} (100%) rename app/assets/javascripts/templates/{issuable_template_selector.js.es6 => issuable_template_selector.js} (100%) rename app/assets/javascripts/templates/{issuable_template_selectors.js.es6 => issuable_template_selectors.js} (100%) rename app/assets/javascripts/terminal/{terminal.js.es6 => terminal.js} (100%) rename app/assets/javascripts/terminal/{terminal_bundle.js.es6 => terminal_bundle.js} (100%) rename app/assets/javascripts/{todos.js.es6 => todos.js} (100%) rename app/assets/javascripts/u2f/{authenticate.js.es6 => authenticate.js} (100%) rename app/assets/javascripts/{user.js.es6 => user.js} (100%) rename app/assets/javascripts/{user_tabs.js.es6 => user_tabs.js} (100%) rename app/assets/javascripts/{username_validator.js.es6 => username_validator.js} (100%) rename app/assets/javascripts/{version_check_image.js.es6 => version_check_image.js} (100%) rename app/assets/javascripts/{visibility_select.js.es6 => visibility_select.js} (100%) rename app/assets/javascripts/vue_pipelines_index/{index.js.es6 => index.js} (100%) rename app/assets/javascripts/vue_pipelines_index/{pipeline_actions.js.es6 => pipeline_actions.js} (100%) rename app/assets/javascripts/vue_pipelines_index/{pipeline_url.js.es6 => pipeline_url.js} (100%) rename app/assets/javascripts/vue_pipelines_index/{pipelines.js.es6 => pipelines.js} (100%) rename app/assets/javascripts/vue_pipelines_index/{stage.js.es6 => stage.js} (100%) rename app/assets/javascripts/vue_pipelines_index/{status.js.es6 => status.js} (100%) rename app/assets/javascripts/vue_pipelines_index/{store.js.es6 => store.js} (100%) rename app/assets/javascripts/vue_pipelines_index/{time_ago.js.es6 => time_ago.js} (100%) rename app/assets/javascripts/vue_realtime_listener/{index.js.es6 => index.js} (100%) rename app/assets/javascripts/vue_shared/components/{commit.js.es6 => commit.js} (100%) rename app/assets/javascripts/vue_shared/components/{pipelines_table.js.es6 => pipelines_table.js} (100%) rename app/assets/javascripts/vue_shared/components/{pipelines_table_row.js.es6 => pipelines_table_row.js} (100%) rename app/assets/javascripts/vue_shared/components/{table_pagination.js.es6 => table_pagination.js} (100%) rename app/assets/javascripts/vue_shared/{vue_resource_interceptor.js.es6 => vue_resource_interceptor.js} (100%) rename app/assets/javascripts/{wikis.js.es6 => wikis.js} (100%) rename spec/javascripts/{abuse_reports_spec.js.es6 => abuse_reports_spec.js} (100%) rename spec/javascripts/{activities_spec.js.es6 => activities_spec.js} (100%) rename spec/javascripts/boards/{boards_store_spec.js.es6 => boards_store_spec.js} (100%) rename spec/javascripts/boards/{issue_card_spec.js.es6 => issue_card_spec.js} (100%) rename spec/javascripts/boards/{issue_spec.js.es6 => issue_spec.js} (100%) rename spec/javascripts/boards/{list_spec.js.es6 => list_spec.js} (100%) rename spec/javascripts/boards/{mock_data.js.es6 => mock_data.js} (100%) rename spec/javascripts/boards/{modal_store_spec.js.es6 => modal_store_spec.js} (100%) rename spec/javascripts/{bootstrap_linked_tabs_spec.js.es6 => bootstrap_linked_tabs_spec.js} (100%) rename spec/javascripts/{build_spec.js.es6 => build_spec.js} (100%) rename spec/javascripts/commit/pipelines/{mock_data.js.es6 => mock_data.js} (100%) rename spec/javascripts/commit/pipelines/{pipelines_spec.js.es6 => pipelines_spec.js} (100%) rename spec/javascripts/commit/pipelines/{pipelines_store_spec.js.es6 => pipelines_store_spec.js} (100%) rename spec/javascripts/{commits_spec.js.es6 => commits_spec.js} (100%) rename spec/javascripts/{datetime_utility_spec.js.es6 => datetime_utility_spec.js} (100%) rename spec/javascripts/{diff_comments_store_spec.js.es6 => diff_comments_store_spec.js} (100%) rename spec/javascripts/environments/{environment_actions_spec.js.es6 => environment_actions_spec.js} (100%) rename spec/javascripts/environments/{environment_external_url_spec.js.es6 => environment_external_url_spec.js} (100%) rename spec/javascripts/environments/{environment_item_spec.js.es6 => environment_item_spec.js} (100%) rename spec/javascripts/environments/{environment_rollback_spec.js.es6 => environment_rollback_spec.js} (100%) rename spec/javascripts/environments/{environment_spec.js.es6 => environment_spec.js} (100%) rename spec/javascripts/environments/{environment_stop_spec.js.es6 => environment_stop_spec.js} (100%) rename spec/javascripts/environments/{environment_table_spec.js.es6 => environment_table_spec.js} (100%) rename spec/javascripts/environments/{environments_store_spec.js.es6 => environments_store_spec.js} (100%) rename spec/javascripts/environments/folder/{environments_folder_view_spec.js.es6 => environments_folder_view_spec.js} (100%) rename spec/javascripts/environments/{mock_data.js.es6 => mock_data.js} (100%) rename spec/javascripts/extensions/{array_spec.js.es6 => array_spec.js} (100%) rename spec/javascripts/extensions/{element_spec.js.es6 => element_spec.js} (100%) rename spec/javascripts/extensions/{object_spec.js.es6 => object_spec.js} (100%) rename spec/javascripts/filtered_search/{dropdown_user_spec.js.es6 => dropdown_user_spec.js} (100%) rename spec/javascripts/filtered_search/{dropdown_utils_spec.js.es6 => dropdown_utils_spec.js} (100%) rename spec/javascripts/filtered_search/{filtered_search_dropdown_manager_spec.js.es6 => filtered_search_dropdown_manager_spec.js} (100%) rename spec/javascripts/filtered_search/{filtered_search_manager_spec.js.es6 => filtered_search_manager_spec.js} (100%) rename spec/javascripts/filtered_search/{filtered_search_token_keys_spec.js.es6 => filtered_search_token_keys_spec.js} (100%) rename spec/javascripts/filtered_search/{filtered_search_tokenizer_spec.js.es6 => filtered_search_tokenizer_spec.js} (100%) rename spec/javascripts/{gfm_auto_complete_spec.js.es6 => gfm_auto_complete_spec.js} (100%) rename spec/javascripts/{gl_dropdown_spec.js.es6 => gl_dropdown_spec.js} (100%) rename spec/javascripts/{gl_field_errors_spec.js.es6 => gl_field_errors_spec.js} (100%) rename spec/javascripts/{gl_form_spec.js.es6 => gl_form_spec.js} (100%) rename spec/javascripts/helpers/{class_spec_helper.js.es6 => class_spec_helper.js} (100%) rename spec/javascripts/helpers/{class_spec_helper_spec.js.es6 => class_spec_helper_spec.js} (100%) rename spec/javascripts/{issuable_spec.js.es6 => issuable_spec.js} (100%) rename spec/javascripts/{issuable_time_tracker_spec.js.es6 => issuable_time_tracker_spec.js} (100%) rename spec/javascripts/{labels_issue_sidebar_spec.js.es6 => labels_issue_sidebar_spec.js} (100%) rename spec/javascripts/lib/utils/{common_utils_spec.js.es6 => common_utils_spec.js} (100%) rename spec/javascripts/lib/utils/{text_utility_spec.js.es6 => text_utility_spec.js} (100%) rename spec/javascripts/{mini_pipeline_graph_dropdown_spec.js.es6 => mini_pipeline_graph_dropdown_spec.js} (100%) rename spec/javascripts/{pipelines_spec.js.es6 => pipelines_spec.js} (100%) rename spec/javascripts/{pretty_time_spec.js.es6 => pretty_time_spec.js} (100%) rename spec/javascripts/{signin_tabs_memoizer_spec.js.es6 => signin_tabs_memoizer_spec.js} (100%) rename spec/javascripts/{smart_interval_spec.js.es6 => smart_interval_spec.js} (100%) rename spec/javascripts/{subbable_resource_spec.js.es6 => subbable_resource_spec.js} (100%) rename spec/javascripts/{user_callout_spec.js.es6 => user_callout_spec.js} (100%) rename spec/javascripts/{version_check_image_spec.js.es6 => version_check_image_spec.js} (100%) rename spec/javascripts/{visibility_select_spec.js.es6 => visibility_select_spec.js} (100%) rename spec/javascripts/vue_shared/components/{commit_spec.js.es6 => commit_spec.js} (100%) rename spec/javascripts/vue_shared/components/{pipelines_table_row_spec.js.es6 => pipelines_table_row_spec.js} (100%) rename spec/javascripts/vue_shared/components/{pipelines_table_spec.js.es6 => pipelines_table_spec.js} (100%) rename spec/javascripts/vue_shared/components/{table_pagination_spec.js.es6 => table_pagination_spec.js} (100%) diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js similarity index 100% rename from app/assets/javascripts/abuse_reports.js.es6 rename to app/assets/javascripts/abuse_reports.js diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js similarity index 100% rename from app/assets/javascripts/activities.js.es6 rename to app/assets/javascripts/activities.js diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js similarity index 100% rename from app/assets/javascripts/blob/blob_ci_yaml.js.es6 rename to app/assets/javascripts/blob/blob_ci_yaml.js diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js similarity index 100% rename from app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 rename to app/assets/javascripts/blob/blob_dockerfile_selector.js diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js similarity index 100% rename from app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 rename to app/assets/javascripts/blob/blob_dockerfile_selectors.js diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js similarity index 100% rename from app/assets/javascripts/blob/blob_license_selectors.js.es6 rename to app/assets/javascripts/blob/blob_license_selectors.js diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js similarity index 100% rename from app/assets/javascripts/blob/template_selector.js.es6 rename to app/assets/javascripts/blob/template_selector.js diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js similarity index 100% rename from app/assets/javascripts/boards/boards_bundle.js.es6 rename to app/assets/javascripts/boards/boards_bundle.js diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js similarity index 100% rename from app/assets/javascripts/boards/components/board.js.es6 rename to app/assets/javascripts/boards/components/board.js diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js similarity index 100% rename from app/assets/javascripts/boards/components/board_blank_state.js.es6 rename to app/assets/javascripts/boards/components/board_blank_state.js diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js similarity index 100% rename from app/assets/javascripts/boards/components/board_delete.js.es6 rename to app/assets/javascripts/boards/components/board_delete.js diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js similarity index 100% rename from app/assets/javascripts/boards/components/board_list.js.es6 rename to app/assets/javascripts/boards/components/board_list.js diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js similarity index 100% rename from app/assets/javascripts/boards/components/board_sidebar.js.es6 rename to app/assets/javascripts/boards/components/board_sidebar.js diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js similarity index 100% rename from app/assets/javascripts/boards/components/issue_card_inner.js.es6 rename to app/assets/javascripts/boards/components/issue_card_inner.js diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/empty_state.js.es6 rename to app/assets/javascripts/boards/components/modal/empty_state.js diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/filters.js.es6 rename to app/assets/javascripts/boards/components/modal/filters.js diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/filters/label.js.es6 rename to app/assets/javascripts/boards/components/modal/filters/label.js diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 rename to app/assets/javascripts/boards/components/modal/filters/milestone.js diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/filters/user.js.es6 rename to app/assets/javascripts/boards/components/modal/filters/user.js diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/footer.js.es6 rename to app/assets/javascripts/boards/components/modal/footer.js diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/header.js.es6 rename to app/assets/javascripts/boards/components/modal/header.js diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/index.js.es6 rename to app/assets/javascripts/boards/components/modal/index.js diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/list.js.es6 rename to app/assets/javascripts/boards/components/modal/list.js diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 rename to app/assets/javascripts/boards/components/modal/lists_dropdown.js diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js similarity index 100% rename from app/assets/javascripts/boards/components/modal/tabs.js.es6 rename to app/assets/javascripts/boards/components/modal/tabs.js diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js similarity index 100% rename from app/assets/javascripts/boards/components/new_list_dropdown.js.es6 rename to app/assets/javascripts/boards/components/new_list_dropdown.js diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js similarity index 100% rename from app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 rename to app/assets/javascripts/boards/components/sidebar/remove_issue.js diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js similarity index 100% rename from app/assets/javascripts/boards/filters/due_date_filters.js.es6 rename to app/assets/javascripts/boards/filters/due_date_filters.js diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js similarity index 100% rename from app/assets/javascripts/boards/mixins/modal_mixins.js.es6 rename to app/assets/javascripts/boards/mixins/modal_mixins.js diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js similarity index 100% rename from app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 rename to app/assets/javascripts/boards/mixins/sortable_default_options.js diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js similarity index 100% rename from app/assets/javascripts/boards/models/issue.js.es6 rename to app/assets/javascripts/boards/models/issue.js diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js similarity index 100% rename from app/assets/javascripts/boards/models/label.js.es6 rename to app/assets/javascripts/boards/models/label.js diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js similarity index 100% rename from app/assets/javascripts/boards/models/list.js.es6 rename to app/assets/javascripts/boards/models/list.js diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js similarity index 100% rename from app/assets/javascripts/boards/models/milestone.js.es6 rename to app/assets/javascripts/boards/models/milestone.js diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js similarity index 100% rename from app/assets/javascripts/boards/models/user.js.es6 rename to app/assets/javascripts/boards/models/user.js diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js similarity index 100% rename from app/assets/javascripts/boards/services/board_service.js.es6 rename to app/assets/javascripts/boards/services/board_service.js diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js similarity index 100% rename from app/assets/javascripts/boards/stores/boards_store.js.es6 rename to app/assets/javascripts/boards/stores/boards_store.js diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js similarity index 100% rename from app/assets/javascripts/boards/stores/modal_store.js.es6 rename to app/assets/javascripts/boards/stores/modal_store.js diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js similarity index 100% rename from app/assets/javascripts/build_variables.js.es6 rename to app/assets/javascripts/build_variables.js diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js similarity index 100% rename from app/assets/javascripts/ci_lint_editor.js.es6 rename to app/assets/javascripts/ci_lint_editor.js diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js similarity index 100% rename from app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 rename to app/assets/javascripts/commit/pipelines/pipelines_bundle.js diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js similarity index 100% rename from app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 rename to app/assets/javascripts/commit/pipelines/pipelines_service.js diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js similarity index 100% rename from app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 rename to app/assets/javascripts/commit/pipelines/pipelines_store.js diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js similarity index 100% rename from app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 rename to app/assets/javascripts/commit/pipelines/pipelines_table.js diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js similarity index 100% rename from app/assets/javascripts/compare_autocomplete.js.es6 rename to app/assets/javascripts/compare_autocomplete.js diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js similarity index 100% rename from app/assets/javascripts/copy_as_gfm.js.es6 rename to app/assets/javascripts/copy_as_gfm.js diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js similarity index 100% rename from app/assets/javascripts/create_label.js.es6 rename to app/assets/javascripts/create_label.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 rename to app/assets/javascripts/cycle_analytics/components/stage_code_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 rename to app/assets/javascripts/cycle_analytics/components/stage_issue_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 rename to app/assets/javascripts/cycle_analytics/components/stage_plan_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 rename to app/assets/javascripts/cycle_analytics/components/stage_production_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 rename to app/assets/javascripts/cycle_analytics/components/stage_review_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 rename to app/assets/javascripts/cycle_analytics/components/stage_staging_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 rename to app/assets/javascripts/cycle_analytics/components/stage_test_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 rename to app/assets/javascripts/cycle_analytics/components/total_time_component.js diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 rename to app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 rename to app/assets/javascripts/cycle_analytics/cycle_analytics_service.js diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 rename to app/assets/javascripts/cycle_analytics/cycle_analytics_store.js diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js similarity index 100% rename from app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 rename to app/assets/javascripts/cycle_analytics/default_event_objects.js diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js similarity index 100% rename from app/assets/javascripts/diff.js.es6 rename to app/assets/javascripts/diff.js diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js similarity index 100% rename from app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 rename to app/assets/javascripts/diff_notes/components/comment_resolve_btn.js diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js similarity index 100% rename from app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 rename to app/assets/javascripts/diff_notes/components/jump_to_discussion.js diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js similarity index 100% rename from app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 rename to app/assets/javascripts/diff_notes/components/resolve_btn.js diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js similarity index 100% rename from app/assets/javascripts/diff_notes/components/resolve_count.js.es6 rename to app/assets/javascripts/diff_notes/components/resolve_count.js diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js similarity index 100% rename from app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 rename to app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js similarity index 100% rename from app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 rename to app/assets/javascripts/diff_notes/diff_notes_bundle.js diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js similarity index 100% rename from app/assets/javascripts/diff_notes/mixins/discussion.js.es6 rename to app/assets/javascripts/diff_notes/mixins/discussion.js diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js similarity index 100% rename from app/assets/javascripts/diff_notes/models/discussion.js.es6 rename to app/assets/javascripts/diff_notes/models/discussion.js diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js similarity index 100% rename from app/assets/javascripts/diff_notes/models/note.js.es6 rename to app/assets/javascripts/diff_notes/models/note.js diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js similarity index 100% rename from app/assets/javascripts/diff_notes/services/resolve.js.es6 rename to app/assets/javascripts/diff_notes/services/resolve.js diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js similarity index 100% rename from app/assets/javascripts/diff_notes/stores/comments.js.es6 rename to app/assets/javascripts/diff_notes/stores/comments.js diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js similarity index 100% rename from app/assets/javascripts/dispatcher.js.es6 rename to app/assets/javascripts/dispatcher.js diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js similarity index 100% rename from app/assets/javascripts/due_date_select.js.es6 rename to app/assets/javascripts/due_date_select.js diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js similarity index 100% rename from app/assets/javascripts/environments/components/environment.js.es6 rename to app/assets/javascripts/environments/components/environment.js diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js similarity index 100% rename from app/assets/javascripts/environments/components/environment_actions.js.es6 rename to app/assets/javascripts/environments/components/environment_actions.js diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js similarity index 100% rename from app/assets/javascripts/environments/components/environment_external_url.js.es6 rename to app/assets/javascripts/environments/components/environment_external_url.js diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js similarity index 100% rename from app/assets/javascripts/environments/components/environment_item.js.es6 rename to app/assets/javascripts/environments/components/environment_item.js diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js similarity index 100% rename from app/assets/javascripts/environments/components/environment_rollback.js.es6 rename to app/assets/javascripts/environments/components/environment_rollback.js diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js similarity index 100% rename from app/assets/javascripts/environments/components/environment_stop.js.es6 rename to app/assets/javascripts/environments/components/environment_stop.js diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js similarity index 100% rename from app/assets/javascripts/environments/components/environment_terminal_button.js.es6 rename to app/assets/javascripts/environments/components/environment_terminal_button.js diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js similarity index 100% rename from app/assets/javascripts/environments/components/environments_table.js.es6 rename to app/assets/javascripts/environments/components/environments_table.js diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js similarity index 100% rename from app/assets/javascripts/environments/environments_bundle.js.es6 rename to app/assets/javascripts/environments/environments_bundle.js diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_bundle.js similarity index 100% rename from app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 rename to app/assets/javascripts/environments/folder/environments_folder_bundle.js diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js similarity index 100% rename from app/assets/javascripts/environments/folder/environments_folder_view.js.es6 rename to app/assets/javascripts/environments/folder/environments_folder_view.js diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js similarity index 100% rename from app/assets/javascripts/environments/services/environments_service.js.es6 rename to app/assets/javascripts/environments/services/environments_service.js diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js similarity index 100% rename from app/assets/javascripts/environments/stores/environments_store.js.es6 rename to app/assets/javascripts/environments/stores/environments_store.js diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js similarity index 100% rename from app/assets/javascripts/extensions/array.js.es6 rename to app/assets/javascripts/extensions/array.js diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js similarity index 100% rename from app/assets/javascripts/extensions/custom_event.js.es6 rename to app/assets/javascripts/extensions/custom_event.js diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js similarity index 100% rename from app/assets/javascripts/extensions/element.js.es6 rename to app/assets/javascripts/extensions/element.js diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js similarity index 100% rename from app/assets/javascripts/extensions/object.js.es6 rename to app/assets/javascripts/extensions/object.js diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js similarity index 100% rename from app/assets/javascripts/filtered_search/dropdown_hint.js.es6 rename to app/assets/javascripts/filtered_search/dropdown_hint.js diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js similarity index 100% rename from app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 rename to app/assets/javascripts/filtered_search/dropdown_non_user.js diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js similarity index 100% rename from app/assets/javascripts/filtered_search/dropdown_user.js.es6 rename to app/assets/javascripts/filtered_search/dropdown_user.js diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js similarity index 100% rename from app/assets/javascripts/filtered_search/dropdown_utils.js.es6 rename to app/assets/javascripts/filtered_search/dropdown_utils.js diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js similarity index 100% rename from app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 rename to app/assets/javascripts/filtered_search/filtered_search_dropdown.js diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js similarity index 100% rename from app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 rename to app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js similarity index 100% rename from app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 rename to app/assets/javascripts/filtered_search/filtered_search_manager.js diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js similarity index 100% rename from app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 rename to app/assets/javascripts/filtered_search/filtered_search_token_keys.js diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js similarity index 100% rename from app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 rename to app/assets/javascripts/filtered_search/filtered_search_tokenizer.js diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js similarity index 100% rename from app/assets/javascripts/gfm_auto_complete.js.es6 rename to app/assets/javascripts/gfm_auto_complete.js diff --git a/app/assets/javascripts/gl_field_error.js.es6 b/app/assets/javascripts/gl_field_error.js similarity index 100% rename from app/assets/javascripts/gl_field_error.js.es6 rename to app/assets/javascripts/gl_field_error.js diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js similarity index 100% rename from app/assets/javascripts/gl_field_errors.js.es6 rename to app/assets/javascripts/gl_field_errors.js diff --git a/app/assets/javascripts/gl_form.js.es6 b/app/assets/javascripts/gl_form.js similarity index 100% rename from app/assets/javascripts/gl_form.js.es6 rename to app/assets/javascripts/gl_form.js diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js similarity index 100% rename from app/assets/javascripts/group_label_subscription.js.es6 rename to app/assets/javascripts/group_label_subscription.js diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js similarity index 100% rename from app/assets/javascripts/issuable.js.es6 rename to app/assets/javascripts/issuable.js diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js similarity index 100% rename from app/assets/javascripts/issuable/issuable_bundle.js.es6 rename to app/assets/javascripts/issuable/issuable_bundle.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js similarity index 100% rename from app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 rename to app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js similarity index 100% rename from app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 rename to app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js similarity index 100% rename from app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 rename to app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js similarity index 100% rename from app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 rename to app/assets/javascripts/issuable/time_tracking/components/help_state.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js similarity index 100% rename from app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 rename to app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js similarity index 100% rename from app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 rename to app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js similarity index 100% rename from app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 rename to app/assets/javascripts/issuable/time_tracking/components/time_tracker.js diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js similarity index 100% rename from app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 rename to app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js similarity index 100% rename from app/assets/javascripts/issues_bulk_assignment.js.es6 rename to app/assets/javascripts/issues_bulk_assignment.js diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js similarity index 100% rename from app/assets/javascripts/label_manager.js.es6 rename to app/assets/javascripts/label_manager.js diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js similarity index 100% rename from app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 rename to app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js similarity index 100% rename from app/assets/javascripts/lib/utils/common_utils.js.es6 rename to app/assets/javascripts/lib/utils/common_utils.js diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js similarity index 100% rename from app/assets/javascripts/lib/utils/datetime_utility.js.es6 rename to app/assets/javascripts/lib/utils/datetime_utility.js diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js similarity index 100% rename from app/assets/javascripts/lib/utils/pretty_time.js.es6 rename to app/assets/javascripts/lib/utils/pretty_time.js diff --git a/app/assets/javascripts/lib/utils/url_utility.js.es6 b/app/assets/javascripts/lib/utils/url_utility.js similarity index 100% rename from app/assets/javascripts/lib/utils/url_utility.js.es6 rename to app/assets/javascripts/lib/utils/url_utility.js diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js similarity index 100% rename from app/assets/javascripts/member_expiration_date.js.es6 rename to app/assets/javascripts/member_expiration_date.js diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js similarity index 100% rename from app/assets/javascripts/members.js.es6 rename to app/assets/javascripts/members.js diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js similarity index 100% rename from app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 rename to app/assets/javascripts/merge_conflicts/components/diff_file_editor.js diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js similarity index 100% rename from app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 rename to app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js similarity index 100% rename from app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 rename to app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js similarity index 100% rename from app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 rename to app/assets/javascripts/merge_conflicts/merge_conflict_service.js diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js similarity index 100% rename from app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 rename to app/assets/javascripts/merge_conflicts/merge_conflict_store.js diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js similarity index 100% rename from app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 rename to app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js similarity index 100% rename from app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 rename to app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js similarity index 100% rename from app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 rename to app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js similarity index 100% rename from app/assets/javascripts/merge_request_tabs.js.es6 rename to app/assets/javascripts/merge_request_tabs.js diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js similarity index 100% rename from app/assets/javascripts/merge_request_widget.js.es6 rename to app/assets/javascripts/merge_request_widget.js diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js similarity index 100% rename from app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 rename to app/assets/javascripts/merge_request_widget/ci_bundle.js diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js similarity index 100% rename from app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 rename to app/assets/javascripts/mini_pipeline_graph_dropdown.js diff --git a/app/assets/javascripts/pager.js.es6 b/app/assets/javascripts/pager.js similarity index 100% rename from app/assets/javascripts/pager.js.es6 rename to app/assets/javascripts/pager.js diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js similarity index 100% rename from app/assets/javascripts/pipelines.js.es6 rename to app/assets/javascripts/pipelines.js diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js similarity index 100% rename from app/assets/javascripts/profile/gl_crop.js.es6 rename to app/assets/javascripts/profile/gl_crop.js diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js similarity index 100% rename from app/assets/javascripts/profile/profile.js.es6 rename to app/assets/javascripts/profile/profile.js diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js similarity index 100% rename from app/assets/javascripts/project_label_subscription.js.es6 rename to app/assets/javascripts/project_label_subscription.js diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js similarity index 100% rename from app/assets/javascripts/project_variables.js.es6 rename to app/assets/javascripts/project_variables.js diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js similarity index 100% rename from app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js similarity index 100% rename from app/assets/javascripts/protected_branches/protected_branch_create.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_create.js diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js similarity index 100% rename from app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_dropdown.js diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js similarity index 100% rename from app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_edit.js diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js similarity index 100% rename from app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_edit_list.js diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js similarity index 100% rename from app/assets/javascripts/search_autocomplete.js.es6 rename to app/assets/javascripts/search_autocomplete.js diff --git a/app/assets/javascripts/shortcuts_blob.js.es6 b/app/assets/javascripts/shortcuts_blob.js similarity index 100% rename from app/assets/javascripts/shortcuts_blob.js.es6 rename to app/assets/javascripts/shortcuts_blob.js diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js similarity index 100% rename from app/assets/javascripts/signin_tabs_memoizer.js.es6 rename to app/assets/javascripts/signin_tabs_memoizer.js diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js similarity index 100% rename from app/assets/javascripts/smart_interval.js.es6 rename to app/assets/javascripts/smart_interval.js diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js similarity index 100% rename from app/assets/javascripts/snippets_list.js.es6 rename to app/assets/javascripts/snippets_list.js diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js similarity index 100% rename from app/assets/javascripts/subbable_resource.js.es6 rename to app/assets/javascripts/subbable_resource.js diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js similarity index 100% rename from app/assets/javascripts/subscription.js.es6 rename to app/assets/javascripts/subscription.js diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js similarity index 100% rename from app/assets/javascripts/templates/issuable_template_selector.js.es6 rename to app/assets/javascripts/templates/issuable_template_selector.js diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js similarity index 100% rename from app/assets/javascripts/templates/issuable_template_selectors.js.es6 rename to app/assets/javascripts/templates/issuable_template_selectors.js diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js similarity index 100% rename from app/assets/javascripts/terminal/terminal.js.es6 rename to app/assets/javascripts/terminal/terminal.js diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js similarity index 100% rename from app/assets/javascripts/terminal/terminal_bundle.js.es6 rename to app/assets/javascripts/terminal/terminal_bundle.js diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js similarity index 100% rename from app/assets/javascripts/todos.js.es6 rename to app/assets/javascripts/todos.js diff --git a/app/assets/javascripts/u2f/authenticate.js.es6 b/app/assets/javascripts/u2f/authenticate.js similarity index 100% rename from app/assets/javascripts/u2f/authenticate.js.es6 rename to app/assets/javascripts/u2f/authenticate.js diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js similarity index 100% rename from app/assets/javascripts/user.js.es6 rename to app/assets/javascripts/user.js diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js similarity index 100% rename from app/assets/javascripts/user_tabs.js.es6 rename to app/assets/javascripts/user_tabs.js diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js similarity index 100% rename from app/assets/javascripts/username_validator.js.es6 rename to app/assets/javascripts/username_validator.js diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js similarity index 100% rename from app/assets/javascripts/version_check_image.js.es6 rename to app/assets/javascripts/version_check_image.js diff --git a/app/assets/javascripts/visibility_select.js.es6 b/app/assets/javascripts/visibility_select.js similarity index 100% rename from app/assets/javascripts/visibility_select.js.es6 rename to app/assets/javascripts/visibility_select.js diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js similarity index 100% rename from app/assets/javascripts/vue_pipelines_index/index.js.es6 rename to app/assets/javascripts/vue_pipelines_index/index.js diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js similarity index 100% rename from app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 rename to app/assets/javascripts/vue_pipelines_index/pipeline_actions.js diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js similarity index 100% rename from app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 rename to app/assets/javascripts/vue_pipelines_index/pipeline_url.js diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js similarity index 100% rename from app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 rename to app/assets/javascripts/vue_pipelines_index/pipelines.js diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js similarity index 100% rename from app/assets/javascripts/vue_pipelines_index/stage.js.es6 rename to app/assets/javascripts/vue_pipelines_index/stage.js diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js similarity index 100% rename from app/assets/javascripts/vue_pipelines_index/status.js.es6 rename to app/assets/javascripts/vue_pipelines_index/status.js diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js similarity index 100% rename from app/assets/javascripts/vue_pipelines_index/store.js.es6 rename to app/assets/javascripts/vue_pipelines_index/store.js diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js similarity index 100% rename from app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 rename to app/assets/javascripts/vue_pipelines_index/time_ago.js diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js similarity index 100% rename from app/assets/javascripts/vue_realtime_listener/index.js.es6 rename to app/assets/javascripts/vue_realtime_listener/index.js diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js similarity index 100% rename from app/assets/javascripts/vue_shared/components/commit.js.es6 rename to app/assets/javascripts/vue_shared/components/commit.js diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js similarity index 100% rename from app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 rename to app/assets/javascripts/vue_shared/components/pipelines_table.js diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js similarity index 100% rename from app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 rename to app/assets/javascripts/vue_shared/components/pipelines_table_row.js diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js similarity index 100% rename from app/assets/javascripts/vue_shared/components/table_pagination.js.es6 rename to app/assets/javascripts/vue_shared/components/table_pagination.js diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js similarity index 100% rename from app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 rename to app/assets/javascripts/vue_shared/vue_resource_interceptor.js diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js similarity index 100% rename from app/assets/javascripts/wikis.js.es6 rename to app/assets/javascripts/wikis.js diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js similarity index 100% rename from spec/javascripts/abuse_reports_spec.js.es6 rename to spec/javascripts/abuse_reports_spec.js diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js similarity index 100% rename from spec/javascripts/activities_spec.js.es6 rename to spec/javascripts/activities_spec.js diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js similarity index 100% rename from spec/javascripts/boards/boards_store_spec.js.es6 rename to spec/javascripts/boards/boards_store_spec.js diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js similarity index 100% rename from spec/javascripts/boards/issue_card_spec.js.es6 rename to spec/javascripts/boards/issue_card_spec.js diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js similarity index 100% rename from spec/javascripts/boards/issue_spec.js.es6 rename to spec/javascripts/boards/issue_spec.js diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js similarity index 100% rename from spec/javascripts/boards/list_spec.js.es6 rename to spec/javascripts/boards/list_spec.js diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js similarity index 100% rename from spec/javascripts/boards/mock_data.js.es6 rename to spec/javascripts/boards/mock_data.js diff --git a/spec/javascripts/boards/modal_store_spec.js.es6 b/spec/javascripts/boards/modal_store_spec.js similarity index 100% rename from spec/javascripts/boards/modal_store_spec.js.es6 rename to spec/javascripts/boards/modal_store_spec.js diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js similarity index 100% rename from spec/javascripts/bootstrap_linked_tabs_spec.js.es6 rename to spec/javascripts/bootstrap_linked_tabs_spec.js diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js similarity index 100% rename from spec/javascripts/build_spec.js.es6 rename to spec/javascripts/build_spec.js diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js similarity index 100% rename from spec/javascripts/commit/pipelines/mock_data.js.es6 rename to spec/javascripts/commit/pipelines/mock_data.js diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js similarity index 100% rename from spec/javascripts/commit/pipelines/pipelines_spec.js.es6 rename to spec/javascripts/commit/pipelines/pipelines_spec.js diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js similarity index 100% rename from spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 rename to spec/javascripts/commit/pipelines/pipelines_store_spec.js diff --git a/spec/javascripts/commits_spec.js.es6 b/spec/javascripts/commits_spec.js similarity index 100% rename from spec/javascripts/commits_spec.js.es6 rename to spec/javascripts/commits_spec.js diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js similarity index 100% rename from spec/javascripts/datetime_utility_spec.js.es6 rename to spec/javascripts/datetime_utility_spec.js diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js similarity index 100% rename from spec/javascripts/diff_comments_store_spec.js.es6 rename to spec/javascripts/diff_comments_store_spec.js diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js similarity index 100% rename from spec/javascripts/environments/environment_actions_spec.js.es6 rename to spec/javascripts/environments/environment_actions_spec.js diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js similarity index 100% rename from spec/javascripts/environments/environment_external_url_spec.js.es6 rename to spec/javascripts/environments/environment_external_url_spec.js diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js similarity index 100% rename from spec/javascripts/environments/environment_item_spec.js.es6 rename to spec/javascripts/environments/environment_item_spec.js diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js similarity index 100% rename from spec/javascripts/environments/environment_rollback_spec.js.es6 rename to spec/javascripts/environments/environment_rollback_spec.js diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js similarity index 100% rename from spec/javascripts/environments/environment_spec.js.es6 rename to spec/javascripts/environments/environment_spec.js diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js similarity index 100% rename from spec/javascripts/environments/environment_stop_spec.js.es6 rename to spec/javascripts/environments/environment_stop_spec.js diff --git a/spec/javascripts/environments/environment_table_spec.js.es6 b/spec/javascripts/environments/environment_table_spec.js similarity index 100% rename from spec/javascripts/environments/environment_table_spec.js.es6 rename to spec/javascripts/environments/environment_table_spec.js diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js similarity index 100% rename from spec/javascripts/environments/environments_store_spec.js.es6 rename to spec/javascripts/environments/environments_store_spec.js diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 b/spec/javascripts/environments/folder/environments_folder_view_spec.js similarity index 100% rename from spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 rename to spec/javascripts/environments/folder/environments_folder_view_spec.js diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js similarity index 100% rename from spec/javascripts/environments/mock_data.js.es6 rename to spec/javascripts/environments/mock_data.js diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js similarity index 100% rename from spec/javascripts/extensions/array_spec.js.es6 rename to spec/javascripts/extensions/array_spec.js diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js similarity index 100% rename from spec/javascripts/extensions/element_spec.js.es6 rename to spec/javascripts/extensions/element_spec.js diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js similarity index 100% rename from spec/javascripts/extensions/object_spec.js.es6 rename to spec/javascripts/extensions/object_spec.js diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js similarity index 100% rename from spec/javascripts/filtered_search/dropdown_user_spec.js.es6 rename to spec/javascripts/filtered_search/dropdown_user_spec.js diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js similarity index 100% rename from spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 rename to spec/javascripts/filtered_search/dropdown_utils_spec.js diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js similarity index 100% rename from spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 rename to spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js similarity index 100% rename from spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 rename to spec/javascripts/filtered_search/filtered_search_manager_spec.js diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js similarity index 100% rename from spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 rename to spec/javascripts/filtered_search/filtered_search_token_keys_spec.js diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js similarity index 100% rename from spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 rename to spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js similarity index 100% rename from spec/javascripts/gfm_auto_complete_spec.js.es6 rename to spec/javascripts/gfm_auto_complete_spec.js diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js similarity index 100% rename from spec/javascripts/gl_dropdown_spec.js.es6 rename to spec/javascripts/gl_dropdown_spec.js diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js similarity index 100% rename from spec/javascripts/gl_field_errors_spec.js.es6 rename to spec/javascripts/gl_field_errors_spec.js diff --git a/spec/javascripts/gl_form_spec.js.es6 b/spec/javascripts/gl_form_spec.js similarity index 100% rename from spec/javascripts/gl_form_spec.js.es6 rename to spec/javascripts/gl_form_spec.js diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js similarity index 100% rename from spec/javascripts/helpers/class_spec_helper.js.es6 rename to spec/javascripts/helpers/class_spec_helper.js diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 b/spec/javascripts/helpers/class_spec_helper_spec.js similarity index 100% rename from spec/javascripts/helpers/class_spec_helper_spec.js.es6 rename to spec/javascripts/helpers/class_spec_helper_spec.js diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js similarity index 100% rename from spec/javascripts/issuable_spec.js.es6 rename to spec/javascripts/issuable_spec.js diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js similarity index 100% rename from spec/javascripts/issuable_time_tracker_spec.js.es6 rename to spec/javascripts/issuable_time_tracker_spec.js diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js similarity index 100% rename from spec/javascripts/labels_issue_sidebar_spec.js.es6 rename to spec/javascripts/labels_issue_sidebar_spec.js diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js similarity index 100% rename from spec/javascripts/lib/utils/common_utils_spec.js.es6 rename to spec/javascripts/lib/utils/common_utils_spec.js diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js similarity index 100% rename from spec/javascripts/lib/utils/text_utility_spec.js.es6 rename to spec/javascripts/lib/utils/text_utility_spec.js diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js similarity index 100% rename from spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 rename to spec/javascripts/mini_pipeline_graph_dropdown_spec.js diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js similarity index 100% rename from spec/javascripts/pipelines_spec.js.es6 rename to spec/javascripts/pipelines_spec.js diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js similarity index 100% rename from spec/javascripts/pretty_time_spec.js.es6 rename to spec/javascripts/pretty_time_spec.js diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js similarity index 100% rename from spec/javascripts/signin_tabs_memoizer_spec.js.es6 rename to spec/javascripts/signin_tabs_memoizer_spec.js diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js similarity index 100% rename from spec/javascripts/smart_interval_spec.js.es6 rename to spec/javascripts/smart_interval_spec.js diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js similarity index 100% rename from spec/javascripts/subbable_resource_spec.js.es6 rename to spec/javascripts/subbable_resource_spec.js diff --git a/spec/javascripts/user_callout_spec.js.es6 b/spec/javascripts/user_callout_spec.js similarity index 100% rename from spec/javascripts/user_callout_spec.js.es6 rename to spec/javascripts/user_callout_spec.js diff --git a/spec/javascripts/version_check_image_spec.js.es6 b/spec/javascripts/version_check_image_spec.js similarity index 100% rename from spec/javascripts/version_check_image_spec.js.es6 rename to spec/javascripts/version_check_image_spec.js diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js similarity index 100% rename from spec/javascripts/visibility_select_spec.js.es6 rename to spec/javascripts/visibility_select_spec.js diff --git a/spec/javascripts/vue_shared/components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js similarity index 100% rename from spec/javascripts/vue_shared/components/commit_spec.js.es6 rename to spec/javascripts/vue_shared/components/commit_spec.js diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js similarity index 100% rename from spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 rename to spec/javascripts/vue_shared/components/pipelines_table_row_spec.js diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js similarity index 100% rename from spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 rename to spec/javascripts/vue_shared/components/pipelines_table_spec.js diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js similarity index 100% rename from spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 rename to spec/javascripts/vue_shared/components/table_pagination_spec.js From b626869f8fccfef65a652b0acca7c52a3647aeb3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 21:23:57 +0100 Subject: [PATCH 55/95] Fix import model attributes --- spec/lib/gitlab/import_export/all_models.yml | 1 + spec/lib/gitlab/import_export/safe_model_attributes.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index eef283c2460..f20b6be51e1 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -97,6 +97,7 @@ variables: triggers: - project - trigger_requests +- owner deploy_keys: - user - deploy_keys_projects diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 6534902b52d..3bd1f335a89 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -240,6 +240,8 @@ Ci::Trigger: - created_at - updated_at - gl_project_id +- owner_id +- description DeployKey: - id - user_id From e4a9b8a9e1f4052fde9ecacad73e2f96fdd8d21d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 21:40:59 +0100 Subject: [PATCH 56/95] Move foreign key to separate migration --- db/migrate/20170217151948_add_owner_id_to_triggers.rb | 1 - db/migrate/20170305203726_add_owner_id_foreign_key.rb | 11 +++++++++++ db/schema.rb | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20170305203726_add_owner_id_foreign_key.rb diff --git a/db/migrate/20170217151948_add_owner_id_to_triggers.rb b/db/migrate/20170217151948_add_owner_id_to_triggers.rb index 845c89c703e..16d7cc5bed6 100644 --- a/db/migrate/20170217151948_add_owner_id_to_triggers.rb +++ b/db/migrate/20170217151948_add_owner_id_to_triggers.rb @@ -5,6 +5,5 @@ class AddOwnerIdToTriggers < ActiveRecord::Migration def change add_column :ci_triggers, :owner_id, :integer - add_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade end end diff --git a/db/migrate/20170305203726_add_owner_id_foreign_key.rb b/db/migrate/20170305203726_add_owner_id_foreign_key.rb new file mode 100644 index 00000000000..3eece0e2eb5 --- /dev/null +++ b/db/migrate/20170305203726_add_owner_id_foreign_key.rb @@ -0,0 +1,11 @@ +class AddOwnerIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade + end +end diff --git a/db/schema.rb b/db/schema.rb index 75daae86a4a..9deed46530e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170217151949) do +ActiveRecord::Schema.define(version: 20170305203726) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1335,7 +1335,7 @@ ActiveRecord::Schema.define(version: 20170217151949) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "boards", "projects" - add_foreign_key "ci_triggers", "users", column: "owner_id", on_delete: :cascade + add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade From 2cc6485518c332d1452316e24155e921020886d9 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Wed, 15 Feb 2017 13:49:14 +0000 Subject: [PATCH 57/95] Improved team selection Review changes --- app/assets/stylesheets/framework/layout.scss | 13 ++++++ app/helpers/mattermost_helper.rb | 6 +-- app/views/layouts/application.html.haml | 2 +- .../mattermosts/_team_selection.html.haml | 13 +++--- app/views/projects/mattermosts/new.html.haml | 2 + lib/mattermost/team.rb | 2 +- .../services/mattermost_slash_command_spec.rb | 42 ++++++++++++------- 7 files changed, 53 insertions(+), 27 deletions(-) diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 29d55c44699..0a42b17c1f5 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -8,6 +8,19 @@ body { &.navless { background-color: $white-light !important; } + + &.card-content { + background-color: $gray-darker; + + .content-wrapper { + padding: 0; + + .container-fluid, + .container-limited { + background-color: $gray-darker; + } + } + } } .container { diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb index 49ac12db832..27ff4051c8d 100644 --- a/app/helpers/mattermost_helper.rb +++ b/app/helpers/mattermost_helper.rb @@ -1,9 +1,7 @@ module MattermostHelper def mattermost_teams_options(teams) - teams_options = teams.map do |id, options| - [options['display_name'] || options['name'], id] + teams.map do |team| + [team['display_name'] || team['name'], team['id']] end - - teams_options.compact.unshift(['Select team...', '0']) end end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 19bd9b6d5c9..36543edc040 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en", class: "#{page_class}" } = render "layouts/head" - %body{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } + %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data = render "layouts/header/default", title: header_title diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index a80f9aa4c4a..04bd4e8b683 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -2,16 +2,15 @@ This service will be installed on the Mattermost instance at %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host %hr -= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f| += form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project), html: { class: 'js-requires-input'} ) do |f| %h4 Team %p = @teams.one? ? 'The team' : 'Select the team' where the slash commands will be used in - - selected_id = @teams.one? ? @teams.keys.first : 0 - - options = mattermost_teams_options(@teams) - - options = options_for_select(options, selected_id) - = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id }) - = f.hidden_field(:team_id, value: selected_id) if @teams.one? + - selected_id = @teams.one? ? @teams.first['id'] : nil + - options = options_for_select(mattermost_teams_options(@teams), selected_id) + = f.select(:team_id, options, { include_blank: 'Select team...'}, { class: 'form-control', disabled: @teams.one?, selected: selected_id, required: true }) + = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one? .help-block - if @teams.one? This is the only available team. @@ -25,7 +24,7 @@ %hr %h4 Command trigger word %p Choose the word that will trigger commands - = f.text_field(:trigger, value: @project.path, class: 'form-control') + = f.text_field(:trigger, value: @project.path, class: 'form-control', required: true) .help-block %p Trigger word must be unique, and can't begin with a slash or contain any spaces. diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml index 96b1d2aee61..15829a3f143 100644 --- a/app/views/projects/mattermosts/new.html.haml +++ b/app/views/projects/mattermosts/new.html.haml @@ -1,3 +1,5 @@ +- @body_class = 'card-content' + .service-installation .inline.pull-right = custom_icon('mattermost_logo', size: 48) diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 09dfd082b3a..afc152aa02e 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,7 +1,7 @@ module Mattermost class Team < Client def all - session_get('/api/v3/teams/all') + session_get('/api/v3/teams/all').values end end end diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index f5adb53a2dc..24d22a092d4 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Setup Mattermost slash commands', feature: true do +feature 'Setup Mattermost slash commands', :feature, :js do let(:user) { create(:user) } let(:project) { create(:empty_project) } let(:service) { project.create_mattermost_slash_commands_service } @@ -62,11 +62,11 @@ feature 'Setup Mattermost slash commands', feature: true do click_link 'Add to Mattermost' - team_name = teams.first[1]['display_name'] - select_element = find('select#mattermost_team_id') + team_name = teams.first['display_name'] + select_element = find('#mattermost_team_id') selected_option = select_element.find('option[selected]') - expect(select_element['disabled']).to eq('disabled') + expect(select_element['disabled']).to be(true) expect(selected_option).to have_content(team_name.to_s) end @@ -75,7 +75,7 @@ feature 'Setup Mattermost slash commands', feature: true do click_link 'Add to Mattermost' - expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first[0].to_s) + expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first['id']) end it 'shows an explanation user is a member of multiple teams' do @@ -92,12 +92,9 @@ feature 'Setup Mattermost slash commands', feature: true do click_link 'Add to Mattermost' - select_element = find('select#mattermost_team_id') - selected_option = select_element.find('option[selected]') + select_element = find('#mattermost_team_id') - expect(select_element['disabled']).to be(nil) - expect(selected_option).to have_content('Select team...') - # The 'Select team...' placeholder is item `0`. + expect(select_element['disabled']).to be(false) expect(select_element.all('option').count).to eq(3) end @@ -110,20 +107,37 @@ feature 'Setup Mattermost slash commands', feature: true do expect(page).to have_content('test mattermost error message') end + it 'enables the submit button if the required fields are provided', :js do + stub_teams(count: 1) + + click_link 'Add to Mattermost' + + expect(find('input[type="submit"]')['disabled']).not_to be(true) + end + + it 'disables the submit button if the required fields are not provided', :js do + stub_teams(count: 1) + + click_link 'Add to Mattermost' + + fill_in('mattermost_trigger', with: '') + + expect(find('input[type="submit"]')['disabled']).to be(true) + end + def stub_teams(count: 0) teams = create_teams(count) - allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { teams } + allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [teams, nil] } teams end def create_teams(count = 0) - teams = {} + teams = [] count.times do |i| - i += 1 - teams[i] = { id: i, display_name: i } + teams.push({ "id" => "x#{i}", "display_name" => "x#{i}-name" }) end teams From 16897d32d2d8fe848c7bbd7ba2acb8ff64bbd0bf Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 23:03:54 +0100 Subject: [PATCH 58/95] Lint doc --- doc/api/pipeline_triggers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md index aebc84b9a66..a26bdfdbfca 100644 --- a/doc/api/pipeline_triggers.md +++ b/doc/api/pipeline_triggers.md @@ -77,7 +77,7 @@ POST /projects/:id/triggers | `description` | string | yes | The trigger name | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers" ``` ```json @@ -107,7 +107,7 @@ PUT /projects/:id/triggers/:trigger_id | `description` | string | no | The trigger name | ``` -curl --request PUT -F description="my description" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/10" +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers/10" ``` ```json From 829cc80e1d9147721f6edc137c52ed91500c931e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 5 Mar 2017 23:14:02 +0100 Subject: [PATCH 59/95] Delete artifacts for pages unless expiry date is specified --- app/services/projects/update_pages_service.rb | 2 ++ .../unreleased/delete-artifacts-for-pages.yml | 4 ++++ .../pages/getting_started_part_four.md | 3 +++ .../projects/update_pages_service_spec.rb | 20 +++++++++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 changelogs/unreleased/delete-artifacts-for-pages.yml diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 2d42c4fc04a..523b9f41916 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -34,6 +34,8 @@ module Projects end rescue => e error(e.message) + ensure + build.erase_artifacts! unless build.has_expiring_artifacts? end private diff --git a/changelogs/unreleased/delete-artifacts-for-pages.yml b/changelogs/unreleased/delete-artifacts-for-pages.yml new file mode 100644 index 00000000000..3963a2e5014 --- /dev/null +++ b/changelogs/unreleased/delete-artifacts-for-pages.yml @@ -0,0 +1,4 @@ +--- +title: Delete artifacts for pages unless expiry date is specified +merge_request: +author: diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md index 6edf99239ea..83f88fbeb40 100644 --- a/doc/user/project/pages/getting_started_part_four.md +++ b/doc/user/project/pages/getting_started_part_four.md @@ -133,6 +133,9 @@ your Jekyll 3.4.0 site with GitLab Pages. This is the minimum configuration for our example. On the steps below, we'll refine the script by adding extra options to our GitLab CI. +Artifacts will be automatically deleted once GitLab Pages got deployed. +You can preserve artifacts for limited time by specifying expiry time. + ### Image At this point, you probably ask yourself: "okay, but to install Jekyll diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 411b22a0fb8..6c583f18a9c 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -26,6 +26,26 @@ describe Projects::UpdatePagesService do build.update_attributes(artifacts_metadata: metadata) end + context 'artifacts' do + context 'with expiry date' do + before do + build.artifacts_expire_in = "2 days" + end + + it "doesn't delete artifacts" do + expect(execute).to eq(:success) + expect(build.reload.artifacts_file?).to eq(true) + end + end + + context 'without expiry date' do + it "does delete artifacts" do + expect(execute).to eq(:success) + expect(build.reload.artifacts_file?).to eq(false) + end + end + end + it 'succeeds' do expect(project.pages_deployed?).to be_falsey expect(execute).to eq(:success) From 098a876f93e037d5a7800de74855acadf3ecbd84 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 6 Mar 2017 09:41:01 +0100 Subject: [PATCH 60/95] Add MR fo changelog about removing pages artifacts --- changelogs/unreleased/delete-artifacts-for-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/delete-artifacts-for-pages.yml b/changelogs/unreleased/delete-artifacts-for-pages.yml index 3963a2e5014..50b3dd81d60 100644 --- a/changelogs/unreleased/delete-artifacts-for-pages.yml +++ b/changelogs/unreleased/delete-artifacts-for-pages.yml @@ -1,4 +1,4 @@ --- title: Delete artifacts for pages unless expiry date is specified -merge_request: +merge_request: 9716 author: From 8f227f23a70c0027ac272b48d5f6f1e03dac36e9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 6 Mar 2017 09:49:38 +0100 Subject: [PATCH 61/95] Improve docs and specs related to pages artifacts --- doc/user/project/pages/getting_started_part_four.md | 2 +- spec/services/projects/update_pages_service_spec.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md index 83f88fbeb40..35af48724f2 100644 --- a/doc/user/project/pages/getting_started_part_four.md +++ b/doc/user/project/pages/getting_started_part_four.md @@ -134,7 +134,7 @@ configuration for our example. On the steps below, we'll refine the script by adding extra options to our GitLab CI. Artifacts will be automatically deleted once GitLab Pages got deployed. -You can preserve artifacts for limited time by specifying expiry time. +You can preserve artifacts for limited time by specifying the expiry time. ### Image diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 6c583f18a9c..f75fdd9e03f 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -26,7 +26,7 @@ describe Projects::UpdatePagesService do build.update_attributes(artifacts_metadata: metadata) end - context 'artifacts' do + describe 'pages artifacts' do context 'with expiry date' do before do build.artifacts_expire_in = "2 days" @@ -34,6 +34,7 @@ describe Projects::UpdatePagesService do it "doesn't delete artifacts" do expect(execute).to eq(:success) + expect(build.reload.artifacts_file?).to eq(true) end end @@ -41,6 +42,7 @@ describe Projects::UpdatePagesService do context 'without expiry date' do it "does delete artifacts" do expect(execute).to eq(:success) + expect(build.reload.artifacts_file?).to eq(false) end end From 905a2993e93c99c85b8b41d4959494c798262ed5 Mon Sep 17 00:00:00 2001 From: George Andrinopoulos Date: Sun, 5 Mar 2017 21:45:19 +0200 Subject: [PATCH 62/95] Fix json response in branches controller --- .../projects/branches_controller.rb | 2 +- ...-fix-search-branches-in-cherry-picking.yml | 4 ++++ .../projects/branches_controller_spec.rb | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index a01c0caa959..c40f9b7f75f 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -20,7 +20,7 @@ class Projects::BranchesController < Projects::ApplicationController respond_to do |format| format.html format.json do - render json: @repository.branch_names + render json: @branches.map(&:name) end end end diff --git a/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml b/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml new file mode 100644 index 00000000000..48e62f8f70d --- /dev/null +++ b/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml @@ -0,0 +1,4 @@ +--- +title: Fix json response in branches controller +merge_request: 9710 +author: George Andrinopoulos diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index e70737376af..298a7ff179c 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -244,4 +244,27 @@ describe Projects::BranchesController do end end end + + describe "GET index" do + render_views + + before do + sign_in(user) + end + + context 'when rendering a JSON format' do + it 'filters branches by name' do + get :index, + namespace_id: project.namespace, + project_id: project, + format: :json, + search: 'master' + + parsed_response = JSON.parse(response.body) + + expect(parsed_response.length).to eq 1 + expect(parsed_response.first).to eq 'master' + end + end + end end From 3b89ebb81c828dec546e32093aeebdeb8a7e1829 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 6 Mar 2017 09:09:18 +0100 Subject: [PATCH 63/95] Fix values being called at Array instead of Hash --- spec/lib/mattermost/team_spec.rb | 6 +++--- .../mattermost_slash_commands_service_spec.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index 6a9c7cebfff..ac493fdb20f 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -13,7 +13,7 @@ describe Mattermost::Team do context 'for valid request' do let(:response) do - [{ + { "xiyro8huptfhdndadpz8r3wnbo" => { "id" => "xiyro8huptfhdndadpz8r3wnbo", "create_at" => 1482174222155, "update_at" => 1482174222155, @@ -26,7 +26,7 @@ describe Mattermost::Team do "allowed_domains" => "", "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", "allow_open_invite" => false - }] + } } end before do @@ -39,7 +39,7 @@ describe Mattermost::Team do end it 'returns a token' do - is_expected.to eq(response) + is_expected.to eq(response.values) end end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 598ff232c82..f9531be5d25 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -92,7 +92,7 @@ describe MattermostSlashCommandsService, :models do to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, - body: ['list'].to_json + body: { 'list' => true }.to_json ) end From 01f99bd26910bb2b0480c44e752bdc7de107cfb1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Mar 2017 11:48:33 +0100 Subject: [PATCH 64/95] Update after review --- doc/api/build_triggers.md | 1 + doc/api/v3_to_v4.md | 3 +++ spec/requests/api/triggers_spec.rb | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 doc/api/build_triggers.md diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md new file mode 100644 index 00000000000..20d924ab35e --- /dev/null +++ b/doc/api/build_triggers.md @@ -0,0 +1 @@ +This document was moved to [Pipeline Triggers](pipeline_triggers.md). diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 39dc6d98e7b..67ee2b69c3f 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -59,3 +59,6 @@ changes are in V4: - Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) - `projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096) - Return basic info about pipeline in `GET /projects/:id/pipelines` [!8875](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8875) +- Rename Build Triggers to be Pipeline Triggers API [!9713](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9713) + - `POST /projects/:id/trigger/builds` to `POST /projects/:id/trigger/pipeline` + - Require description when creating a new trigger `POST /projects/:id/triggers` diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index f2effd71755..c4e8c9b09d7 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -180,7 +180,7 @@ describe API::Triggers do end context 'without required parameters' do - it 'creates trigger' do + it 'does not create trigger' do post api("/projects/#{project.id}/triggers", user) expect(response).to have_http_status(:bad_request) From d5f7060400a06d98f9e7107949aca8d89eaba7a8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Mar 2017 12:00:15 +0100 Subject: [PATCH 65/95] Rename `/take` to `/take_ownership`, expose `owner` in `v3`. --- doc/api/pipeline_triggers.md | 4 ++-- lib/api/triggers.rb | 2 +- lib/api/v3/entities.rb | 1 + spec/requests/api/triggers_spec.rb | 8 ++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md index a26bdfdbfca..fdb41a1d615 100644 --- a/doc/api/pipeline_triggers.md +++ b/doc/api/pipeline_triggers.md @@ -128,7 +128,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form descrip Update an owner of a project trigger. ``` -POST /projects/:id/triggers/:trigger_id/take +POST /projects/:id/triggers/:trigger_id/take_ownership ``` | Attribute | Type | required | Description | @@ -136,7 +136,7 @@ POST /projects/:id/triggers/:trigger_id/take | `trigger_id` | integer | yes | The trigger id | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/10/take" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/10/take_ownership" ``` ```json diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 157f3cef1fd..119e9024712 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -114,7 +114,7 @@ module API params do requires :trigger_id, type: Integer, desc: 'The trigger ID' end - post ':id/triggers/:trigger_id/take' do + post ':id/triggers/:trigger_id/take_ownership' do authenticate! authorize! :admin_build, user_project diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 29a44d4c7e5..2492481e4f0 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -189,6 +189,7 @@ module API class Trigger < Grape::Entity expose :token, :created_at, :updated_at, :deleted_at, :last_used + expose :owner, using: Entities::UserBasic end class TriggerRequest < Grape::Entity diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index c4e8c9b09d7..424c02932ab 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -238,12 +238,12 @@ describe API::Triggers do end end - describe 'POST /projects/:id/triggers/:trigger_id/take' do + describe 'POST /projects/:id/triggers/:trigger_id/take_ownership' do context 'authenticated user with valid permissions' do it 'updates owner' do expect(trigger.owner).to be_nil - post api("/projects/#{project.id}/triggers/#{trigger.id}/take", user) + post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user) expect(response).to have_http_status(200) expect(json_response).to include('owner') @@ -253,7 +253,7 @@ describe API::Triggers do context 'authenticated user with invalid permissions' do it 'does not update owner' do - post api("/projects/#{project.id}/triggers/#{trigger.id}/take", user2) + post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user2) expect(response).to have_http_status(403) end @@ -261,7 +261,7 @@ describe API::Triggers do context 'unauthenticated user' do it 'does not update owner' do - post api("/projects/#{project.id}/triggers/#{trigger.id}/take") + post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership") expect(response).to have_http_status(401) end From 20feaa9eff410cba133e5598f643883e603ba5da Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Mar 2017 12:27:55 +0100 Subject: [PATCH 66/95] Fix UserBasic --- lib/api/v3/entities.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 2492481e4f0..69853d33bec 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -189,7 +189,7 @@ module API class Trigger < Grape::Entity expose :token, :created_at, :updated_at, :deleted_at, :last_used - expose :owner, using: Entities::UserBasic + expose :owner, using: ::API::Entities::UserBasic end class TriggerRequest < Grape::Entity From 69b6a5a9e25c0035e492805305d5e4895146b693 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 6 Mar 2017 11:43:39 +0000 Subject: [PATCH 67/95] Changed the styling to allow bigger link targets on mobile --- app/assets/stylesheets/framework/header.scss | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 99651559d0d..846b77c37e5 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -260,19 +260,34 @@ header { font-size: 18px; .navbar-nav { + display: table; + table-layout: fixed; + width: 100%; margin: 0; - padding-right: 10px; text-align: right; } .navbar-collapse { padding-left: 5px; - .nav > li { - display: inline-block; + .nav > li:not(.hidden-xs) { + display: table-cell!important; + width: 25%; + + a { + margin-right: 8px; + } } } } + + .header-user-dropdown-toggle { + text-align: center; + } + + .header-user-avatar { + float: none; + } } .header-user { From 8bebc5872d97419b50f5b20c10133c325ba9c8e8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 6 Mar 2017 13:40:51 +0100 Subject: [PATCH 68/95] Add gitaly server dependency In GitLab 9.0.0 we want to turn on Gitaly in Omnibus installations. Gitaly is not a required component yet for source installations because we want to iterate on the configuration and architecture some more. Omnibus installations will have the option to completely disable Gitaly if they want to or if necessary. The new GITALY_SERVER_VERSION file which version of the Gitaly server application is needed by the current GitLab version. --- GITALY_SERVER_VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 GITALY_SERVER_VERSION diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION new file mode 100644 index 00000000000..0d91a54c7d4 --- /dev/null +++ b/GITALY_SERVER_VERSION @@ -0,0 +1 @@ +0.3.0 From 5753acfabc20b97f3ff8f9767382b960a4a9e95a Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Mon, 6 Mar 2017 12:49:52 +0100 Subject: [PATCH 69/95] Move schema definitions for our public API to a separate directory --- .../api/schemas/{ => public_api/v4}/user/login.json | 0 .../api/schemas/{ => public_api/v4}/user/public.json | 0 spec/requests/api/users_spec.rb | 8 ++++---- 3 files changed, 4 insertions(+), 4 deletions(-) rename spec/fixtures/api/schemas/{ => public_api/v4}/user/login.json (100%) rename spec/fixtures/api/schemas/{ => public_api/v4}/user/public.json (100%) diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json similarity index 100% rename from spec/fixtures/api/schemas/user/login.json rename to spec/fixtures/api/schemas/public_api/v4/user/login.json diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/public_api/v4/user/public.json similarity index 100% rename from spec/fixtures/api/schemas/user/public.json rename to spec/fixtures/api/schemas/public_api/v4/user/public.json diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index e5e4c84755f..881c48c75e0 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -728,7 +728,7 @@ describe API::Users, api: true do get api("/user", user) expect(response).to have_http_status(200) - expect(response).to match_response_schema('user/public') + expect(response).to match_response_schema('public_api/v4/user/public') expect(json_response['id']).to eq(user.id) end end @@ -747,7 +747,7 @@ describe API::Users, api: true do get api("/user?private_token=#{admin_personal_access_token}") expect(response).to have_http_status(200) - expect(response).to match_response_schema('user/public') + expect(response).to match_response_schema('public_api/v4/user/public') expect(json_response['id']).to eq(admin.id) end end @@ -757,7 +757,7 @@ describe API::Users, api: true do get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}") expect(response).to have_http_status(200) - expect(response).to match_response_schema('user/login') + expect(response).to match_response_schema('public_api/v4/user/login') expect(json_response['id']).to eq(user.id) end @@ -765,7 +765,7 @@ describe API::Users, api: true do get api("/user?private_token=#{admin.private_token}") expect(response).to have_http_status(200) - expect(response).to match_response_schema('user/public') + expect(response).to match_response_schema('public_api/v4/user/public') expect(json_response['id']).to eq(admin.id) end end From c727d4328fa14c4e90eb47c04f045c0f54224018 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Mon, 6 Mar 2017 12:48:10 +0100 Subject: [PATCH 70/95] Remove "subscribed" field from API responses returning list of issues or merge requests --- changelogs/unreleased/api-drop-subscribed.yml | 5 ++ doc/api/issues.md | 3 - doc/api/merge_requests.md | 1 - doc/api/v3_to_v4.md | 4 + lib/api/entities.rb | 22 +++-- lib/api/helpers.rb | 8 -- lib/api/issues.rb | 12 +-- lib/api/merge_requests.rb | 12 ++- lib/api/milestones.rb | 11 ++- lib/api/v3/merge_requests.rb | 8 ++ lib/api/v3/milestones.rb | 21 +++++ .../api/schemas/public_api/v3/issues.json | 77 ++++++++++++++++ .../schemas/public_api/v3/merge_requests.json | 89 +++++++++++++++++++ .../api/schemas/public_api/v4/issues.json | 76 ++++++++++++++++ .../schemas/public_api/v4/merge_requests.json | 88 ++++++++++++++++++ spec/requests/api/issues_spec.rb | 7 ++ spec/requests/api/merge_requests_spec.rb | 7 ++ spec/requests/api/milestones_spec.rb | 7 ++ spec/requests/api/v3/issues_spec.rb | 7 ++ spec/requests/api/v3/merge_requests_spec.rb | 7 ++ spec/requests/api/v3/milestones_spec.rb | 7 ++ 21 files changed, 447 insertions(+), 32 deletions(-) create mode 100644 changelogs/unreleased/api-drop-subscribed.yml create mode 100644 spec/fixtures/api/schemas/public_api/v3/issues.json create mode 100644 spec/fixtures/api/schemas/public_api/v3/merge_requests.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/issues.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/merge_requests.json diff --git a/changelogs/unreleased/api-drop-subscribed.yml b/changelogs/unreleased/api-drop-subscribed.yml new file mode 100644 index 00000000000..2a39026b519 --- /dev/null +++ b/changelogs/unreleased/api-drop-subscribed.yml @@ -0,0 +1,5 @@ +--- +title: Remove "subscribed" field from API responses returning list of issues or merge + requests +merge_request: 9661 +author: diff --git a/doc/api/issues.md b/doc/api/issues.md index 0f22d0b7f94..4047ff14af2 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -84,7 +84,6 @@ Example response: "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, "labels" : [], - "subscribed" : false, "user_notes_count": 1, "due_date": "2016-07-22", "web_url": "http://example.com/example/example/issues/6", @@ -167,7 +166,6 @@ Example response: "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z", - "subscribed" : false, "user_notes_count": 1, "due_date": null, "web_url": "http://example.com/example/example/issues/1", @@ -250,7 +248,6 @@ Example response: "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z", - "subscribed" : false, "user_notes_count": 1, "due_date": "2016-07-22", "web_url": "http://example.com/example/example/issues/1", diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index e178d5c1629..09d23cd2ff6 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -67,7 +67,6 @@ Parameters: }, "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : false, "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": null, "user_notes_count": 1, diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 67ee2b69c3f..ad52e1720e1 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -52,6 +52,10 @@ changes are in V4: - PUT `projects/:id` - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) +- Remove `subscribed` field from responses returning list of issues or merge + requests. Fetch individual issues or merge requests to obtain the value + of `subscribed` + [!9661](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9661) - Use `visibility` as string parameter everywhere [!9337](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9337) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) - Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 98ef9d4118e..6e251b36bda 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -249,14 +249,11 @@ module API expose :start_date end - class Issue < ProjectEntity + class IssueBasic < ProjectEntity expose :label_names, as: :labels expose :milestone, using: Entities::Milestone expose :assignee, :author, using: Entities::UserBasic - expose :subscribed do |issue, options| - issue.subscribed?(options[:current_user], options[:project] || issue.project) - end expose :user_notes_count expose :upvotes, :downvotes expose :due_date @@ -267,6 +264,12 @@ module API end end + class Issue < IssueBasic + expose :subscribed do |issue, options| + issue.subscribed?(options[:current_user], options[:project] || issue.project) + end + end + class IssuableTimeStats < Grape::Entity expose :time_estimate expose :total_time_spent @@ -279,7 +282,7 @@ module API expose :id end - class MergeRequest < ProjectEntity + class MergeRequestBasic < ProjectEntity expose :target_branch, :source_branch expose :upvotes, :downvotes expose :author, :assignee, using: Entities::UserBasic @@ -291,9 +294,6 @@ module API expose :merge_status expose :diff_head_sha, as: :sha expose :merge_commit_sha - expose :subscribed do |merge_request, options| - merge_request.subscribed?(options[:current_user], options[:project]) - end expose :user_notes_count expose :should_remove_source_branch?, as: :should_remove_source_branch expose :force_remove_source_branch?, as: :force_remove_source_branch @@ -303,6 +303,12 @@ module API end end + class MergeRequest < MergeRequestBasic + expose :subscribed do |merge_request, options| + merge_request.subscribed?(options[:current_user], options[:project]) + end + end + class MergeRequestChanges < MergeRequest expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _| compare.raw_diffs(all_diffs: true).to_a diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 9c41146f1e3..a43252a4661 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -388,14 +388,6 @@ module API header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) end - def issue_entity(project) - if project.has_external_issue_tracker? - Entities::ExternalIssue - else - Entities::Issue - end - end - # The Grape Error Middleware only has access to env but no params. We workaround this by # defining a method that returns the right value. def define_params_for_grape_middleware diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 1d6d0b05750..bda74069ad5 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -41,7 +41,7 @@ module API resource :issues do desc "Get currently authenticated user's issues" do - success Entities::Issue + success Entities::IssueBasic end params do optional :state, type: String, values: %w[opened closed all], default: 'all', @@ -51,7 +51,7 @@ module API get do issues = find_issues(scope: 'authored') - present paginate(issues), with: Entities::Issue, current_user: current_user + present paginate(issues), with: Entities::IssueBasic, current_user: current_user end end @@ -60,7 +60,7 @@ module API end resource :groups do desc 'Get a list of group issues' do - success Entities::Issue + success Entities::IssueBasic end params do optional :state, type: String, values: %w[opened closed all], default: 'opened', @@ -72,7 +72,7 @@ module API issues = find_issues(group_id: group.id, state: params[:state] || 'opened') - present paginate(issues), with: Entities::Issue, current_user: current_user + present paginate(issues), with: Entities::IssueBasic, current_user: current_user end end @@ -83,7 +83,7 @@ module API include TimeTrackingEndpoints desc 'Get a list of project issues' do - success Entities::Issue + success Entities::IssueBasic end params do optional :state, type: String, values: %w[opened closed all], default: 'all', @@ -95,7 +95,7 @@ module API issues = find_issues(project_id: project.id) - present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project + present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project end desc 'Get a single project issue' do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4638a66811d..6fc33a7a54a 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -25,6 +25,14 @@ module API render_api_error!(errors, 400) end + def issue_entity(project) + if project.has_external_issue_tracker? + Entities::ExternalIssue + else + Entities::IssueBasic + end + end + params :optional_params do optional :description, type: String, desc: 'The description of the merge request' optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' @@ -35,7 +43,7 @@ module API end desc 'List merge requests' do - success Entities::MergeRequest + success Entities::MergeRequestBasic end params do optional :state, type: String, values: %w[opened closed merged all], default: 'all', @@ -62,7 +70,7 @@ module API end merge_requests = merge_requests.reorder(params[:order_by] => params[:sort]) - present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project + present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project end desc 'Create a merge request' do diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index bd74174c655..e7f7edd95c7 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -103,7 +103,7 @@ module API end desc 'Get all issues for a single project milestone' do - success Entities::Issue + success Entities::IssueBasic end params do requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' @@ -120,12 +120,12 @@ module API } issues = IssuesFinder.new(current_user, finder_params).execute - present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project + present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project end desc 'Get all merge requests for a single project milestone' do detail 'This feature was introduced in GitLab 9.' - success Entities::MergeRequest + success Entities::MergeRequestBasic end params do requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' @@ -142,7 +142,10 @@ module API } merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute - present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project + present paginate(merge_requests), + with: Entities::MergeRequestBasic, + current_user: current_user, + project: user_project end end end diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 654e818e1b5..7dbd4691a94 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -28,6 +28,14 @@ module API render_api_error!(errors, 400) end + def issue_entity(project) + if project.has_external_issue_tracker? + ::API::Entities::ExternalIssue + else + ::API::Entities::Issue + end + end + params :optional_params do optional :description, type: String, desc: 'The description of the merge request' optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb index bbc29c40ee2..2a850a08a8a 100644 --- a/lib/api/v3/milestones.rb +++ b/lib/api/v3/milestones.rb @@ -37,6 +37,27 @@ module API present paginate(milestones), with: ::API::Entities::Milestone end + + desc 'Get all issues for a single project milestone' do + success ::API::Entities::Issue + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + use :pagination + end + get ':id/milestones/:milestone_id/issues' do + authorize! :read_milestone, user_project + + milestone = user_project.milestones.find(params[:milestone_id]) + + finder_params = { + project_id: user_project.id, + milestone_title: milestone.title + } + + issues = IssuesFinder.new(current_user, finder_params).execute + present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project + end end end end diff --git a/spec/fixtures/api/schemas/public_api/v3/issues.json b/spec/fixtures/api/schemas/public_api/v3/issues.json new file mode 100644 index 00000000000..f2ee9c925ae --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v3/issues.json @@ -0,0 +1,77 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "milestone": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "due_date": { "type": "date" }, + "start_date": { "type": "date" } + }, + "additionalProperties": false + }, + "assignee": { + "type": ["object", "null"], + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "user_notes_count": { "type": "integer" }, + "upvotes": { "type": "integer" }, + "downvotes": { "type": "integer" }, + "due_date": { "type": ["date", "null"] }, + "confidential": { "type": "boolean" }, + "web_url": { "type": "uri" }, + "subscribed": { "type": ["boolean"] } + }, + "required": [ + "id", "iid", "project_id", "title", "description", + "state", "created_at", "updated_at", "labels", + "milestone", "assignee", "author", "user_notes_count", + "upvotes", "downvotes", "due_date", "confidential", + "web_url", "subscribed" + ], + "additionalProperties": false + } +} diff --git a/spec/fixtures/api/schemas/public_api/v3/merge_requests.json b/spec/fixtures/api/schemas/public_api/v3/merge_requests.json new file mode 100644 index 00000000000..01f9fbb2c89 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v3/merge_requests.json @@ -0,0 +1,89 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "target_branch": { "type": "string" }, + "source_branch": { "type": "string" }, + "upvotes": { "type": "integer" }, + "downvotes": { "type": "integer" }, + "author": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "assignee": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "source_project_id": { "type": "integer" }, + "target_project_id": { "type": "integer" }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "work_in_progress": { "type": "boolean" }, + "milestone": { + "type": ["object", "null"], + "properties": { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "due_date": { "type": "date" }, + "start_date": { "type": "date" } + }, + "additionalProperties": false + }, + "merge_when_build_succeeds": { "type": "boolean" }, + "merge_status": { "type": "string" }, + "sha": { "type": "string" }, + "merge_commit_sha": { "type": ["string", "null"] }, + "user_notes_count": { "type": "integer" }, + "should_remove_source_branch": { "type": ["boolean", "null"] }, + "force_remove_source_branch": { "type": ["boolean", "null"] }, + "web_url": { "type": "uri" }, + "subscribed": { "type": ["boolean"] } + }, + "required": [ + "id", "iid", "project_id", "title", "description", + "state", "created_at", "updated_at", "target_branch", + "source_branch", "upvotes", "downvotes", "author", + "assignee", "source_project_id", "target_project_id", + "labels", "work_in_progress", "milestone", "merge_when_build_succeeds", + "merge_status", "sha", "merge_commit_sha", "user_notes_count", + "should_remove_source_branch", "force_remove_source_branch", + "web_url", "subscribed" + ], + "additionalProperties": false + } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json new file mode 100644 index 00000000000..52199e75734 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/issues.json @@ -0,0 +1,76 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "milestone": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "due_date": { "type": "date" }, + "start_date": { "type": "date" } + }, + "additionalProperties": false + }, + "assignee": { + "type": ["object", "null"], + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "user_notes_count": { "type": "integer" }, + "upvotes": { "type": "integer" }, + "downvotes": { "type": "integer" }, + "due_date": { "type": ["date", "null"] }, + "confidential": { "type": "boolean" }, + "web_url": { "type": "uri" } + }, + "required": [ + "id", "iid", "project_id", "title", "description", + "state", "created_at", "updated_at", "labels", + "milestone", "assignee", "author", "user_notes_count", + "upvotes", "downvotes", "due_date", "confidential", + "web_url" + ], + "additionalProperties": false + } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json new file mode 100644 index 00000000000..51642e8cbb8 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json @@ -0,0 +1,88 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "target_branch": { "type": "string" }, + "source_branch": { "type": "string" }, + "upvotes": { "type": "integer" }, + "downvotes": { "type": "integer" }, + "author": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "assignee": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "source_project_id": { "type": "integer" }, + "target_project_id": { "type": "integer" }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "work_in_progress": { "type": "boolean" }, + "milestone": { + "type": ["object", "null"], + "properties": { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "due_date": { "type": "date" }, + "start_date": { "type": "date" } + }, + "additionalProperties": false + }, + "merge_when_pipeline_succeeds": { "type": "boolean" }, + "merge_status": { "type": "string" }, + "sha": { "type": "string" }, + "merge_commit_sha": { "type": ["string", "null"] }, + "user_notes_count": { "type": "integer" }, + "should_remove_source_branch": { "type": ["boolean", "null"] }, + "force_remove_source_branch": { "type": ["boolean", "null"] }, + "web_url": { "type": "uri" } + }, + "required": [ + "id", "iid", "project_id", "title", "description", + "state", "created_at", "updated_at", "target_branch", + "source_branch", "upvotes", "downvotes", "author", + "assignee", "source_project_id", "target_project_id", + "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds", + "merge_status", "sha", "merge_commit_sha", "user_notes_count", + "should_remove_source_branch", "force_remove_source_branch", + "web_url" + ], + "additionalProperties": false + } +} diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 710e4320fd1..aca7c6a0734 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -270,6 +270,13 @@ describe API::Issues, api: true do expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end + + it 'matches V4 response schema' do + get api('/issues', user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index b3f0876c822..1083abf2ad3 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -93,6 +93,13 @@ describe API::MergeRequests, api: true do expect(json_response.first['id']).to eq merge_request_closed.id end + it 'matches V4 response schema' do + get api("/projects/#{project.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v4/merge_requests') + end + context "with ordering" do before do @mr_later = mr_with_later_created_and_updated_at_time diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 78c230117b8..3bb8b6fdbeb 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -227,6 +227,13 @@ describe API::Milestones, api: true do expect(json_response.first['milestone']['title']).to eq(milestone.title) end + it 'matches V4 response schema for a list of issues' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + end + it 'returns a 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 803acd55470..2a8105d5a2b 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -232,6 +232,13 @@ describe API::V3::Issues, api: true do expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end + + it 'matches V3 response schema' do + get v3_api('/issues', user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v3/issues') + end end end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 51764d1000e..b7ed643bc21 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -73,6 +73,13 @@ describe API::MergeRequests, api: true do expect(json_response.first['title']).to eq(merge_request_merged.title) end + it 'matches V3 response schema' do + get v3_api("/projects/#{project.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v3/merge_requests') + end + context "with ordering" do before do @mr_later = mr_with_later_created_and_updated_at_time diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb index 77705d8c839..127c0eec881 100644 --- a/spec/requests/api/v3/milestones_spec.rb +++ b/spec/requests/api/v3/milestones_spec.rb @@ -181,6 +181,13 @@ describe API::V3::Milestones, api: true do expect(json_response.first['milestone']['title']).to eq(milestone.title) end + it 'matches V3 response schema for a list of issues' do + get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v3/issues') + end + it 'returns a 401 error if user not authenticated' do get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues") From 111748ea89752825d6baba783b3b687c48e9e830 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 22 Feb 2017 11:13:59 +0100 Subject: [PATCH 71/95] Rename Builds to Jobs in the API Fixes gitlab-org/gitlab-ce#28515 [ci skip] --- lib/api/api.rb | 3 +- lib/api/entities.rb | 24 +- lib/api/{builds.rb => jobs.rb} | 117 +++--- lib/api/v3/builds.rb | 263 ++++++++++++ lib/api/v3/entities.rb | 10 + spec/requests/api/jobs_spec.rb | 477 ++++++++++++++++++++++ spec/requests/api/{ => v3}/builds_spec.rb | 2 +- 7 files changed, 827 insertions(+), 69 deletions(-) rename lib/api/{builds.rb => jobs.rb} (66%) create mode 100644 lib/api/v3/builds.rb create mode 100644 spec/requests/api/jobs_spec.rb rename spec/requests/api/{ => v3}/builds_spec.rb (99%) diff --git a/lib/api/api.rb b/lib/api/api.rb index 89449ce8813..91ca8c66344 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -9,6 +9,7 @@ module API mount ::API::V3::Boards mount ::API::V3::Branches mount ::API::V3::BroadcastMessages + mount ::API::V3::Builds mount ::API::V3::Commits mount ::API::V3::DeployKeys mount ::API::V3::Environments @@ -77,7 +78,6 @@ module API mount ::API::Boards mount ::API::Branches mount ::API::BroadcastMessages - mount ::API::Builds mount ::API::Commits mount ::API::CommitStatuses mount ::API::DeployKeys @@ -87,6 +87,7 @@ module API mount ::API::Groups mount ::API::Internal mount ::API::Issues + mount ::API::Jobs mount ::API::Keys mount ::API::Labels mount ::API::Lint diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 98ef9d4118e..22e2c4088d7 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -49,7 +49,8 @@ module API class ProjectHook < Hook expose :project_id, :issues_events, :merge_requests_events - expose :note_events, :build_events, :pipeline_events, :wiki_page_events + expose :note_events, :pipeline_events, :wiki_page_events + expose :job_events, as: :build_events end class BasicProjectDetails < Grape::Entity @@ -80,7 +81,7 @@ module API expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } - expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } + expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } expose :created_at, :last_activity_at @@ -93,7 +94,7 @@ module API expose :star_count, :forks_count expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } - expose :public_builds + expose :public_jobs, as: :public_builds expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links.all, options) end @@ -109,7 +110,7 @@ module API expose :storage_size expose :repository_size expose :lfs_objects_size - expose :build_artifacts_size + expose :job_artifacts_size, as: :build_artifacts_size end class Member < UserBasic @@ -144,7 +145,7 @@ module API expose :storage_size expose :repository_size expose :lfs_objects_size - expose :build_artifacts_size + expose :job_artifacts_size, as: :build_artifacts_size end end end @@ -448,7 +449,8 @@ module API class ProjectService < Grape::Entity expose :id, :title, :created_at, :updated_at, :active expose :push_events, :issues_events, :merge_requests_events - expose :tag_push_events, :note_events, :build_events, :pipeline_events + expose :tag_push_events, :note_events, :pipeline_events + expose :job_events, as: :build_events # Expose serialized properties expose :properties do |service, options| field_names = service.fields. @@ -616,11 +618,15 @@ module API end end +<<<<<<< HEAD class RunnerRegistrationDetails < Grape::Entity expose :id, :token end class BuildArtifactFile < Grape::Entity +======= + class JobArtifactFile < Grape::Entity +>>>>>>> 239b5f49c5... Rename Builds to Jobs in the API expose :filename, :size end @@ -628,11 +634,11 @@ module API expose :id, :sha, :ref, :status end - class Build < Grape::Entity + class Job < Grape::Entity expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :created_at, :started_at, :finished_at expose :user, with: User - expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? } + expose :artifacts_file, using: JobArtifactFile, if: -> (build, opts) { build.artifacts? } expose :commit, with: RepoCommit expose :runner, with: Runner expose :pipeline, with: PipelineBasic @@ -670,7 +676,7 @@ module API expose :id, :iid, :ref, :sha, :created_at expose :user, using: Entities::UserBasic expose :environment, using: Entities::EnvironmentBasic - expose :deployable, using: Entities::Build + expose :deployable, using: Entities::Job end class RepoLicense < Grape::Entity diff --git a/lib/api/builds.rb b/lib/api/jobs.rb similarity index 66% rename from lib/api/builds.rb rename to lib/api/jobs.rb index 5b76913fe45..b48574e173f 100644 --- a/lib/api/builds.rb +++ b/lib/api/jobs.rb @@ -1,5 +1,5 @@ module API - class Builds < Grape::API + class Jobs < Grape::API include PaginationParams before { authenticate! } @@ -13,9 +13,10 @@ module API optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', values: ::CommitStatus::AVAILABLE_STATUSES, coerce_with: ->(scope) { - if scope.is_a?(String) + case scope + when String [scope] - elsif scope.is_a?(Hashie::Mash) + when Hashie::Mash scope.values else ['unknown'] @@ -24,30 +25,30 @@ module API end end - desc 'Get a project builds' do - success Entities::Build + desc 'Get a projects jobs' do + success Entities::Job end params do use :optional_scope use :pagination end - get ':id/builds' do + get ':id/jobs' do builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) - present paginate(builds), with: Entities::Build, + present paginate(builds), with: Entities::Job, user_can_download_artifacts: can?(current_user, :read_build, user_project) end - desc 'Get builds for a specific commit of a project' do - success Entities::Build + desc 'Get jobs for a specific commit of a project' do + success Entities::Job end params do requires :sha, type: String, desc: 'The SHA id of a commit' use :optional_scope use :pagination end - get ':id/repository/commits/:sha/builds' do + get ':id/repository/commits/:sha/jobs' do authorize_read_builds! return not_found! unless user_project.commit(params[:sha]) @@ -56,47 +57,47 @@ module API builds = user_project.builds.where(pipeline: pipelines).order('id DESC') builds = filter_builds(builds, params[:scope]) - present paginate(builds), with: Entities::Build, + present paginate(builds), with: Entities::Job, user_can_download_artifacts: can?(current_user, :read_build, user_project) end - desc 'Get a specific build of a project' do - success Entities::Build + desc 'Get a specific job of a project' do + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - get ':id/builds/:build_id' do + get ':id/jobs/:job_id' do authorize_read_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) - present build, with: Entities::Build, + present build, with: Entities::Job, user_can_download_artifacts: can?(current_user, :read_build, user_project) end - desc 'Download the artifacts file from build' do + desc 'Download the artifacts file from a job' do detail 'This feature was introduced in GitLab 8.5' end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - get ':id/builds/:build_id/artifacts' do + get ':id/jobs/:job_id/artifacts' do authorize_read_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) present_artifacts!(build.artifacts_file) end - desc 'Download the artifacts file from build' do + desc 'Download the artifacts file from a job' do detail 'This feature was introduced in GitLab 8.10' end params do requires :ref_name, type: String, desc: 'The ref from repository' - requires :job, type: String, desc: 'The name for the build' + requires :job, type: String, desc: 'The name for the job' end - get ':id/builds/artifacts/:ref_name/download', + get ':id/jobs/artifacts/:ref_name/download', requirements: { ref_name: /.+/ } do authorize_read_builds! @@ -109,14 +110,14 @@ module API # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace # is saved in the DB instead of file). But before that, we need to consider how to replace the value of # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. - desc 'Get a trace of a specific build of a project' + desc 'Get a trace of a specific job of a project' params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - get ':id/builds/:build_id/trace' do + get ':id/jobs/:job_id/trace' do authorize_read_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" content_type 'text/plain' @@ -126,95 +127,95 @@ module API body trace end - desc 'Cancel a specific build of a project' do - success Entities::Build + desc 'Cancel a specific job of a project' do + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - post ':id/builds/:build_id/cancel' do + post ':id/jobs/:job_id/cancel' do authorize_update_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) build.cancel - present build, with: Entities::Build, + present build, with: Entities::Job, user_can_download_artifacts: can?(current_user, :read_build, user_project) end desc 'Retry a specific build of a project' do - success Entities::Build + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a build' end - post ':id/builds/:build_id/retry' do + post ':id/jobs/:job_id/retry' do authorize_update_builds! - build = get_build!(params[:build_id]) - return forbidden!('Build is not retryable') unless build.retryable? + build = get_build!(params[:job_id]) + return forbidden!('Job is not retryable') unless build.retryable? build = Ci::Build.retry(build, current_user) - present build, with: Entities::Build, + present build, with: Entities::Job, user_can_download_artifacts: can?(current_user, :read_build, user_project) end - desc 'Erase build (remove artifacts and build trace)' do - success Entities::Build + desc 'Erase job (remove artifacts and the trace)' do + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a build' end - post ':id/builds/:build_id/erase' do + post ':id/jobs/:job_id/erase' do authorize_update_builds! - build = get_build!(params[:build_id]) - return forbidden!('Build is not erasable!') unless build.erasable? + build = get_build!(params[:job_id]) + return forbidden!('Job is not erasable!') unless build.erasable? build.erase(erased_by: current_user) - present build, with: Entities::Build, + present build, with: Entities::Job, user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) end desc 'Keep the artifacts to prevent them from being deleted' do - success Entities::Build + success Entities::Job end params do - requires :build_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a job' end - post ':id/builds/:build_id/artifacts/keep' do + post ':id/jobs/:job_id/artifacts/keep' do authorize_update_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) return not_found!(build) unless build.artifacts? build.keep_artifacts! status 200 - present build, with: Entities::Build, + present build, with: Entities::Job, user_can_download_artifacts: can?(current_user, :read_build, user_project) end - desc 'Trigger a manual build' do - success Entities::Build + desc 'Trigger a manual job' do + success Entities::Job detail 'This feature was added in GitLab 8.11' end params do - requires :build_id, type: Integer, desc: 'The ID of a Build' + requires :job_id, type: Integer, desc: 'The ID of a Job' end - post ":id/builds/:build_id/play" do + post ":id/jobs/:job_id/play" do authorize_read_builds! - build = get_build!(params[:build_id]) + build = get_build!(params[:job_id]) bad_request!("Unplayable Job") unless build.playable? build.play(current_user) status 200 - present build, with: Entities::Build, + present build, with: Entities::Job, user_can_download_artifacts: can?(current_user, :read_build, user_project) end end diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb new file mode 100644 index 00000000000..33f9cfa6927 --- /dev/null +++ b/lib/api/v3/builds.rb @@ -0,0 +1,263 @@ +module API + module V3 + class Builds < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + helpers do + params :optional_scope do + optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', + values: ['pending', 'running', 'failed', 'success', 'canceled'], + coerce_with: ->(scope) { + if scope.is_a?(String) + [scope] + elsif scope.is_a?(Hashie::Mash) + scope.values + else + ['unknown'] + end + } + end + end + + desc 'Get a project builds' do + success V3::Entities::Build + end + params do + use :optional_scope + use :pagination + end + get ':id/builds' do + builds = user_project.builds.order('id DESC') + builds = filter_builds(builds, params[:scope]) + + present paginate(builds), with: Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Get builds for a specific commit of a project' do + success Entities::Build + end + params do + requires :sha, type: String, desc: 'The SHA id of a commit' + use :optional_scope + use :pagination + end + get ':id/repository/commits/:sha/builds' do + authorize_read_builds! + + return not_found! unless user_project.commit(params[:sha]) + + pipelines = user_project.pipelines.where(sha: params[:sha]) + builds = user_project.builds.where(pipeline: pipelines).order('id DESC') + builds = filter_builds(builds, params[:scope]) + + present paginate(builds), with: Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Get a specific build of a project' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + get ':id/builds/:build_id' do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Download the artifacts file from build' do + detail 'This feature was introduced in GitLab 8.5' + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + get ':id/builds/:build_id/artifacts' do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + present_artifacts!(build.artifacts_file) + end + + desc 'Download the artifacts file from build' do + detail 'This feature was introduced in GitLab 8.10' + end + params do + requires :ref_name, type: String, desc: 'The ref from repository' + requires :job, type: String, desc: 'The name for the build' + end + get ':id/builds/artifacts/:ref_name/download', + requirements: { ref_name: /.+/ } do + authorize_read_builds! + + builds = user_project.latest_successful_builds_for(params[:ref_name]) + latest_build = builds.find_by!(name: params[:job]) + + present_artifacts!(latest_build.artifacts_file) + end + + # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace + # is saved in the DB instead of file). But before that, we need to consider how to replace the value of + # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. + desc 'Get a trace of a specific build of a project' + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + get ':id/builds/:build_id/trace' do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" + content_type 'text/plain' + env['api.format'] = :binary + + trace = build.trace + body trace + end + + desc 'Cancel a specific build of a project' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/cancel' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + + build.cancel + + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Retry a specific build of a project' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/retry' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + return forbidden!('Build is not retryable') unless build.retryable? + + build = Ci::Build.retry(build, current_user) + + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Erase build (remove artifacts and build trace)' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/erase' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + return forbidden!('Build is not erasable!') unless build.erasable? + + build.erase(erased_by: current_user) + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + end + + desc 'Keep the artifacts to prevent them from being deleted' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end + post ':id/builds/:build_id/artifacts/keep' do + authorize_update_builds! + + build = get_build!(params[:build_id]) + return not_found!(build) unless build.artifacts? + + build.keep_artifacts! + + status 200 + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + + desc 'Trigger a manual build' do + success Entities::Build + detail 'This feature was added in GitLab 8.11' + end + params do + requires :build_id, type: Integer, desc: 'The ID of a Build' + end + post ":id/builds/:build_id/play" do + authorize_read_builds! + + build = get_build!(params[:build_id]) + + bad_request!("Unplayable Job") unless build.playable? + + build.play(current_user) + + status 200 + present build, with: Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) + end + end + + helpers do + def get_build(id) + user_project.builds.find_by(id: id.to_i) + end + + def get_build!(id) + get_build(id) || not_found! + end + + def present_artifacts!(artifacts_file) + if !artifacts_file.file_storage? + redirect_to(build.artifacts_file.url) + elsif artifacts_file.exists? + present_file!(artifacts_file.path, artifacts_file.filename) + else + not_found! + end + end + + def filter_builds(builds, scope) + return builds if scope.nil? || scope.empty? + + available_statuses = ::CommitStatus::AVAILABLE_STATUSES + + unknown = scope - available_statuses + render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? + + builds.where(status: available_statuses && scope) + end + + def authorize_read_builds! + authorize! :read_build, user_project + end + + def authorize_update_builds! + authorize! :update_build, user_project + end + end + end + end +end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 69853d33bec..7c7bcad490b 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -195,6 +195,16 @@ module API class TriggerRequest < Grape::Entity expose :id, :variables end + + class Build < Grape::Entity + expose :id, :status, :stage, :name, :ref, :tag, :coverage + expose :created_at, :started_at, :finished_at + expose :user, with: ::API::Entities::User + expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? } + expose :commit, with: ::API::Entities::RepoCommit + expose :runner, with: ::API::Entities::Runner + expose :pipeline, with: ::API::Entities::PipelineBasic + end end end end diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb new file mode 100644 index 00000000000..f0e08e73763 --- /dev/null +++ b/spec/requests/api/jobs_spec.rb @@ -0,0 +1,477 @@ +require 'spec_helper' + +describe API::Jobs, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:api_user) { user } + let!(:project) { create(:project, :repository, creator: user, public_builds: false) } + let!(:developer) { create(:project_member, :developer, user: user, project: project) } + let(:reporter) { create(:project_member, :reporter, project: project) } + let(:guest) { create(:project_member, :guest, project: project) } + let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } + let!(:build) { create(:ci_build, pipeline: pipeline) } + + describe 'GET /projects/:id/jobs' do + let(:query) { Hash.new } + + before do + get api("/projects/#{project.id}/jobs", api_user), query + end + + context 'authorized user' do + it 'returns project jobs' do + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + end + + it 'returns correct values' do + expect(json_response).not_to be_empty + expect(json_response.first['commit']['id']).to eq project.commit.id + end + + it 'returns pipeline data' do + json_build = json_response.first + + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end + + context 'filter project with one scope element' do + let(:query) { { 'scope' => 'pending' } } + + it do + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + end + + context 'filter project with array of scope elements' do + let(:query) { { 'scope[0]' => 'pending', 'scope[1]' => 'running' } } + + it do + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + end + + context 'respond 400 when scope contains invalid state' do + let(:query) { { 'scope[0]' => 'unknown', 'scope[1]' => 'running' } } + + it { expect(response).to have_http_status(400) } + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return project builds' do + expect(response).to have_http_status(401) + end + end + end + + describe 'GET /projects/:id/repository/commits/:sha/jobs' do + context 'when commit does not exist in repository' do + before do + get api("/projects/#{project.id}/repository/commits/1a271fd1/jobs", api_user) + end + + it 'responds with 404' do + expect(response).to have_http_status(404) + end + end + + context 'when commit exists in repository' do + context 'when user is authorized' do + context 'when pipeline has jobs' do + before do + create(:ci_pipeline, project: project, sha: project.commit.id) + create(:ci_build, pipeline: pipeline) + create(:ci_build) + + get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/jobs", api_user) + end + + it 'returns project jobs for specific commit' do + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq 2 + end + + it 'returns pipeline data' do + json_build = json_response.first + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end + end + + context 'when pipeline has no jobs' do + before do + branch_head = project.commit('feature').id + get api("/projects/#{project.id}/repository/commits/#{branch_head}/jobs", api_user) + end + + it 'returns an empty array' do + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response).to be_empty + end + end + end + + context 'when user is not authorized' do + before do + create(:ci_pipeline, project: project, sha: project.commit.id) + create(:ci_build, pipeline: pipeline) + + get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/jobs", nil) + end + + it 'does not return project jobs' do + expect(response).to have_http_status(401) + expect(json_response.except('message')).to be_empty + end + end + end + end + + describe 'GET /projects/:id/jobs/:job_id' do + before do + get api("/projects/#{project.id}/jobs/#{build.id}", api_user) + end + + context 'authorized user' do + it 'returns specific job data' do + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('test') + end + + it 'returns pipeline data' do + json_build = json_response + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return specific job data' do + expect(response).to have_http_status(401) + end + end + end + + describe 'GET /projects/:id/jobs/:job_id/artifacts' do + before do + get api("/projects/#{project.id}/jobs/#{build.id}/artifacts", api_user) + end + + context 'job with artifacts' do + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + context 'authorized user' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } + end + + it 'returns specific job artifacts' do + expect(response).to have_http_status(200) + expect(response.headers).to include(download_headers) + expect(response.body).to match_file(build.artifacts_file.file.file) + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return specific job artifacts' do + expect(response).to have_http_status(401) + end + end + end + + it 'does not return job artifacts if not uploaded' do + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do + let(:api_user) { reporter.user } + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + before do + build.success + end + + def get_for_ref(ref = pipeline.ref, job = build.name) + get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), job: job + end + + context 'when not logged in' do + let(:api_user) { nil } + + before do + get_for_ref + end + + it 'gives 401' do + expect(response).to have_http_status(401) + end + end + + context 'when logging as guest' do + let(:api_user) { guest.user } + + before do + get_for_ref + end + + it 'gives 403' do + expect(response).to have_http_status(403) + end + end + + context 'non-existing job' do + shared_examples 'not found' do + it { expect(response).to have_http_status(:not_found) } + end + + context 'has no such ref' do + before do + get_for_ref('TAIL') + end + + it_behaves_like 'not found' + end + + context 'has no such job' do + before do + get_for_ref(pipeline.ref, 'NOBUILD') + end + + it_behaves_like 'not found' + end + end + + context 'find proper job' do + shared_examples 'a valid file' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => + "attachment; filename=#{build.artifacts_file.filename}" } + end + + it { expect(response).to have_http_status(200) } + it { expect(response.headers).to include(download_headers) } + end + + context 'with regular branch' do + before do + pipeline.reload + pipeline.update(ref: 'master', + sha: project.commit('master').sha) + + get_for_ref('master') + end + + it_behaves_like 'a valid file' + end + + context 'with branch name containing slash' do + before do + pipeline.reload + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + end + + before do + get_for_ref('improve/awesome') + end + + it_behaves_like 'a valid file' + end + end + end + + describe 'GET /projects/:id/jobs/:job_id/trace' do + let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + + before do + get api("/projects/#{project.id}/jobs/#{build.id}/trace", api_user) + end + + context 'authorized user' do + it 'returns specific job trace' do + expect(response).to have_http_status(200) + expect(response.body).to eq(build.trace) + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return specific job trace' do + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/jobs/:job_id/cancel' do + before do + post api("/projects/#{project.id}/jobs/#{build.id}/cancel", api_user) + end + + context 'authorized user' do + context 'user with :update_build persmission' do + it 'cancels running or pending job' do + expect(response).to have_http_status(201) + expect(project.builds.first.status).to eq('canceled') + end + end + + context 'user without :update_build permission' do + let(:api_user) { reporter.user } + + it 'does not cancel job' do + expect(response).to have_http_status(403) + end + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not cancel job' do + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/jobs/:job_id/retry' do + let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } + + before do + post api("/projects/#{project.id}/jobs/#{build.id}/retry", api_user) + end + + context 'authorized user' do + context 'user with :update_build permission' do + it 'retries non-running job' do + expect(response).to have_http_status(201) + expect(project.builds.first.status).to eq('canceled') + expect(json_response['status']).to eq('pending') + end + end + + context 'user without :update_build permission' do + let(:api_user) { reporter.user } + + it 'does not retry job' do + expect(response).to have_http_status(403) + end + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not retry job' do + expect(response).to have_http_status(401) + end + end + end + + describe 'POST /projects/:id/jobs/:job_id/erase' do + before do + post api("/projects/#{project.id}/jobs/#{build.id}/erase", user) + end + + context 'job is erasable' do + let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } + + it 'erases job content' do + expect(response).to have_http_status(201) + expect(build.trace).to be_empty + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy + end + + it 'updates job' do + build.reload + expect(build.erased_at).to be_truthy + expect(build.erased_by).to eq(user) + end + end + + context 'job is not erasable' do + let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } + + it 'responds with forbidden' do + expect(response).to have_http_status(403) + end + end + end + + describe 'POST /projects/:id/jobs/:build_id/artifacts/keep' do + before do + post api("/projects/#{project.id}/jobs/#{build.id}/artifacts/keep", user) + end + + context 'artifacts did not expire' do + let(:build) do + create(:ci_build, :trace, :artifacts, :success, + project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) + end + + it 'keeps artifacts' do + expect(response).to have_http_status(200) + expect(build.reload.artifacts_expire_at).to be_nil + end + end + + context 'no artifacts' do + let(:build) { create(:ci_build, project: project, pipeline: pipeline) } + + it 'responds with not found' do + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /projects/:id/jobs/:job_id/play' do + before do + post api("/projects/#{project.id}/jobs/#{build.id}/play", user) + end + + context 'on an playable job' do + let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } + + it 'plays the job' do + expect(response).to have_http_status(200) + expect(json_response['user']['id']).to eq(user.id) + expect(json_response['id']).to eq(build.id) + end + end + + context 'on a non-playable job' do + it 'returns a status code 400, Bad Request' do + expect(response).to have_http_status 400 + expect(response.body).to match("Unplayable Job") + end + end + end +end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb similarity index 99% rename from spec/requests/api/builds_spec.rb rename to spec/requests/api/v3/builds_spec.rb index 76a10a2374c..41ccb0fa3ef 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/v3/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::Builds, api: true do +describe API::V3::Builds, api: true do include ApiHelpers let(:user) { create(:user) } From b5b93f80e57437163c45f8fef056a2a0b58e643e Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 23 Feb 2017 09:16:20 +0100 Subject: [PATCH 72/95] Update entities, rename from builds to jobs This commit only renames the commits, the cascading effects will be dealt with later. --- lib/api/entities.rb | 14 +++++--------- spec/requests/api/groups_spec.rb | 3 +++ spec/requests/api/v3/builds_spec.rb | 28 ++++++++++++++-------------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 22e2c4088d7..3db67ff455b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -50,7 +50,7 @@ module API class ProjectHook < Hook expose :project_id, :issues_events, :merge_requests_events expose :note_events, :pipeline_events, :wiki_page_events - expose :job_events, as: :build_events + expose :build_events, as: :job_events end class BasicProjectDetails < Grape::Entity @@ -94,7 +94,7 @@ module API expose :star_count, :forks_count expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } - expose :public_jobs, as: :public_builds + expose :public_builds, as: :public_jobs expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links.all, options) end @@ -110,7 +110,7 @@ module API expose :storage_size expose :repository_size expose :lfs_objects_size - expose :job_artifacts_size, as: :build_artifacts_size + expose :build_artifacts_size, as: :job_artifacts_size end class Member < UserBasic @@ -145,7 +145,7 @@ module API expose :storage_size expose :repository_size expose :lfs_objects_size - expose :job_artifacts_size, as: :build_artifacts_size + expose :build_artifacts_size, as: :job_artifacts_size end end end @@ -450,7 +450,7 @@ module API expose :id, :title, :created_at, :updated_at, :active expose :push_events, :issues_events, :merge_requests_events expose :tag_push_events, :note_events, :pipeline_events - expose :job_events, as: :build_events + expose :build_events, as: :job_events # Expose serialized properties expose :properties do |service, options| field_names = service.fields. @@ -618,15 +618,11 @@ module API end end -<<<<<<< HEAD class RunnerRegistrationDetails < Grape::Entity expose :id, :token end - class BuildArtifactFile < Grape::Entity -======= class JobArtifactFile < Grape::Entity ->>>>>>> 239b5f49c5... Rename Builds to Jobs in the API expose :filename, :size end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 2b8fd7e31a1..858eb6a9635 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -76,6 +76,9 @@ describe API::Groups, api: true do lfs_objects_size: 234, build_artifacts_size: 345, }.stringify_keys + exposed_attributes = attributes.dup + exposed_attributes['job_artifacts_size'] = exposed_attributes['build_artifacts_size'] + exposed_attributes.delete('build_artifacts_size') project1.statistics.update!(attributes) diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb index 41ccb0fa3ef..a50c22a6dd1 100644 --- a/spec/requests/api/v3/builds_spec.rb +++ b/spec/requests/api/v3/builds_spec.rb @@ -18,7 +18,7 @@ describe API::V3::Builds, api: true do before do create(:ci_build, :skipped, pipeline: pipeline) - get api("/projects/#{project.id}/builds?#{query}", api_user) + get v3_api("/projects/#{project.id}/builds?#{query}", api_user) end context 'authorized user' do @@ -91,7 +91,7 @@ describe API::V3::Builds, api: true do describe 'GET /projects/:id/repository/commits/:sha/builds' do context 'when commit does not exist in repository' do before do - get api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user) + get v3_api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user) end it 'responds with 404' do @@ -107,7 +107,7 @@ describe API::V3::Builds, api: true do create(:ci_build, pipeline: pipeline) create(:ci_build) - get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) + get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) end it 'returns project jobs for specific commit' do @@ -130,7 +130,7 @@ describe API::V3::Builds, api: true do context 'when pipeline has no jobs' do before do branch_head = project.commit('feature').id - get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) + get v3_api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) end it 'returns an empty array' do @@ -146,7 +146,7 @@ describe API::V3::Builds, api: true do create(:ci_pipeline, project: project, sha: project.commit.id) create(:ci_build, pipeline: pipeline) - get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) + get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) end it 'does not return project jobs' do @@ -159,7 +159,7 @@ describe API::V3::Builds, api: true do describe 'GET /projects/:id/builds/:build_id' do before do - get api("/projects/#{project.id}/builds/#{build.id}", api_user) + get v3_api("/projects/#{project.id}/builds/#{build.id}", api_user) end context 'authorized user' do @@ -189,7 +189,7 @@ describe API::V3::Builds, api: true do describe 'GET /projects/:id/builds/:build_id/artifacts' do before do - get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) + get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) end context 'job with artifacts' do @@ -231,7 +231,7 @@ describe API::V3::Builds, api: true do end def path_for_ref(ref = pipeline.ref, job = build.name) - api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) + v3_api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) end context 'when not logged in' do @@ -324,7 +324,7 @@ describe API::V3::Builds, api: true do let(:build) { create(:ci_build, :trace, pipeline: pipeline) } before do - get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) + get v3_api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) end context 'authorized user' do @@ -345,7 +345,7 @@ describe API::V3::Builds, api: true do describe 'POST /projects/:id/builds/:build_id/cancel' do before do - post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) end context 'authorized user' do @@ -378,7 +378,7 @@ describe API::V3::Builds, api: true do let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } before do - post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) end context 'authorized user' do @@ -410,7 +410,7 @@ describe API::V3::Builds, api: true do describe 'POST /projects/:id/builds/:build_id/erase' do before do - post api("/projects/#{project.id}/builds/#{build.id}/erase", user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user) end context 'job is erasable' do @@ -440,7 +440,7 @@ describe API::V3::Builds, api: true do describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do before do - post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user) end context 'artifacts did not expire' do @@ -466,7 +466,7 @@ describe API::V3::Builds, api: true do describe 'POST /projects/:id/builds/:build_id/play' do before do - post api("/projects/#{project.id}/builds/#{build.id}/play", user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/play", user) end context 'on an playable job' do From 0fb022773892c76c8a6d7a8cf049d9c6fc2c8891 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 23 Feb 2017 12:14:27 +0100 Subject: [PATCH 73/95] Keep entities the same for API v3 --- lib/api/v3/entities.rb | 143 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 7c7bcad490b..7b81d159b06 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -205,6 +205,149 @@ module API expose :runner, with: ::API::Entities::Runner expose :pipeline, with: ::API::Entities::PipelineBasic end + + class BuildArtifactFile < Grape::Entity + expose :filename, :size + end + + class EnvironmentBasic < Grape::Entity + expose :id, :name, :slug, :external_url + end + + class Environment < EnvironmentBasic + expose :project, using: Entities::Project + end + + class Deployment < Grape::Entity + expose :id, :iid, :ref, :sha, :created_at + expose :user, using: Entities::UserBasic + expose :environment, using: Entities::EnvironmentBasic + expose :deployable, using: Entities::Build + end + + class Group < Grape::Entity + expose :id, :name, :path, :description, :visibility_level + expose :lfs_enabled?, as: :lfs_enabled + expose :avatar_url + expose :web_url + expose :request_access_enabled + expose :statistics, if: :statistics do + with_options format_with: -> (value) { value.to_i } do + expose :storage_size + expose :repository_size + expose :lfs_objects_size + expose :build_artifacts_size + end + end + end + + class GroupDetail < Group + expose :projects, using: Entities::Project + expose :shared_projects, using: Entities::Project + end + + class MergeRequest < ProjectEntity + expose :target_branch, :source_branch + expose :upvotes, :downvotes + expose :author, :assignee, using: Entities::UserBasic + expose :source_project_id, :target_project_id + expose :label_names, as: :labels + expose :work_in_progress?, as: :work_in_progress + expose :milestone, using: Entities::Milestone + expose :merge_when_build_succeeds + expose :merge_status + expose :diff_head_sha, as: :sha + expose :merge_commit_sha + expose :subscribed do |merge_request, options| + merge_request.subscribed?(options[:current_user], options[:project]) + end + expose :user_notes_count + expose :should_remove_source_branch?, as: :should_remove_source_branch + expose :force_remove_source_branch?, as: :force_remove_source_branch + expose :web_url do |merge_request, options| + Gitlab::UrlBuilder.build(merge_request) + end + end + + class MergeRequestChanges < MergeRequest + expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _| + compare.raw_diffs(all_diffs: true).to_a + end + end + + class Project < Grape::Entity + expose :id, :description, :default_branch, :tag_list + expose :public?, as: :public + expose :archived?, as: :archived + expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url + expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } + expose :name, :name_with_namespace + expose :path, :path_with_namespace + expose :container_registry_enabled + # Expose old field names with the new permissions methods to keep API compatible + expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } + expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } + expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } + expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } + expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } + expose :created_at, :last_activity_at + expose :shared_runners_enabled + expose :lfs_enabled?, as: :lfs_enabled + expose :creator_id + expose :namespace, using: 'API::Entities::Namespace' + expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } + expose :avatar_url + expose :star_count, :forks_count + expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } + expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } + expose :public_builds + expose :shared_with_groups do |project, options| + SharedGroup.represent(project.project_group_links.all, options) + end + expose :only_allow_merge_if_build_succeeds + expose :request_access_enabled + expose :only_allow_merge_if_all_discussions_are_resolved + expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics + end + + class ProjectStatistics < Grape::Entity + expose :commit_count + expose :storage_size + expose :repository_size + expose :lfs_objects_size + expose :build_artifacts_size + end + + class ProjectService < Grape::Entity + expose :id, :title, :created_at, :updated_at, :active + expose :push_events, :issues_events, :merge_requests_events + expose :tag_push_events, :note_events, :build_events, :pipeline_events + # Expose serialized properties + expose :properties do |service, options| + field_names = service.fields. + select { |field| options[:include_passwords] || field[:type] != 'password' }. + map { |field| field[:name] } + service.properties.slice(*field_names) + end + end + + class ProjectHook < Hook + expose :project_id, :issues_events, :merge_requests_events + expose :note_events, :build_events, :pipeline_events, :wiki_page_events + end + + class ProjectWithAccess < Project + expose :permissions do + expose :project_access, using: Entities::ProjectAccess do |project, options| + project.project_members.find_by(user_id: options[:current_user].id) + end + expose :group_access, using: Entities::GroupAccess do |project, options| + if project.group + project.group.group_members.find_by(user_id: options[:current_user].id) + end + end + end + end end end end From f44ab8e8ec49e643cd7fea20092b63e2603bb8bd Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 23 Feb 2017 12:41:07 +0100 Subject: [PATCH 74/95] Pick API files from 8.16.6 --- lib/api/v3/deployments.rb | 41 ++++ lib/api/v3/merge_request_diffs.rb | 41 ++++ lib/api/v3/project_hooks.rb | 104 +++++++++ lib/api/v3/services.rb | 53 ++++- spec/requests/api/v3/deployments_spec.rb | 60 +++++ .../api/v3/merge_request_diffs_spec.rb | 49 ++++ spec/requests/api/v3/notes_spec.rb | 64 ++++++ spec/requests/api/v3/project_hooks_spec.rb | 215 ++++++++++++++++++ 8 files changed, 625 insertions(+), 2 deletions(-) create mode 100644 lib/api/v3/deployments.rb create mode 100644 lib/api/v3/merge_request_diffs.rb create mode 100644 lib/api/v3/project_hooks.rb create mode 100644 spec/requests/api/v3/deployments_spec.rb create mode 100644 spec/requests/api/v3/merge_request_diffs_spec.rb create mode 100644 spec/requests/api/v3/project_hooks_spec.rb diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb new file mode 100644 index 00000000000..c5feb49b22f --- /dev/null +++ b/lib/api/v3/deployments.rb @@ -0,0 +1,41 @@ +module API + # Deployments RESTfull API endpoints + class Deployments < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The project ID' + end + resource :projects do + desc 'Get all deployments of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Deployment + end + params do + use :pagination + end + get ':id/deployments' do + authorize! :read_deployment, user_project + + present paginate(user_project.deployments), with: Entities::Deployment + end + + desc 'Gets a specific deployment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Deployment + end + params do + requires :deployment_id, type: Integer, desc: 'The deployment ID' + end + get ':id/deployments/:deployment_id' do + authorize! :read_deployment, user_project + + deployment = user_project.deployments.find(params[:deployment_id]) + + present deployment, with: Entities::Deployment + end + end + end +end diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb new file mode 100644 index 00000000000..bc3d69f6904 --- /dev/null +++ b/lib/api/v3/merge_request_diffs.rb @@ -0,0 +1,41 @@ +module API + # MergeRequestDiff API + class MergeRequestDiffs < Grape::API + before { authenticate! } + + resource :projects do + desc 'Get a list of merge request diff versions' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::MergeRequestDiff + end + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + end + + get ":id/merge_requests/:merge_request_id/versions" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + + present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff + end + + desc 'Get a single merge request diff version' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::MergeRequestDiffFull + end + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' + end + + get ":id/merge_requests/:merge_request_id/versions/:version_id" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + + present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull + end + end + end +end diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb new file mode 100644 index 00000000000..cb679e6658a --- /dev/null +++ b/lib/api/v3/project_hooks.rb @@ -0,0 +1,104 @@ +module API + class ProjectHooks < Grape::API + include PaginationParams + + before { authenticate! } + before { authorize_admin_project } + + helpers do + params :project_hook_properties do + requires :url, type: String, desc: "The URL to send the request to" + optional :push_events, type: Boolean, desc: "Trigger hook on push events" + optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" + optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" + optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" + optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" + optional :build_events, type: Boolean, desc: "Trigger hook on build events" + optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" + optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" + optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" + optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get project hooks' do + success Entities::ProjectHook + end + params do + use :pagination + end + get ":id/hooks" do + hooks = paginate user_project.hooks + + present hooks, with: Entities::ProjectHook + end + + desc 'Get a project hook' do + success Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: 'The ID of a project hook' + end + get ":id/hooks/:hook_id" do + hook = user_project.hooks.find(params[:hook_id]) + present hook, with: Entities::ProjectHook + end + + desc 'Add hook to project' do + success Entities::ProjectHook + end + params do + use :project_hook_properties + end + post ":id/hooks" do + hook = user_project.hooks.new(declared_params(include_missing: false)) + + if hook.save + present hook, with: Entities::ProjectHook + else + error!("Invalid url given", 422) if hook.errors[:url].present? + + not_found!("Project hook #{hook.errors.messages}") + end + end + + desc 'Update an existing project hook' do + success Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: "The ID of the hook to update" + use :project_hook_properties + end + put ":id/hooks/:hook_id" do + hook = user_project.hooks.find(params.delete(:hook_id)) + + if hook.update_attributes(declared_params(include_missing: false)) + present hook, with: Entities::ProjectHook + else + error!("Invalid url given", 422) if hook.errors[:url].present? + + not_found!("Project hook #{hook.errors.messages}") + end + end + + desc 'Deletes project hook' do + success Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' + end + delete ":id/hooks/:hook_id" do + begin + present user_project.hooks.destroy(params[:hook_id]), with: Entities::ProjectHook + rescue + # ProjectHook can raise Error if hook_id not found + not_found!("Error deleting hook #{params[:hook_id]}") + end + end + end + end +end diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb index af0a058f69b..de24e6418c7 100644 --- a/lib/api/v3/services.rb +++ b/lib/api/v3/services.rb @@ -561,13 +561,62 @@ module API end if service.update_attributes(attrs.merge(active: false)) - status(200) true else render_api_error!('400 Bad Request', 400) end end + + desc 'Get the service settings for project' do + success Entities::ProjectService + end + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + get ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) + present service, with: Entities::ProjectService, include_passwords: current_user.is_admin? + end + end + + trigger_services.each do |service_slug, settings| + helpers do + def chat_command_service(project, service_slug, params) + project.services.active.where(template: false).find do |service| + service.try(:token) == params[:token] && service.to_param == service_slug.underscore + end + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc "Trigger a slash command for #{service_slug}" do + detail 'Added in GitLab 8.13' + end + params do + settings.each do |setting| + requires setting[:name], type: setting[:type], desc: setting[:desc] + end + end + post ":id/services/#{service_slug.underscore}/trigger" do + project = find_project(params[:id]) + + # This is not accurate, but done to prevent leakage of the project names + not_found!('Service') unless project + + service = chat_command_service(project, service_slug, params) + result = service.try(:trigger, params) + + if result + status result[:status] || 200 + present result + else + not_found!('Service') + end + end + end end end end -end diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb new file mode 100644 index 00000000000..31e3cfa1b2f --- /dev/null +++ b/spec/requests/api/v3/deployments_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe API::Deployments, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { deployment.environment.project } + let!(:deployment) { create(:deployment) } + + before do + project.team << [user, :master] + end + + describe 'GET /projects/:id/deployments' do + context 'as member of the project' do + it_behaves_like 'a paginated resources' do + let(:request) { get api("/projects/#{project.id}/deployments", user) } + end + + it 'returns projects deployments' do + get api("/projects/#{project.id}/deployments", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['iid']).to eq(deployment.iid) + expect(json_response.first['sha']).to match /\A\h{40}\z/ + end + end + + context 'as non member' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/deployments", non_member) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/deployments/:deployment_id' do + context 'as a member of the project' do + it 'returns the projects deployment' do + get api("/projects/#{project.id}/deployments/#{deployment.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['sha']).to match /\A\h{40}\z/ + expect(json_response['id']).to eq(deployment.id) + end + end + + context 'as non member' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb new file mode 100644 index 00000000000..e1887138aab --- /dev/null +++ b/spec/requests/api/v3/merge_request_diffs_spec.rb @@ -0,0 +1,49 @@ +require "spec_helper" + +describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do + include ApiHelpers + + let!(:user) { create(:user) } + let!(:merge_request) { create(:merge_request, importing: true) } + let!(:project) { merge_request.target_project } + + before do + merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') + project.team << [user, :master] + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do + it 'returns 200 for a valid merge request' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) + merge_request_diff = merge_request.merge_request_diffs.first + + expect(response.status).to eq 200 + expect(json_response.size).to eq(merge_request.merge_request_diffs.size) + expect(json_response.first['id']).to eq(merge_request_diff.id) + expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_requests/999/versions", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do + it 'returns a 200 for a valid merge request' do + merge_request_diff = merge_request.merge_request_diffs.first + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) + + expect(response.status).to eq 200 + expect(json_response['id']).to eq(merge_request_diff.id) + expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) + expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb index ddef2d5eb04..a2228132ba9 100644 --- a/spec/requests/api/v3/notes_spec.rb +++ b/spec/requests/api/v3/notes_spec.rb @@ -328,7 +328,11 @@ describe API::V3::Notes, api: true do end it 'returns a 400 bad request error if body not given' do +<<<<<<< HEAD put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ +======= + put api("/projects/#{project.id}/issues/#{issue.id}/"\ +>>>>>>> e306055d88... Pick API files from 8.16.6 "notes/#{issue_note.id}", user) expect(response).to have_http_status(400) @@ -337,7 +341,11 @@ describe API::V3::Notes, api: true do context 'when noteable is a Snippet' do it 'returns modified note' do +<<<<<<< HEAD put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ +======= + put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ +>>>>>>> e306055d88... Pick API files from 8.16.6 "notes/#{snippet_note.id}", user), body: 'Hello!' expect(response).to have_http_status(200) @@ -345,7 +353,11 @@ describe API::V3::Notes, api: true do end it 'returns a 404 error when note id not found' do +<<<<<<< HEAD put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ +======= + put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ +>>>>>>> e306055d88... Pick API files from 8.16.6 "notes/12345", user), body: "Hello!" expect(response).to have_http_status(404) @@ -354,7 +366,11 @@ describe API::V3::Notes, api: true do context 'when noteable is a Merge Request' do it 'returns modified note' do +<<<<<<< HEAD put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ +======= + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ +>>>>>>> e306055d88... Pick API files from 8.16.6 "notes/#{merge_request_note.id}", user), body: 'Hello!' expect(response).to have_http_status(200) @@ -362,7 +378,11 @@ describe API::V3::Notes, api: true do end it 'returns a 404 error when note id not found' do +<<<<<<< HEAD put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ +======= + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ +>>>>>>> e306055d88... Pick API files from 8.16.6 "notes/12345", user), body: "Hello!" expect(response).to have_http_status(404) @@ -373,6 +393,7 @@ describe API::V3::Notes, api: true do describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do context 'when noteable is an Issue' do it 'deletes a note' do +<<<<<<< HEAD delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) @@ -380,11 +401,24 @@ describe API::V3::Notes, api: true do # Check if note is really deleted delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) +======= + delete api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) + + expect(response).to have_http_status(200) + # Check if note is really deleted + delete api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) +>>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do +<<<<<<< HEAD delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) +======= + delete api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) +>>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end @@ -392,6 +426,7 @@ describe API::V3::Notes, api: true do context 'when noteable is a Snippet' do it 'deletes a note' do +<<<<<<< HEAD delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) @@ -399,12 +434,26 @@ describe API::V3::Notes, api: true do # Check if note is really deleted delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) +======= + delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user) + + expect(response).to have_http_status(200) + # Check if note is really deleted + delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user) +>>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do +<<<<<<< HEAD delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/12345", user) +======= + delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/12345", user) +>>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end @@ -412,6 +461,7 @@ describe API::V3::Notes, api: true do context 'when noteable is a Merge Request' do it 'deletes a note' do +<<<<<<< HEAD delete v3_api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) @@ -419,12 +469,26 @@ describe API::V3::Notes, api: true do # Check if note is really deleted delete v3_api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) +======= + delete api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/#{merge_request_note.id}", user) + + expect(response).to have_http_status(200) + # Check if note is really deleted + delete api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/#{merge_request_note.id}", user) +>>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do +<<<<<<< HEAD delete v3_api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/12345", user) +======= + delete api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/12345", user) +>>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb new file mode 100644 index 00000000000..36fbcf088e7 --- /dev/null +++ b/spec/requests/api/v3/project_hooks_spec.rb @@ -0,0 +1,215 @@ +require 'spec_helper' + +describe API::ProjectHooks, 'ProjectHooks', api: true do + include ApiHelpers + let(:user) { create(:user) } + let(:user3) { create(:user) } + let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } + let!(:hook) do + create(:project_hook, + :all_events_enabled, + project: project, + url: 'http://example.com', + enable_ssl_verification: true) + end + + before do + project.team << [user, :master] + project.team << [user3, :developer] + end + + describe "GET /projects/:id/hooks" do + context "authorized user" do + it "returns project hooks" do + get api("/projects/#{project.id}/hooks", user) + expect(response).to have_http_status(200) + + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['url']).to eq("http://example.com") + expect(json_response.first['issues_events']).to eq(true) + expect(json_response.first['push_events']).to eq(true) + expect(json_response.first['merge_requests_events']).to eq(true) + expect(json_response.first['tag_push_events']).to eq(true) + expect(json_response.first['note_events']).to eq(true) + expect(json_response.first['build_events']).to eq(true) + expect(json_response.first['pipeline_events']).to eq(true) + expect(json_response.first['wiki_page_events']).to eq(true) + expect(json_response.first['enable_ssl_verification']).to eq(true) + end + end + + context "unauthorized user" do + it "does not access project hooks" do + get api("/projects/#{project.id}/hooks", user3) + expect(response).to have_http_status(403) + end + end + end + + describe "GET /projects/:id/hooks/:hook_id" do + context "authorized user" do + it "returns a project hook" do + get api("/projects/#{project.id}/hooks/#{hook.id}", user) + expect(response).to have_http_status(200) + expect(json_response['url']).to eq(hook.url) + expect(json_response['issues_events']).to eq(hook.issues_events) + expect(json_response['push_events']).to eq(hook.push_events) + expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) + expect(json_response['tag_push_events']).to eq(hook.tag_push_events) + expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['pipeline_events']).to eq(hook.pipeline_events) + expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) + expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) + end + + it "returns a 404 error if hook id is not available" do + get api("/projects/#{project.id}/hooks/1234", user) + expect(response).to have_http_status(404) + end + end + + context "unauthorized user" do + it "does not access an existing hook" do + get api("/projects/#{project.id}/hooks/#{hook.id}", user3) + expect(response).to have_http_status(403) + end + end + + it "returns a 404 error if hook id is not available" do + get api("/projects/#{project.id}/hooks/1234", user) + expect(response).to have_http_status(404) + end + end + + describe "POST /projects/:id/hooks" do + it "adds hook to project" do + expect do + post api("/projects/#{project.id}/hooks", user), + url: "http://example.com", issues_events: true, wiki_page_events: true + end.to change {project.hooks.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response['url']).to eq('http://example.com') + expect(json_response['issues_events']).to eq(true) + expect(json_response['push_events']).to eq(true) + expect(json_response['merge_requests_events']).to eq(false) + expect(json_response['tag_push_events']).to eq(false) + expect(json_response['note_events']).to eq(false) + expect(json_response['build_events']).to eq(false) + expect(json_response['pipeline_events']).to eq(false) + expect(json_response['wiki_page_events']).to eq(true) + expect(json_response['enable_ssl_verification']).to eq(true) + expect(json_response).not_to include('token') + end + + it "adds the token without including it in the response" do + token = "secret token" + + expect do + post api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token + end.to change {project.hooks.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response["url"]).to eq("http://example.com") + expect(json_response).not_to include("token") + + hook = project.hooks.find(json_response["id"]) + + expect(hook.url).to eq("http://example.com") + expect(hook.token).to eq(token) + end + + it "returns a 400 error if url not given" do + post api("/projects/#{project.id}/hooks", user) + expect(response).to have_http_status(400) + end + + it "returns a 422 error if url not valid" do + post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" + expect(response).to have_http_status(422) + end + end + + describe "PUT /projects/:id/hooks/:hook_id" do + it "updates an existing project hook" do + put api("/projects/#{project.id}/hooks/#{hook.id}", user), + url: 'http://example.org', push_events: false + expect(response).to have_http_status(200) + expect(json_response['url']).to eq('http://example.org') + expect(json_response['issues_events']).to eq(hook.issues_events) + expect(json_response['push_events']).to eq(false) + expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) + expect(json_response['tag_push_events']).to eq(hook.tag_push_events) + expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['pipeline_events']).to eq(hook.pipeline_events) + expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) + expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) + end + + it "adds the token without including it in the response" do + token = "secret token" + + put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token + + expect(response).to have_http_status(200) + expect(json_response["url"]).to eq("http://example.org") + expect(json_response).not_to include("token") + + expect(hook.reload.url).to eq("http://example.org") + expect(hook.reload.token).to eq(token) + end + + it "returns 404 error if hook id not found" do + put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' + expect(response).to have_http_status(404) + end + + it "returns 400 error if url is not given" do + put api("/projects/#{project.id}/hooks/#{hook.id}", user) + expect(response).to have_http_status(400) + end + + it "returns a 422 error if url is not valid" do + put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' + expect(response).to have_http_status(422) + end + end + + describe "DELETE /projects/:id/hooks/:hook_id" do + it "deletes hook from project" do + expect do + delete api("/projects/#{project.id}/hooks/#{hook.id}", user) + end.to change {project.hooks.count}.by(-1) + expect(response).to have_http_status(200) + end + + it "returns success when deleting hook" do + delete api("/projects/#{project.id}/hooks/#{hook.id}", user) + expect(response).to have_http_status(200) + end + + it "returns a 404 error when deleting non existent hook" do + delete api("/projects/#{project.id}/hooks/42", user) + expect(response).to have_http_status(404) + end + + it "returns a 404 error if hook id not given" do + delete api("/projects/#{project.id}/hooks", user) + + expect(response).to have_http_status(404) + end + + it "returns a 404 if a user attempts to delete project hooks he/she does not own" do + test_user = create(:user) + other_project = create(:project) + other_project.team << [test_user, :master] + + delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) + expect(response).to have_http_status(404) + expect(WebHook.exists?(hook.id)).to be_truthy + end + end +end From 9e942b59720a4e22a16f71de66a8cf4706f3c92b Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Fri, 3 Mar 2017 16:39:29 +0100 Subject: [PATCH 75/95] Fix all tests This commit was about 6 commits before squashing, with the main goal to make all tests green. Now, after pushing this commit we'll see what the CI has to say about that. --- lib/api/v3/builds.rb | 68 ++++---- lib/api/v3/entities.rb | 110 +------------ lib/api/v3/merge_request_diffs.rb | 58 +++---- lib/api/v3/project_hooks.rb | 180 +++++++++++---------- lib/api/v3/services.rb | 19 +++ spec/requests/api/groups_spec.rb | 5 +- spec/requests/api/project_hooks_spec.rb | 8 +- spec/requests/api/projects_spec.rb | 4 +- spec/requests/api/v3/deployments_spec.rb | 11 ++ spec/requests/api/v3/notes_spec.rb | 64 -------- spec/requests/api/v3/project_hooks_spec.rb | 43 ++--- 11 files changed, 220 insertions(+), 350 deletions(-) diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb index 33f9cfa6927..c8feba13527 100644 --- a/lib/api/v3/builds.rb +++ b/lib/api/v3/builds.rb @@ -12,21 +12,21 @@ module API helpers do params :optional_scope do optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', - values: ['pending', 'running', 'failed', 'success', 'canceled'], - coerce_with: ->(scope) { - if scope.is_a?(String) - [scope] - elsif scope.is_a?(Hashie::Mash) - scope.values - else - ['unknown'] - end - } + values: %w(pending running failed success canceled skipped), + coerce_with: ->(scope) { + if scope.is_a?(String) + [scope] + elsif scope.is_a?(Hashie::Mash) + scope.values + else + ['unknown'] + end + } end end desc 'Get a project builds' do - success V3::Entities::Build + success ::API::V3::Entities::Build end params do use :optional_scope @@ -36,12 +36,12 @@ module API builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) - present paginate(builds), with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present paginate(builds), with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) end desc 'Get builds for a specific commit of a project' do - success Entities::Build + success ::API::V3::Entities::Build end params do requires :sha, type: String, desc: 'The SHA id of a commit' @@ -57,12 +57,12 @@ module API builds = user_project.builds.where(pipeline: pipelines).order('id DESC') builds = filter_builds(builds, params[:scope]) - present paginate(builds), with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present paginate(builds), with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) end desc 'Get a specific build of a project' do - success Entities::Build + success ::API::V3::Entities::Build end params do requires :build_id, type: Integer, desc: 'The ID of a build' @@ -72,8 +72,8 @@ module API build = get_build!(params[:build_id]) - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) end desc 'Download the artifacts file from build' do @@ -128,7 +128,7 @@ module API end desc 'Cancel a specific build of a project' do - success Entities::Build + success ::API::V3::Entities::Build end params do requires :build_id, type: Integer, desc: 'The ID of a build' @@ -140,12 +140,12 @@ module API build.cancel - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) end desc 'Retry a specific build of a project' do - success Entities::Build + success ::API::V3::Entities::Build end params do requires :build_id, type: Integer, desc: 'The ID of a build' @@ -158,12 +158,12 @@ module API build = Ci::Build.retry(build, current_user) - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) end desc 'Erase build (remove artifacts and build trace)' do - success Entities::Build + success ::API::V3::Entities::Build end params do requires :build_id, type: Integer, desc: 'The ID of a build' @@ -175,12 +175,12 @@ module API return forbidden!('Build is not erasable!') unless build.erasable? build.erase(erased_by: current_user) - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) end desc 'Keep the artifacts to prevent them from being deleted' do - success Entities::Build + success ::API::V3::Entities::Build end params do requires :build_id, type: Integer, desc: 'The ID of a build' @@ -194,12 +194,12 @@ module API build.keep_artifacts! status 200 - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) end desc 'Trigger a manual build' do - success Entities::Build + success ::API::V3::Entities::Build detail 'This feature was added in GitLab 8.11' end params do @@ -215,8 +215,8 @@ module API build.play(current_user) status 200 - present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build, + user_can_download_artifacts: can?(current_user, :read_build, user_project) end end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 7b81d159b06..832b4bdeb4f 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -81,7 +81,7 @@ module API expose :request_access_enabled expose :only_allow_merge_if_all_discussions_are_resolved - expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics + expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics end class ProjectWithAccess < Project @@ -210,106 +210,19 @@ module API expose :filename, :size end - class EnvironmentBasic < Grape::Entity - expose :id, :name, :slug, :external_url - end - - class Environment < EnvironmentBasic - expose :project, using: Entities::Project - end - class Deployment < Grape::Entity expose :id, :iid, :ref, :sha, :created_at - expose :user, using: Entities::UserBasic - expose :environment, using: Entities::EnvironmentBasic + expose :user, using: ::API::Entities::UserBasic + expose :environment, using: ::API::Entities::EnvironmentBasic expose :deployable, using: Entities::Build end - class Group < Grape::Entity - expose :id, :name, :path, :description, :visibility_level - expose :lfs_enabled?, as: :lfs_enabled - expose :avatar_url - expose :web_url - expose :request_access_enabled - expose :statistics, if: :statistics do - with_options format_with: -> (value) { value.to_i } do - expose :storage_size - expose :repository_size - expose :lfs_objects_size - expose :build_artifacts_size - end - end - end - - class GroupDetail < Group - expose :projects, using: Entities::Project - expose :shared_projects, using: Entities::Project - end - - class MergeRequest < ProjectEntity - expose :target_branch, :source_branch - expose :upvotes, :downvotes - expose :author, :assignee, using: Entities::UserBasic - expose :source_project_id, :target_project_id - expose :label_names, as: :labels - expose :work_in_progress?, as: :work_in_progress - expose :milestone, using: Entities::Milestone - expose :merge_when_build_succeeds - expose :merge_status - expose :diff_head_sha, as: :sha - expose :merge_commit_sha - expose :subscribed do |merge_request, options| - merge_request.subscribed?(options[:current_user], options[:project]) - end - expose :user_notes_count - expose :should_remove_source_branch?, as: :should_remove_source_branch - expose :force_remove_source_branch?, as: :force_remove_source_branch - expose :web_url do |merge_request, options| - Gitlab::UrlBuilder.build(merge_request) - end - end - class MergeRequestChanges < MergeRequest - expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _| + expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _| compare.raw_diffs(all_diffs: true).to_a end end - class Project < Grape::Entity - expose :id, :description, :default_branch, :tag_list - expose :public?, as: :public - expose :archived?, as: :archived - expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url - expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } - expose :name, :name_with_namespace - expose :path, :path_with_namespace - expose :container_registry_enabled - # Expose old field names with the new permissions methods to keep API compatible - expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } - expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } - expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } - expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } - expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } - expose :created_at, :last_activity_at - expose :shared_runners_enabled - expose :lfs_enabled?, as: :lfs_enabled - expose :creator_id - expose :namespace, using: 'API::Entities::Namespace' - expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } - expose :avatar_url - expose :star_count, :forks_count - expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } - expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } - expose :public_builds - expose :shared_with_groups do |project, options| - SharedGroup.represent(project.project_group_links.all, options) - end - expose :only_allow_merge_if_build_succeeds - expose :request_access_enabled - expose :only_allow_merge_if_all_discussions_are_resolved - expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics - end - class ProjectStatistics < Grape::Entity expose :commit_count expose :storage_size @@ -331,23 +244,10 @@ module API end end - class ProjectHook < Hook + class ProjectHook < ::API::Entities::Hook expose :project_id, :issues_events, :merge_requests_events expose :note_events, :build_events, :pipeline_events, :wiki_page_events end - - class ProjectWithAccess < Project - expose :permissions do - expose :project_access, using: Entities::ProjectAccess do |project, options| - project.project_members.find_by(user_id: options[:current_user].id) - end - expose :group_access, using: Entities::GroupAccess do |project, options| - if project.group - project.group.group_members.find_by(user_id: options[:current_user].id) - end - end - end - end end end end diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb index bc3d69f6904..a462803e26c 100644 --- a/lib/api/v3/merge_request_diffs.rb +++ b/lib/api/v3/merge_request_diffs.rb @@ -1,40 +1,42 @@ module API - # MergeRequestDiff API - class MergeRequestDiffs < Grape::API - before { authenticate! } + module V3 + # MergeRequestDiff API + class MergeRequestDiffs < Grape::API + before { authenticate! } - resource :projects do - desc 'Get a list of merge request diff versions' do - detail 'This feature was introduced in GitLab 8.12.' - success Entities::MergeRequestDiff - end + resource :projects do + desc 'Get a list of merge request diff versions' do + detail 'This feature was introduced in GitLab 8.12.' + success ::API::Entities::MergeRequestDiff + end - params do - requires :id, type: String, desc: 'The ID of a project' - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - end + params do + requires :id, type: String, desc: 'The ID of a project' + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + end - get ":id/merge_requests/:merge_request_id/versions" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ":id/merge_requests/:merge_request_id/versions" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff - end + present merge_request.merge_request_diffs, with: ::API::Entities::MergeRequestDiff + end - desc 'Get a single merge request diff version' do - detail 'This feature was introduced in GitLab 8.12.' - success Entities::MergeRequestDiffFull - end + desc 'Get a single merge request diff version' do + detail 'This feature was introduced in GitLab 8.12.' + success ::API::Entities::MergeRequestDiffFull + end - params do - requires :id, type: String, desc: 'The ID of a project' - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' - end + params do + requires :id, type: String, desc: 'The ID of a project' + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' + end - get ":id/merge_requests/:merge_request_id/versions/:version_id" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ":id/merge_requests/:merge_request_id/versions/:version_id" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull + present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull + end end end end diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb index cb679e6658a..861b991b8e1 100644 --- a/lib/api/v3/project_hooks.rb +++ b/lib/api/v3/project_hooks.rb @@ -1,102 +1,104 @@ module API - class ProjectHooks < Grape::API - include PaginationParams + module V3 + class ProjectHooks < Grape::API + include PaginationParams - before { authenticate! } - before { authorize_admin_project } + before { authenticate! } + before { authorize_admin_project } - helpers do - params :project_hook_properties do - requires :url, type: String, desc: "The URL to send the request to" - optional :push_events, type: Boolean, desc: "Trigger hook on push events" - optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" - optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" - optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" - optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" - optional :build_events, type: Boolean, desc: "Trigger hook on build events" - optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" - optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" - optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" - optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects do - desc 'Get project hooks' do - success Entities::ProjectHook - end - params do - use :pagination - end - get ":id/hooks" do - hooks = paginate user_project.hooks - - present hooks, with: Entities::ProjectHook - end - - desc 'Get a project hook' do - success Entities::ProjectHook - end - params do - requires :hook_id, type: Integer, desc: 'The ID of a project hook' - end - get ":id/hooks/:hook_id" do - hook = user_project.hooks.find(params[:hook_id]) - present hook, with: Entities::ProjectHook - end - - desc 'Add hook to project' do - success Entities::ProjectHook - end - params do - use :project_hook_properties - end - post ":id/hooks" do - hook = user_project.hooks.new(declared_params(include_missing: false)) - - if hook.save - present hook, with: Entities::ProjectHook - else - error!("Invalid url given", 422) if hook.errors[:url].present? - - not_found!("Project hook #{hook.errors.messages}") + helpers do + params :project_hook_properties do + requires :url, type: String, desc: "The URL to send the request to" + optional :push_events, type: Boolean, desc: "Trigger hook on push events" + optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" + optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" + optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" + optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" + optional :build_events, type: Boolean, desc: "Trigger hook on build events" + optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" + optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" + optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" + optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" end end - desc 'Update an existing project hook' do - success Entities::ProjectHook - end params do - requires :hook_id, type: Integer, desc: "The ID of the hook to update" - use :project_hook_properties + requires :id, type: String, desc: 'The ID of a project' end - put ":id/hooks/:hook_id" do - hook = user_project.hooks.find(params.delete(:hook_id)) - - if hook.update_attributes(declared_params(include_missing: false)) - present hook, with: Entities::ProjectHook - else - error!("Invalid url given", 422) if hook.errors[:url].present? - - not_found!("Project hook #{hook.errors.messages}") + resource :projects do + desc 'Get project hooks' do + success ::API::V3::Entities::ProjectHook end - end + params do + use :pagination + end + get ":id/hooks" do + hooks = paginate user_project.hooks - desc 'Deletes project hook' do - success Entities::ProjectHook - end - params do - requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' - end - delete ":id/hooks/:hook_id" do - begin - present user_project.hooks.destroy(params[:hook_id]), with: Entities::ProjectHook - rescue - # ProjectHook can raise Error if hook_id not found - not_found!("Error deleting hook #{params[:hook_id]}") + present hooks, with: ::API::V3::Entities::ProjectHook + end + + desc 'Get a project hook' do + success ::API::V3::Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: 'The ID of a project hook' + end + get ":id/hooks/:hook_id" do + hook = user_project.hooks.find(params[:hook_id]) + present hook, with: ::API::V3::Entities::ProjectHook + end + + desc 'Add hook to project' do + success ::API::V3::Entities::ProjectHook + end + params do + use :project_hook_properties + end + post ":id/hooks" do + hook = user_project.hooks.new(declared_params(include_missing: false)) + + if hook.save + present hook, with: ::API::V3::Entities::ProjectHook + else + error!("Invalid url given", 422) if hook.errors[:url].present? + + not_found!("Project hook #{hook.errors.messages}") + end + end + + desc 'Update an existing project hook' do + success ::API::V3::Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: "The ID of the hook to update" + use :project_hook_properties + end + put ":id/hooks/:hook_id" do + hook = user_project.hooks.find(params.delete(:hook_id)) + + if hook.update_attributes(declared_params(include_missing: false)) + present hook, with: ::API::V3::Entities::ProjectHook + else + error!("Invalid url given", 422) if hook.errors[:url].present? + + not_found!("Project hook #{hook.errors.messages}") + end + end + + desc 'Deletes project hook' do + success ::API::V3::Entities::ProjectHook + end + params do + requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' + end + delete ":id/hooks/:hook_id" do + begin + present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook + rescue + # ProjectHook can raise Error if hook_id not found + not_found!("Error deleting hook #{params[:hook_id]}") + end end end end diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb index de24e6418c7..d77185ffe5a 100644 --- a/lib/api/v3/services.rb +++ b/lib/api/v3/services.rb @@ -537,6 +537,23 @@ module API ] } + trigger_services = { + 'mattermost-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Mattermost token' + } + ], + 'slack-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Slack token' + } + ] + }.freeze + resource :projects do before { authenticate! } before { authorize_admin_project } @@ -561,6 +578,7 @@ module API end if service.update_attributes(attrs.merge(active: false)) + status(200) true else render_api_error!('400 Bad Request', 400) @@ -620,3 +638,4 @@ module API end end end +end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 858eb6a9635..2545da7b1db 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -77,8 +77,7 @@ describe API::Groups, api: true do build_artifacts_size: 345, }.stringify_keys exposed_attributes = attributes.dup - exposed_attributes['job_artifacts_size'] = exposed_attributes['build_artifacts_size'] - exposed_attributes.delete('build_artifacts_size') + exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size') project1.statistics.update!(attributes) @@ -88,7 +87,7 @@ describe API::Groups, api: true do expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response) - .to satisfy_one { |group| group['statistics'] == attributes } + .to satisfy_one { |group| group['statistics'] == exposed_attributes } end end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index f286568547d..b1f8c249092 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -33,7 +33,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(json_response.first['merge_requests_events']).to eq(true) expect(json_response.first['tag_push_events']).to eq(true) expect(json_response.first['note_events']).to eq(true) - expect(json_response.first['build_events']).to eq(true) + expect(json_response.first['job_events']).to eq(true) expect(json_response.first['pipeline_events']).to eq(true) expect(json_response.first['wiki_page_events']).to eq(true) expect(json_response.first['enable_ssl_verification']).to eq(true) @@ -59,7 +59,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['job_events']).to eq(hook.build_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) @@ -98,7 +98,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(json_response['merge_requests_events']).to eq(false) expect(json_response['tag_push_events']).to eq(false) expect(json_response['note_events']).to eq(false) - expect(json_response['build_events']).to eq(false) + expect(json_response['job_events']).to eq(false) expect(json_response['pipeline_events']).to eq(false) expect(json_response['wiki_page_events']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(true) @@ -144,7 +144,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['job_events']).to eq(hook.build_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 03cae074803..77f79cd5bc7 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -594,7 +594,7 @@ describe API::Projects, api: true do expect(json_response['issues_enabled']).to be_present expect(json_response['merge_requests_enabled']).to be_present expect(json_response['wiki_enabled']).to be_present - expect(json_response['builds_enabled']).to be_present + expect(json_response['jobs_enabled']).to be_present expect(json_response['snippets_enabled']).to be_present expect(json_response['container_registry_enabled']).to be_present expect(json_response['created_at']).to be_present @@ -605,7 +605,7 @@ describe API::Projects, api: true do expect(json_response['avatar_url']).to be_nil expect(json_response['star_count']).to be_present expect(json_response['forks_count']).to be_present - expect(json_response['public_builds']).to be_present + expect(json_response['public_jobs']).to be_present expect(json_response['shared_with_groups']).to be_an Array expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb index 31e3cfa1b2f..3c5ce407b32 100644 --- a/spec/requests/api/v3/deployments_spec.rb +++ b/spec/requests/api/v3/deployments_spec.rb @@ -12,6 +12,17 @@ describe API::Deployments, api: true do project.team << [user, :master] end + shared_examples 'a paginated resources' do + before do + # Fires the request + request + end + + it 'has pagination headers' do + expect(response).to include_pagination_headers + end + end + describe 'GET /projects/:id/deployments' do context 'as member of the project' do it_behaves_like 'a paginated resources' do diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb index a2228132ba9..ddef2d5eb04 100644 --- a/spec/requests/api/v3/notes_spec.rb +++ b/spec/requests/api/v3/notes_spec.rb @@ -328,11 +328,7 @@ describe API::V3::Notes, api: true do end it 'returns a 400 bad request error if body not given' do -<<<<<<< HEAD put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ -======= - put api("/projects/#{project.id}/issues/#{issue.id}/"\ ->>>>>>> e306055d88... Pick API files from 8.16.6 "notes/#{issue_note.id}", user) expect(response).to have_http_status(400) @@ -341,11 +337,7 @@ describe API::V3::Notes, api: true do context 'when noteable is a Snippet' do it 'returns modified note' do -<<<<<<< HEAD put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ -======= - put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ ->>>>>>> e306055d88... Pick API files from 8.16.6 "notes/#{snippet_note.id}", user), body: 'Hello!' expect(response).to have_http_status(200) @@ -353,11 +345,7 @@ describe API::V3::Notes, api: true do end it 'returns a 404 error when note id not found' do -<<<<<<< HEAD put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ -======= - put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ ->>>>>>> e306055d88... Pick API files from 8.16.6 "notes/12345", user), body: "Hello!" expect(response).to have_http_status(404) @@ -366,11 +354,7 @@ describe API::V3::Notes, api: true do context 'when noteable is a Merge Request' do it 'returns modified note' do -<<<<<<< HEAD put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ -======= - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ ->>>>>>> e306055d88... Pick API files from 8.16.6 "notes/#{merge_request_note.id}", user), body: 'Hello!' expect(response).to have_http_status(200) @@ -378,11 +362,7 @@ describe API::V3::Notes, api: true do end it 'returns a 404 error when note id not found' do -<<<<<<< HEAD put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ -======= - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ ->>>>>>> e306055d88... Pick API files from 8.16.6 "notes/12345", user), body: "Hello!" expect(response).to have_http_status(404) @@ -393,7 +373,6 @@ describe API::V3::Notes, api: true do describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do context 'when noteable is an Issue' do it 'deletes a note' do -<<<<<<< HEAD delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) @@ -401,24 +380,11 @@ describe API::V3::Notes, api: true do # Check if note is really deleted delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) -======= - delete api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) - - expect(response).to have_http_status(200) - # Check if note is really deleted - delete api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) ->>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do -<<<<<<< HEAD delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) -======= - delete api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) ->>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end @@ -426,7 +392,6 @@ describe API::V3::Notes, api: true do context 'when noteable is a Snippet' do it 'deletes a note' do -<<<<<<< HEAD delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) @@ -434,26 +399,12 @@ describe API::V3::Notes, api: true do # Check if note is really deleted delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) -======= - delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user) - - expect(response).to have_http_status(200) - # Check if note is really deleted - delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user) ->>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do -<<<<<<< HEAD delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/12345", user) -======= - delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/12345", user) ->>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end @@ -461,7 +412,6 @@ describe API::V3::Notes, api: true do context 'when noteable is a Merge Request' do it 'deletes a note' do -<<<<<<< HEAD delete v3_api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) @@ -469,26 +419,12 @@ describe API::V3::Notes, api: true do # Check if note is really deleted delete v3_api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) -======= - delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) - - expect(response).to have_http_status(200) - # Check if note is really deleted - delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) ->>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do -<<<<<<< HEAD delete v3_api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/12345", user) -======= - delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/12345", user) ->>>>>>> e306055d88... Pick API files from 8.16.6 expect(response).to have_http_status(404) end diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb index 36fbcf088e7..a981119dc5a 100644 --- a/spec/requests/api/v3/project_hooks_spec.rb +++ b/spec/requests/api/v3/project_hooks_spec.rb @@ -21,9 +21,9 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do describe "GET /projects/:id/hooks" do context "authorized user" do it "returns project hooks" do - get api("/projects/#{project.id}/hooks", user) - expect(response).to have_http_status(200) + get v3_api("/projects/#{project.id}/hooks", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.count).to eq(1) expect(json_response.first['url']).to eq("http://example.com") @@ -41,7 +41,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do context "unauthorized user" do it "does not access project hooks" do - get api("/projects/#{project.id}/hooks", user3) + get v3_api("/projects/#{project.id}/hooks", user3) + expect(response).to have_http_status(403) end end @@ -50,7 +51,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do describe "GET /projects/:id/hooks/:hook_id" do context "authorized user" do it "returns a project hook" do - get api("/projects/#{project.id}/hooks/#{hook.id}", user) + get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) expect(response).to have_http_status(200) expect(json_response['url']).to eq(hook.url) expect(json_response['issues_events']).to eq(hook.issues_events) @@ -65,20 +66,20 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do end it "returns a 404 error if hook id is not available" do - get api("/projects/#{project.id}/hooks/1234", user) + get v3_api("/projects/#{project.id}/hooks/1234", user) expect(response).to have_http_status(404) end end context "unauthorized user" do it "does not access an existing hook" do - get api("/projects/#{project.id}/hooks/#{hook.id}", user3) + get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user3) expect(response).to have_http_status(403) end end it "returns a 404 error if hook id is not available" do - get api("/projects/#{project.id}/hooks/1234", user) + get v3_api("/projects/#{project.id}/hooks/1234", user) expect(response).to have_http_status(404) end end @@ -86,7 +87,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do describe "POST /projects/:id/hooks" do it "adds hook to project" do expect do - post api("/projects/#{project.id}/hooks", user), + post v3_api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true, wiki_page_events: true end.to change {project.hooks.count}.by(1) @@ -108,7 +109,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do token = "secret token" expect do - post api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token + post v3_api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token end.to change {project.hooks.count}.by(1) expect(response).to have_http_status(201) @@ -122,19 +123,19 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do end it "returns a 400 error if url not given" do - post api("/projects/#{project.id}/hooks", user) + post v3_api("/projects/#{project.id}/hooks", user) expect(response).to have_http_status(400) end it "returns a 422 error if url not valid" do - post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" + post v3_api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" expect(response).to have_http_status(422) end end describe "PUT /projects/:id/hooks/:hook_id" do it "updates an existing project hook" do - put api("/projects/#{project.id}/hooks/#{hook.id}", user), + put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'http://example.org', push_events: false expect(response).to have_http_status(200) expect(json_response['url']).to eq('http://example.org') @@ -152,7 +153,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do it "adds the token without including it in the response" do token = "secret token" - put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token + put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token expect(response).to have_http_status(200) expect(json_response["url"]).to eq("http://example.org") @@ -163,17 +164,17 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do end it "returns 404 error if hook id not found" do - put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' + put v3_api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' expect(response).to have_http_status(404) end it "returns 400 error if url is not given" do - put api("/projects/#{project.id}/hooks/#{hook.id}", user) + put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) expect(response).to have_http_status(400) end it "returns a 422 error if url is not valid" do - put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' + put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' expect(response).to have_http_status(422) end end @@ -181,23 +182,23 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do describe "DELETE /projects/:id/hooks/:hook_id" do it "deletes hook from project" do expect do - delete api("/projects/#{project.id}/hooks/#{hook.id}", user) + delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) end.to change {project.hooks.count}.by(-1) expect(response).to have_http_status(200) end it "returns success when deleting hook" do - delete api("/projects/#{project.id}/hooks/#{hook.id}", user) + delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) expect(response).to have_http_status(200) end it "returns a 404 error when deleting non existent hook" do - delete api("/projects/#{project.id}/hooks/42", user) + delete v3_api("/projects/#{project.id}/hooks/42", user) expect(response).to have_http_status(404) end it "returns a 404 error if hook id not given" do - delete api("/projects/#{project.id}/hooks", user) + delete v3_api("/projects/#{project.id}/hooks", user) expect(response).to have_http_status(404) end @@ -207,7 +208,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do other_project = create(:project) other_project.team << [test_user, :master] - delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) + delete v3_api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) expect(response).to have_http_status(404) expect(WebHook.exists?(hook.id)).to be_truthy end From e25bb81be2227feff54bd9e9a490994f32004d56 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 6 Mar 2017 09:31:37 +0100 Subject: [PATCH 76/95] Add changelog entry [ci skip] --- changelogs/unreleased/zj-builds-to-jobs-api.yml | 4 ++++ lib/api/entities.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/zj-builds-to-jobs-api.yml diff --git a/changelogs/unreleased/zj-builds-to-jobs-api.yml b/changelogs/unreleased/zj-builds-to-jobs-api.yml new file mode 100644 index 00000000000..473dd9bc8ed --- /dev/null +++ b/changelogs/unreleased/zj-builds-to-jobs-api.yml @@ -0,0 +1,4 @@ +--- +title: Rename builds to job for the v4 API +merge_request: 9463 +author: diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3db67ff455b..e0de5aeddee 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -634,7 +634,7 @@ module API expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :created_at, :started_at, :finished_at expose :user, with: User - expose :artifacts_file, using: JobArtifactFile, if: -> (build, opts) { build.artifacts? } + expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? } expose :commit, with: RepoCommit expose :runner, with: Runner expose :pipeline, with: PipelineBasic From 194223476b8df57a56ba096c7a300d51d2e3e750 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 6 Mar 2017 10:24:03 +0100 Subject: [PATCH 77/95] Rename build to job in the docs --- doc/api/groups.md | 20 +++--- doc/api/{builds.md => jobs.md} | 112 ++++++++++++++++----------------- doc/api/pipelines.md | 4 +- doc/api/projects.md | 52 +++++++-------- doc/api/services.md | 28 ++++----- lib/api/services.rb | 10 +-- 6 files changed, 113 insertions(+), 113 deletions(-) rename doc/api/{builds.md => jobs.md} (82%) diff --git a/doc/api/groups.md b/doc/api/groups.md index 80c08096dea..dfc6b80bfd9 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -84,7 +84,7 @@ Example response: "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "snippets_enabled": true, "created_at": "2016-04-05T21:40:50.169Z", "last_activity_at": "2016-04-06T16:52:08.432Z", @@ -100,7 +100,7 @@ Example response: "star_count": 1, "forks_count": 0, "open_issues_count": 3, - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "request_access_enabled": false } @@ -158,7 +158,7 @@ Example response: "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "snippets_enabled": false, "container_registry_enabled": true, "created_at": "2016-06-17T07:47:25.578Z", @@ -175,7 +175,7 @@ Example response: "star_count": 0, "forks_count": 0, "open_issues_count": 3, - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "request_access_enabled": false }, @@ -196,7 +196,7 @@ Example response: "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "snippets_enabled": false, "container_registry_enabled": true, "created_at": "2016-06-17T07:47:24.661Z", @@ -213,7 +213,7 @@ Example response: "star_count": 0, "forks_count": 0, "open_issues_count": 8, - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "request_access_enabled": false } @@ -236,7 +236,7 @@ Example response: "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "snippets_enabled": false, "container_registry_enabled": true, "created_at": "2016-06-17T07:47:27.089Z", @@ -253,7 +253,7 @@ Example response: "star_count": 0, "forks_count": 0, "open_issues_count": 4, - "public_builds": true, + "public_jobs": true, "shared_with_groups": [ { "group_id": 4, @@ -359,7 +359,7 @@ Example response: "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "snippets_enabled": true, "created_at": "2016-04-05T21:40:50.169Z", "last_activity_at": "2016-04-06T16:52:08.432Z", @@ -375,7 +375,7 @@ Example response: "star_count": 1, "forks_count": 0, "open_issues_count": 3, - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "request_access_enabled": false } diff --git a/doc/api/builds.md b/doc/api/jobs.md similarity index 82% rename from doc/api/builds.md rename to doc/api/jobs.md index 84214e4708f..43451ccce4c 100644 --- a/doc/api/builds.md +++ b/doc/api/jobs.md @@ -1,20 +1,20 @@ -# Builds API +# Jobs API -## List project builds +## List project jobs -Get a list of builds in a project. +Get a list of jobs in a project. ``` -GET /projects/:id/builds +GET /projects/:id/jobs ``` | Attribute | Type | Required | Description | |-----------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | -| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided | +| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/builds?scope%5B0%5D=pending&scope%5B1%5D=running' +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope%5B0%5D=pending&scope%5B1%5D=running' ``` Example of response @@ -115,27 +115,27 @@ Example of response ] ``` -## List commit builds +## List commit jobs -Get a list of builds for specific commit in a project. +Get a list of jobs for specific commit in a project. -This endpoint will return all builds, from all pipelines for a given commit. +This endpoint will return all jobs, from all pipelines for a given commit. If the commit SHA is not found, it will respond with 404, otherwise it will -return an array of builds (an empty array if there are no builds for this +return an array of jobs (an empty array if there are no jobs for this particular commit). ``` -GET /projects/:id/repository/commits/:sha/builds +GET /projects/:id/repository/commits/:sha/jobs ``` | Attribute | Type | Required | Description | |-----------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | | `sha` | string | yes | The SHA id of a commit | -| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided | +| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds?scope%5B0%5D=pending&scope%5B1%5D=running' +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/jobs?scope%5B0%5D=pending&scope%5B1%5D=running' ``` Example of response @@ -219,21 +219,21 @@ Example of response ] ``` -## Get a single build +## Get a single job -Get a single build of a project +Get a single job of a project ``` -GET /projects/:id/builds/:build_id +GET /projects/:id/jobs/:job_id ``` | Attribute | Type | Required | Description | |------------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | -| `build_id` | integer | yes | The ID of a build | +| `job_id` | integer | yes | The ID of a job | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/8" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8" ``` Example of response @@ -285,23 +285,23 @@ Example of response } ``` -## Get build artifacts +## Get job artifacts > [Introduced][ce-2893] in GitLab 8.5 -Get build artifacts of a project +Get job artifacts of a project ``` -GET /projects/:id/builds/:build_id/artifacts +GET /projects/:id/jobs/:job_id/artifacts ``` | Attribute | Type | Required | Description | |------------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | -| `build_id` | integer | yes | The ID of a build | +| `job_id` | integer | yes | The ID of a job | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/8/artifacts" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts" ``` Response: @@ -318,10 +318,10 @@ Response: > [Introduced][ce-5347] in GitLab 8.10. Download the artifacts file from the given reference name and job provided the -build finished successfully. +job finished successfully. ``` -GET /projects/:id/builds/artifacts/:ref_name/download?job=name +GET /projects/:id/jobs/artifacts/:ref_name/download?job=name ``` Parameters @@ -335,7 +335,7 @@ Parameters Example request: ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/artifacts/master/download?job=test" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test" ``` Example response: @@ -349,19 +349,19 @@ Example response: ## Get a trace file -Get a trace of a specific build of a project +Get a trace of a specific job of a project ``` -GET /projects/:id/builds/:build_id/trace +GET /projects/:id/jobs/:job_id/trace ``` | Attribute | Type | Required | Description | |------------|---------|----------|---------------------| | id | integer | yes | The ID of a project | -| build_id | integer | yes | The ID of a build | +| job_id | integer | yes | The ID of a job | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/8/trace" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/trace" ``` Response: @@ -371,21 +371,21 @@ Response: | 200 | Serves the trace file | | 404 | Build not found or no trace file | -## Cancel a build +## Cancel a job -Cancel a single build of a project +Cancel a single job of a project ``` -POST /projects/:id/builds/:build_id/cancel +POST /projects/:id/jobs/:job_id/cancel ``` | Attribute | Type | Required | Description | |------------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | -| `build_id` | integer | yes | The ID of a build | +| `job_id` | integer | yes | The ID of a job | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/cancel" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/cancel" ``` Example of response @@ -417,21 +417,21 @@ Example of response } ``` -## Retry a build +## Retry a job -Retry a single build of a project +Retry a single job of a project ``` -POST /projects/:id/builds/:build_id/retry +POST /projects/:id/jobs/:job_id/retry ``` | Attribute | Type | Required | Description | |------------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | -| `build_id` | integer | yes | The ID of a build | +| `job_id` | integer | yes | The ID of a job | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/retry" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/retry" ``` Example of response @@ -463,12 +463,12 @@ Example of response } ``` -## Erase a build +## Erase a job -Erase a single build of a project (remove build artifacts and a build trace) +Erase a single job of a project (remove job artifacts and a job trace) ``` -POST /projects/:id/builds/:build_id/erase +POST /projects/:id/jobs/:job_id/erase ``` Parameters @@ -476,12 +476,12 @@ Parameters | Attribute | Type | Required | Description | |-------------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | -| `build_id` | integer | yes | The ID of a build | +| `job_id` | integer | yes | The ID of a job | Example of request ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/erase" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/erase" ``` Example of response @@ -518,7 +518,7 @@ Example of response Prevents artifacts from being deleted when expiration is set. ``` -POST /projects/:id/builds/:build_id/artifacts/keep +POST /projects/:id/jobs/:job_id/artifacts/keep ``` Parameters @@ -526,12 +526,12 @@ Parameters | Attribute | Type | Required | Description | |-------------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | -| `build_id` | integer | yes | The ID of a build | +| `job_id` | integer | yes | The ID of a job | Example request: ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/artifacts/keep" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/artifacts/keep" ``` Example response: @@ -563,21 +563,21 @@ Example response: } ``` -## Play a build +## Play a job -Triggers a manual action to start a build. +Triggers a manual action to start a job. ``` -POST /projects/:id/builds/:build_id/play +POST /projects/:id/jobs/:job_id/play ``` -| Attribute | Type | Required | Description | -|------------|---------|----------|---------------------| -| `id` | integer | yes | The ID of a project | -| `build_id` | integer | yes | The ID of a build | +| Attribute | Type | Required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | +| `job_id` | integer | yes | The ID of a job | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/play" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/play" ``` Example of response diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index d809ec032f8..574a8bacb25 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -127,7 +127,7 @@ Example of response } ``` -## Retry builds in a pipeline +## Retry jobs in a pipeline > [Introduced][ce-5837] in GitLab 8.11 @@ -173,7 +173,7 @@ Response: } ``` -## Cancel a pipelines builds +## Cancel a pipelines jobs > [Introduced][ce-5837] in GitLab 8.11 diff --git a/doc/api/projects.md b/doc/api/projects.md index 6062c5ccd71..28e4bfe39dc 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -66,7 +66,7 @@ Parameters: "issues_enabled": true, "open_issues_count": 1, "merge_requests_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "container_registry_enabled": false, @@ -86,7 +86,7 @@ Parameters: "forks_count": 0, "star_count": 0, "runners_token": "b8547b1dc37721d05889db52fa2f02", - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, @@ -116,7 +116,7 @@ Parameters: "issues_enabled": true, "open_issues_count": 1, "merge_requests_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "container_registry_enabled": false, @@ -146,7 +146,7 @@ Parameters: "forks_count": 0, "star_count": 0, "runners_token": "b8547b1dc37721d05889db52fa2f02", - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, @@ -196,7 +196,7 @@ Parameters: "issues_enabled": true, "open_issues_count": 1, "merge_requests_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "container_registry_enabled": false, @@ -226,7 +226,7 @@ Parameters: "forks_count": 0, "star_count": 0, "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", - "public_builds": true, + "public_jobs": true, "shared_with_groups": [ { "group_id": 4, @@ -439,15 +439,15 @@ Parameters: | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -| `builds_enabled` | boolean | no | Enable builds for this project | +| `jobs_enabled` | boolean | no | Enable jobs for this project | | `wiki_enabled` | boolean | no | Enable wiki for this project | | `snippets_enabled` | boolean | no | Enable snippets for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `visibility` | String | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | -| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | -| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | +| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members | +| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | @@ -472,15 +472,15 @@ Parameters: | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -| `builds_enabled` | boolean | no | Enable builds for this project | +| `jobs_enabled` | boolean | no | Enable jobs for this project | | `wiki_enabled` | boolean | no | Enable wiki for this project | | `snippets_enabled` | boolean | no | Enable snippets for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `visibility` | string | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | -| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | -| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | +| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members | +| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | @@ -504,15 +504,15 @@ Parameters: | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | | `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -| `builds_enabled` | boolean | no | Enable builds for this project | +| `jobs_enabled` | boolean | no | Enable jobs for this project | | `wiki_enabled` | boolean | no | Enable wiki for this project | | `snippets_enabled` | boolean | no | Enable snippets for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `visibility` | string | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | -| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | -| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | +| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members | +| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | @@ -572,7 +572,7 @@ Example response: "issues_enabled": true, "open_issues_count": 1, "merge_requests_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "container_registry_enabled": false, @@ -591,7 +591,7 @@ Example response: "shared_runners_enabled": true, "forks_count": 0, "star_count": 1, - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, @@ -637,7 +637,7 @@ Example response: "issues_enabled": true, "open_issues_count": 1, "merge_requests_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "container_registry_enabled": false, @@ -656,7 +656,7 @@ Example response: "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, @@ -708,7 +708,7 @@ Example response: "issues_enabled": true, "open_issues_count": 1, "merge_requests_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "container_registry_enabled": false, @@ -738,7 +738,7 @@ Example response: "forks_count": 0, "star_count": 0, "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, @@ -790,7 +790,7 @@ Example response: "issues_enabled": true, "open_issues_count": 1, "merge_requests_enabled": true, - "builds_enabled": true, + "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, "container_registry_enabled": false, @@ -820,7 +820,7 @@ Example response: "forks_count": 0, "star_count": 0, "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", - "public_builds": true, + "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, @@ -955,7 +955,7 @@ Parameters: "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "pipeline_events": true, "wiki_page_events": true, "enable_ssl_verification": true, @@ -982,7 +982,7 @@ Parameters: | `merge_requests_events` | boolean | no | Trigger hook on merge requests events | | `tag_push_events` | boolean | no | Trigger hook on tag push events | | `note_events` | boolean | no | Trigger hook on note events | -| `build_events` | boolean | no | Trigger hook on build events | +| `job_events` | boolean | no | Trigger hook on job events | | `pipeline_events` | boolean | no | Trigger hook on pipeline events | | `wiki_events` | boolean | no | Trigger hook on wiki events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | @@ -1008,7 +1008,7 @@ Parameters: | `merge_requests_events` | boolean | no | Trigger hook on merge requests events | | `tag_push_events` | boolean | no | Trigger hook on tag push events | | `note_events` | boolean | no | Trigger hook on note events | -| `build_events` | boolean | no | Trigger hook on build events | +| `job_events` | boolean | no | Trigger hook on job events | | `pipeline_events` | boolean | no | Trigger hook on pipeline events | | `wiki_events` | boolean | no | Trigger hook on wiki events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | diff --git a/doc/api/services.md b/doc/api/services.md index b030a425a7a..4bb45497d11 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -70,7 +70,7 @@ GET /projects/:id/services/assembla ## Atlassian Bamboo CI -A continuous integration and build server +A continuous integration and job server ### Create/Edit Atlassian Bamboo CI service @@ -85,7 +85,7 @@ PUT /projects/:id/services/bamboo Parameters: - `bamboo_url` (**required**) - Bamboo root URL like https://bamboo.example.com -- `build_key` (**required**) - Bamboo build plan key like KEY +- `job_key` (**required**) - Bamboo job plan key like KEY - `username` (**required**) - A user with API access, if applicable - `password` (**required**) @@ -114,13 +114,13 @@ Continuous integration and deployments Set Buildkite service for a project. ``` -PUT /projects/:id/services/buildkite +PUT /projects/:id/services/jobkite ``` Parameters: - `token` (**required**) - Buildkite project GitLab token -- `project_url` (**required**) - https://buildkite.com/example/project +- `project_url` (**required**) - https://jobkite.com/example/project - `enable_ssl_verification` (optional) - Enable SSL verification ### Delete Buildkite service @@ -128,7 +128,7 @@ Parameters: Delete Buildkite service for a project. ``` -DELETE /projects/:id/services/buildkite +DELETE /projects/:id/services/jobkite ``` ### Get Buildkite service settings @@ -136,19 +136,19 @@ DELETE /projects/:id/services/buildkite Get Buildkite service settings for a project. ``` -GET /projects/:id/services/buildkite +GET /projects/:id/services/jobkite ``` ## Build-Emails -Get emails for GitLab CI builds. +Get emails for GitLab CI jobs. ### Create/Edit Build-Emails service Set Build-Emails service for a project. ``` -PUT /projects/:id/services/builds-email +PUT /projects/:id/services/jobs-email ``` Parameters: @@ -157,23 +157,23 @@ Parameters: | --------- | ---- | -------- | ----------- | | `recipients` | string | yes | Comma-separated list of recipient email addresses | | `add_pusher` | boolean | no | Add pusher to recipients list | -| `notify_only_broken_builds` | boolean | no | Notify only broken builds | +| `notify_only_broken_jobs` | boolean | no | Notify only broken jobs | -### Delete Build-Emails service +### Delete Job-Emails service Delete Build-Emails service for a project. ``` -DELETE /projects/:id/services/builds-email +DELETE /projects/:id/services/jobs-email ``` -### Get Build-Emails service settings +### Get Job-Emails service settings Get Build-Emails service settings for a project. ``` -GET /projects/:id/services/builds-email +GET /projects/:id/services/jobs-email ``` ## Campfire @@ -580,7 +580,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `recipients` | string | yes | Comma-separated list of recipient email addresses | | `add_pusher` | boolean | no | Add pusher to recipients list | -| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines | +| `notify_only_broken_jobs` | boolean | no | Notify only broken pipelines | ### Delete Pipeline-Emails service diff --git a/lib/api/services.rb b/lib/api/services.rb index 79a5f27dc4d..1cf29d9a1a3 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -122,9 +122,9 @@ module API }, { required: false, - name: :notify_only_broken_builds, + name: :notify_only_broken_jobs, type: Boolean, - desc: 'Notify only broken builds' + desc: 'Notify only broken jobs' } ], 'campfire' => [ @@ -403,9 +403,9 @@ module API }, { required: false, - name: :notify_only_broken_builds, + name: :notify_only_broken_jobs, type: Boolean, - desc: 'Notify only broken builds' + desc: 'Notify only broken jobs' } ], 'pivotaltracker' => [ @@ -611,7 +611,7 @@ module API desc "Set #{service_slug} service for project" params do service_classes.each do |service| - event_names = service.try(:event_names) || [] + event_names = service.try(:event_names) || next event_names.each do |event_name| services[service.to_param.tr("_", "-")] << { required: false, From 75123fa9f706a6ab7cdd5dba4393ad9d8bac0947 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 6 Mar 2017 11:50:32 +0100 Subject: [PATCH 78/95] Incorporate review, drop old endpoint The endpoint dropped, get ':id/repository/commits/:sha/jobs', should be replace by a new endpoint. --- doc/api/services.md | 14 +++---- doc/api/v3_to_v4.md | 2 + lib/api/jobs.rb | 21 ----------- lib/api/v3/deployments.rb | 8 ++-- spec/requests/api/jobs_spec.rb | 69 ---------------------------------- 5 files changed, 13 insertions(+), 101 deletions(-) diff --git a/doc/api/services.md b/doc/api/services.md index 4bb45497d11..8e7afe41b0c 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -70,7 +70,7 @@ GET /projects/:id/services/assembla ## Atlassian Bamboo CI -A continuous integration and job server +A continuous integration and build server ### Create/Edit Atlassian Bamboo CI service @@ -85,7 +85,7 @@ PUT /projects/:id/services/bamboo Parameters: - `bamboo_url` (**required**) - Bamboo root URL like https://bamboo.example.com -- `job_key` (**required**) - Bamboo job plan key like KEY +- `build_key` (**required**) - Bamboo build plan key like KEY - `username` (**required**) - A user with API access, if applicable - `password` (**required**) @@ -114,13 +114,13 @@ Continuous integration and deployments Set Buildkite service for a project. ``` -PUT /projects/:id/services/jobkite +PUT /projects/:id/services/buildkite ``` Parameters: - `token` (**required**) - Buildkite project GitLab token -- `project_url` (**required**) - https://jobkite.com/example/project +- `project_url` (**required**) - https://buildkite.com/example/project - `enable_ssl_verification` (optional) - Enable SSL verification ### Delete Buildkite service @@ -128,7 +128,7 @@ Parameters: Delete Buildkite service for a project. ``` -DELETE /projects/:id/services/jobkite +DELETE /projects/:id/services/buildkite ``` ### Get Buildkite service settings @@ -136,12 +136,12 @@ DELETE /projects/:id/services/jobkite Get Buildkite service settings for a project. ``` -GET /projects/:id/services/jobkite +GET /projects/:id/services/buildkite ``` ## Build-Emails -Get emails for GitLab CI jobs. +Get emails for GitLab CI builds. ### Create/Edit Build-Emails service diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 67ee2b69c3f..1d24e625f22 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -59,6 +59,8 @@ changes are in V4: - Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) - `projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096) - Return basic info about pipeline in `GET /projects/:id/pipelines` [!8875](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8875) +- Renamed all `build` references to `job` [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463) +- Drop GET '/projects/:id/repository/commits/:sha/jobs' [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463) - Rename Build Triggers to be Pipeline Triggers API [!9713](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9713) - `POST /projects/:id/trigger/builds` to `POST /projects/:id/trigger/pipeline` - Require description when creating a new trigger `POST /projects/:id/triggers` diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index b48574e173f..33c05e8aa63 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -40,27 +40,6 @@ module API user_can_download_artifacts: can?(current_user, :read_build, user_project) end - desc 'Get jobs for a specific commit of a project' do - success Entities::Job - end - params do - requires :sha, type: String, desc: 'The SHA id of a commit' - use :optional_scope - use :pagination - end - get ':id/repository/commits/:sha/jobs' do - authorize_read_builds! - - return not_found! unless user_project.commit(params[:sha]) - - pipelines = user_project.pipelines.where(sha: params[:sha]) - builds = user_project.builds.where(pipeline: pipelines).order('id DESC') - builds = filter_builds(builds, params[:scope]) - - present paginate(builds), with: Entities::Job, - user_can_download_artifacts: can?(current_user, :read_build, user_project) - end - desc 'Get a specific job of a project' do success Entities::Job end diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb index c5feb49b22f..545485fac0a 100644 --- a/lib/api/v3/deployments.rb +++ b/lib/api/v3/deployments.rb @@ -11,7 +11,7 @@ module API resource :projects do desc 'Get all deployments of the project' do detail 'This feature was introduced in GitLab 8.11.' - success Entities::Deployment + success ::API::V3::Deployments end params do use :pagination @@ -19,12 +19,12 @@ module API get ':id/deployments' do authorize! :read_deployment, user_project - present paginate(user_project.deployments), with: Entities::Deployment + present paginate(user_project.deployments), with: ::API::V3::Deployments end desc 'Gets a specific deployment' do detail 'This feature was introduced in GitLab 8.11.' - success Entities::Deployment + success ::API::V3::Deployments end params do requires :deployment_id, type: Integer, desc: 'The deployment ID' @@ -34,7 +34,7 @@ module API deployment = user_project.deployments.find(params[:deployment_id]) - present deployment, with: Entities::Deployment + present deployment, with: ::API::V3::Deployments end end end diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index f0e08e73763..a4d27734cc2 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -75,75 +75,6 @@ describe API::Jobs, api: true do end end - describe 'GET /projects/:id/repository/commits/:sha/jobs' do - context 'when commit does not exist in repository' do - before do - get api("/projects/#{project.id}/repository/commits/1a271fd1/jobs", api_user) - end - - it 'responds with 404' do - expect(response).to have_http_status(404) - end - end - - context 'when commit exists in repository' do - context 'when user is authorized' do - context 'when pipeline has jobs' do - before do - create(:ci_pipeline, project: project, sha: project.commit.id) - create(:ci_build, pipeline: pipeline) - create(:ci_build) - - get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/jobs", api_user) - end - - it 'returns project jobs for specific commit' do - expect(response).to have_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.size).to eq 2 - end - - it 'returns pipeline data' do - json_build = json_response.first - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status - end - end - - context 'when pipeline has no jobs' do - before do - branch_head = project.commit('feature').id - get api("/projects/#{project.id}/repository/commits/#{branch_head}/jobs", api_user) - end - - it 'returns an empty array' do - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response).to be_empty - end - end - end - - context 'when user is not authorized' do - before do - create(:ci_pipeline, project: project, sha: project.commit.id) - create(:ci_build, pipeline: pipeline) - - get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/jobs", nil) - end - - it 'does not return project jobs' do - expect(response).to have_http_status(401) - expect(json_response.except('message')).to be_empty - end - end - end - end - describe 'GET /projects/:id/jobs/:job_id' do before do get api("/projects/#{project.id}/jobs/#{build.id}", api_user) From d369acb516eed108d6897f93be549f0a2f302c9c Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Tue, 7 Feb 2017 14:15:07 +0100 Subject: [PATCH 79/95] Improve issues filtering performance --- app/finders/issuable_finder.rb | 35 +++++++++++-------- app/finders/issues_finder.rb | 4 +++ app/finders/merge_requests_finder.rb | 6 ++++ .../25503_issues_finder_performance.yml | 4 +++ 4 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/25503_issues_finder_performance.yml diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f49301e2631..2fca012252e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -33,15 +33,17 @@ class IssuableFinder items = by_scope(items) items = by_state(items) items = by_group(items) - items = by_project(items) items = by_search(items) - items = by_milestone(items) items = by_assignee(items) items = by_author(items) - items = by_label(items) items = by_due_date(items) items = by_non_archived(items) items = by_iids(items) + items = by_milestone(items) + items = by_label(items) + + # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far + items = by_project(items) sort(items) end @@ -107,8 +109,7 @@ class IssuableFinder @project = project end - def projects - return @projects if defined?(@projects) + def projects(items = nil) return @projects = project if project? projects = @@ -117,7 +118,7 @@ class IssuableFinder elsif group GroupProjectsFinder.new(group).execute(current_user) else - ProjectsFinder.new.execute(current_user) + projects_finder.execute(current_user, item_project_ids(items)) end @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) @@ -257,9 +258,9 @@ class IssuableFinder def by_project(items) items = if project? - items.of_projects(projects).references_project - elsif projects - items.merge(projects.reorder(nil)).join_project + items.of_projects(projects(items)).references_project + elsif projects(items) + items.merge(projects(items).reorder(nil)).join_project else items.none end @@ -314,13 +315,14 @@ class IssuableFinder if filter_by_no_milestone? items = items.left_joins_milestones.where(milestone_id: [-1, nil]) elsif filter_by_upcoming_milestone? - upcoming_ids = Milestone.upcoming_ids_by_projects(projects) + upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items)) items = items.left_joins_milestones.where(milestone_id: upcoming_ids) else items = items.with_milestone(params[:milestone_title]) + items_projects = projects(items) - if projects - items = items.where(milestones: { project_id: projects }) + if items_projects + items = items.where(milestones: { project_id: items_projects }) end end end @@ -334,9 +336,10 @@ class IssuableFinder items = items.without_label else items = items.with_label(label_names, params[:sort]) + items_projects = projects(items) - if projects - label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id) + if items_projects + label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id) items = items.where(labels: { id: label_ids }) end end @@ -396,4 +399,8 @@ class IssuableFinder def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end + + def projects_finder + @projects_finder ||= ProjectsFinder.new + end end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index f542f72a386..08713272947 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -41,4 +41,8 @@ class IssuesFinder < IssuableFinder user_id: user.id, project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) end + + def item_project_ids(items) + items&.reorder(nil)&.select(:project_id) + end end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index b76ca389f38..1eec45d9cb5 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -20,4 +20,10 @@ class MergeRequestsFinder < IssuableFinder def klass MergeRequest end + + private + + def item_project_ids(items) + items&.reorder(nil)&.select(:target_project_id) + end end diff --git a/changelogs/unreleased/25503_issues_finder_performance.yml b/changelogs/unreleased/25503_issues_finder_performance.yml new file mode 100644 index 00000000000..87964269c6d --- /dev/null +++ b/changelogs/unreleased/25503_issues_finder_performance.yml @@ -0,0 +1,4 @@ +--- +title: Filter by projects in the end of search +merge_request: 9030 +author: From 848a8f579421a4abb3a736ac12ed61ff7454fb31 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 6 Mar 2017 15:10:28 +0100 Subject: [PATCH 80/95] Use gitlab-workhorse 1.4.0 Bug fixes, Gitaly gRPC support, experimental Redis integration. --- GITLAB_WORKHORSE_VERSION | 2 +- changelogs/unreleased/workhorse-1-4-0.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/workhorse-1-4-0.yml diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index f0bb29e7638..88c5fb891dc 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.3.0 +1.4.0 diff --git a/changelogs/unreleased/workhorse-1-4-0.yml b/changelogs/unreleased/workhorse-1-4-0.yml new file mode 100644 index 00000000000..b55fabddb0f --- /dev/null +++ b/changelogs/unreleased/workhorse-1-4-0.yml @@ -0,0 +1,4 @@ +--- +title: Use gitlab-workhorse 1.4.0 +merge_request: 9724 +author: From 9f612cc428c47a578bef8869e2e93966c021e655 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 3 Mar 2017 14:25:52 +0000 Subject: [PATCH 81/95] Fix issues mentioned but not closed for JIRA The `ReferenceExtractor` would return an array of `ExternalIssue` objects, and then perform `Array#-` to remove the issues closed. `ExternalIssue`s had `==` defined, but not `hash` or `eql?`, which are used by `Array#-`. --- app/models/external_issue.rb | 5 +++++ ...x-mentioned-issues-for-external-trackers.yml | 4 ++++ spec/factories/services.rb | 9 +++++++++ spec/models/external_issue_spec.rb | 8 ++++++++ spec/models/merge_request_spec.rb | 17 +++++++++++++++++ 5 files changed, 43 insertions(+) create mode 100644 changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index b973bbcd8da..e63f89a9f85 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -24,6 +24,11 @@ class ExternalIssue def ==(other) other.is_a?(self.class) && (to_s == other.to_s) end + alias_method :eql?, :== + + def hash + [self.class, to_s].hash + end def project @project diff --git a/changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml b/changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml new file mode 100644 index 00000000000..ee827b7c939 --- /dev/null +++ b/changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml @@ -0,0 +1,4 @@ +--- +title: Fix issues mentioned but not closed for external issue trackers +merge_request: +author: diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 51335bdcf1d..88f6c265505 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -12,4 +12,13 @@ FactoryGirl.define do token: 'a' * 40, }) end + + factory :jira_service do + project factory: :empty_project + active true + properties( + url: 'https://jira.example.com', + project_key: 'jira-key' + ) + end end diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb index 2debe1289a3..cd50bda8996 100644 --- a/spec/models/external_issue_spec.rb +++ b/spec/models/external_issue_spec.rb @@ -42,4 +42,12 @@ describe ExternalIssue, models: true do expect(issue.project_id).to eq(project.id) end end + + describe '#hash' do + it 'returns the hash of its [class, to_s] pair' do + issue_2 = described_class.new(issue.to_s, project) + + expect(issue.hash).to eq(issue_2.hash) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index e000d0d38b3..fcaf4c71182 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -346,6 +346,23 @@ describe MergeRequest, models: true do expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue]) end + + context 'when the project has an external issue tracker' do + before do + subject.project.team << [subject.author, :developer] + commit = double(:commit, safe_message: 'Fixes TEST-3') + + create(:jira_service, project: subject.project) + + allow(subject).to receive(:commits).and_return([commit]) + allow(subject).to receive(:description).and_return('Is related to TEST-2 and TEST-3') + allow(subject.project).to receive(:default_branch).and_return(subject.target_branch) + end + + it 'detects issues mentioned in description but not closed' do + expect(subject.issues_mentioned_but_not_closing(subject.author).map(&:to_s)).to match_array(['TEST-2']) + end + end end describe "#work_in_progress?" do From 5ce3845f17c2c6e168cf2bd1b3c5093f10db1953 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 6 Mar 2017 14:18:59 +0000 Subject: [PATCH 82/95] DRY up {jira,kubernets}_project factories --- spec/factories/projects.rb | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 04de3512125..70c65bc693a 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -189,29 +189,10 @@ FactoryGirl.define do factory :jira_project, parent: :project do has_external_issue_tracker true - - after :create do |project| - project.create_jira_service( - active: true, - properties: { - title: 'JIRA tracker', - url: 'http://jira.example.net', - project_key: 'JIRA' - } - ) - end + jira_service end factory :kubernetes_project, parent: :empty_project do - after :create do |project| - project.create_kubernetes_service( - active: true, - properties: { - namespace: project.path, - api_url: 'https://kubernetes.example.com', - token: 'a' * 40, - } - ) - end + kubernetes_service end end From e5cf3f51fb568361a247d715facb6cd9bb15bb16 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 6 Feb 2017 13:48:46 +0100 Subject: [PATCH 83/95] Allow limiting logging in users from too many different IPs. --- app/controllers/sessions_controller.rb | 10 ++- app/models/application_setting.rb | 3 + config/application.rb | 5 ++ config/initializers/doorkeeper.rb | 6 +- config/initializers/request_context.rb | 3 + ...nique_ips_limit_to_application_settings.rb | 17 ++++ db/schema.rb | 5 +- lib/gitlab/auth.rb | 22 +++-- lib/gitlab/auth/unique_ips_limiter.rb | 70 +++++++++++++++ lib/gitlab/request_context.rb | 25 ++++++ .../gitlab/auth/unique_ips_limiter_spec.rb | 88 +++++++++++++++++++ spec/lib/gitlab/request_context_spec.rb | 40 +++++++++ 12 files changed, 278 insertions(+), 16 deletions(-) create mode 100644 config/initializers/request_context.rb create mode 100644 db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb create mode 100644 lib/gitlab/auth/unique_ips_limiter.rb create mode 100644 lib/gitlab/request_context.rb create mode 100644 spec/lib/gitlab/auth/unique_ips_limiter_spec.rb create mode 100644 spec/lib/gitlab/request_context_spec.rb diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 7d81c96262f..3f5b92d9a99 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -67,10 +67,12 @@ class SessionsController < Devise::SessionsController end def find_user - if session[:otp_user_id] - User.find(session[:otp_user_id]) - elsif user_params[:login] - User.by_login(user_params[:login]) + Gitlab::Auth::UniqueIpsLimiter.limit_user! do + if session[:otp_user_id] + User.find(session[:otp_user_id]) + elsif user_params[:login] + User.by_login(user_params[:login]) + end end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 255e8c4ff78..2f64fb1a6a2 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -184,6 +184,9 @@ class ApplicationSetting < ActiveRecord::Base domain_whitelist: Settings.gitlab['domain_whitelist'], gravatar_enabled: Settings.gravatar['enabled'], help_page_text: nil, + unique_ips_limit_per_user: 10, + unique_ips_limit_time_window: 3600, + unique_ips_limit_enabled: false, housekeeping_bitmaps_enabled: true, housekeeping_enabled: true, housekeeping_full_repack_period: 50, diff --git a/config/application.rb b/config/application.rb index f1a986d1731..c4dea9e92b0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -7,6 +7,9 @@ Bundler.require(:default, Rails.env) module Gitlab class Application < Rails::Application require_dependency Rails.root.join('lib/gitlab/redis') + require_dependency Rails.root.join('lib/gitlab/request_context') + require_dependency Rails.root.join('lib/gitlab/auth') + require_dependency Rails.root.join('lib/gitlab/auth/unique_ips_limiter') # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers @@ -111,6 +114,8 @@ module Gitlab config.middleware.insert_before Warden::Manager, Rack::Attack + config.middleware.insert_before Warden::Manager, Gitlab::Auth::UniqueIpsLimiter + # Allow access to GitLab API from other domains config.middleware.insert_before Warden::Manager, Rack::Cors do allow do diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 88cd0f5f652..44b658e5872 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -12,8 +12,10 @@ Doorkeeper.configure do end resource_owner_from_credentials do |routes| - user = Gitlab::Auth.find_with_user_password(params[:username], params[:password]) - user unless user.try(:two_factor_enabled?) + Gitlab::Auth::UniqueIpsLimiter.limit_user! do + user = Gitlab::Auth.find_with_user_password(params[:username], params[:password]) + user unless user.try(:two_factor_enabled?) + end end # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. diff --git a/config/initializers/request_context.rb b/config/initializers/request_context.rb new file mode 100644 index 00000000000..0b485fc1adc --- /dev/null +++ b/config/initializers/request_context.rb @@ -0,0 +1,3 @@ +Rails.application.configure do |config| + config.middleware.insert_after RequestStore::Middleware, Gitlab::RequestContext +end diff --git a/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb new file mode 100644 index 00000000000..2aa305f6b58 --- /dev/null +++ b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb @@ -0,0 +1,17 @@ +class AddUniqueIpsLimitToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, :unique_ips_limit_per_user, :integer, default: 10) + add_column_with_default(:application_settings, :unique_ips_limit_time_window, :integer, default: 3600) + add_column_with_default(:application_settings, :unique_ips_limit_enabled, :boolean, default: false) + end + + def down + remove_column(:application_settings, :unique_ips_limit_per_user) + remove_column(:application_settings, :unique_ips_limit_time_window) + remove_column(:application_settings, :unique_ips_limit_enabled) + end +end diff --git a/db/schema.rb b/db/schema.rb index 9deed46530e..8aed4e13b28 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -112,6 +112,9 @@ ActiveRecord::Schema.define(version: 20170305203726) do t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false t.string "default_artifacts_expire_in", default: "0", null: false + t.integer "unique_ips_limit_per_user", default: 10, null: false + t.integer "unique_ips_limit_time_window", default: 3600, null: false + t.boolean "unique_ips_limit_enabled", default: false, null: false end create_table "audit_events", force: :cascade do |t| @@ -252,8 +255,8 @@ ActiveRecord::Schema.define(version: 20170305203726) do t.integer "lock_version" end - add_index "ci_commits", ["gl_project_id", "ref", "status"], name: "index_ci_commits_on_gl_project_id_and_ref_and_status", using: :btree add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree + add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 0a5abc92190..be055080853 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -22,23 +22,27 @@ module Gitlab user_with_password_for_git(login, password) || Gitlab::Auth::Result.new + Gitlab::Auth::UniqueIpsLimiter.limit_user! { result.actor } + rate_limit!(ip, success: result.success?, login: login) result end def find_with_user_password(login, password) - user = User.by_login(login) + Gitlab::Auth::UniqueIpsLimiter.limit_user! do + user = User.by_login(login) - # If no user is found, or it's an LDAP server, try LDAP. - # LDAP users are only authenticated via LDAP - if user.nil? || user.ldap_user? - # Second chance - try LDAP authentication - return nil unless Gitlab::LDAP::Config.enabled? + # If no user is found, or it's an LDAP server, try LDAP. + # LDAP users are only authenticated via LDAP + if user.nil? || user.ldap_user? + # Second chance - try LDAP authentication + return nil unless Gitlab::LDAP::Config.enabled? - Gitlab::LDAP::Authentication.login(login, password) - else - user if user.valid_password?(password) + Gitlab::LDAP::Authentication.login(login, password) + else + user if user.valid_password?(password) + end end end diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb new file mode 100644 index 00000000000..21307eb35e4 --- /dev/null +++ b/lib/gitlab/auth/unique_ips_limiter.rb @@ -0,0 +1,70 @@ +module Gitlab + module Auth + class TooManyIps < StandardError + attr_reader :user_id, :ip, :unique_ips_count + + def initialize(user_id, ip, unique_ips_count) + @user_id = user_id + @ip = ip + @unique_ips_count = unique_ips_count + end + + def message + "User #{user_id} from IP: #{ip} tried logging from too many ips: #{unique_ips_count}" + end + end + + class UniqueIpsLimiter + USER_UNIQUE_IPS_PREFIX = 'user_unique_ips' + + class << self + def limit_user_id!(user_id) + if config.unique_ips_limit_enabled + ip = RequestContext.client_ip + unique_ips = count_unique_ips(user_id, ip) + raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user + end + end + + def limit_user!(user = nil) + user = yield if user.nil? + limit_user_id!(user.id) unless user.nil? + user + end + + def config + Gitlab::CurrentSettings.current_application_settings + end + + def count_unique_ips(user_id, ip) + time = Time.now.to_i + key = "#{USER_UNIQUE_IPS_PREFIX}:#{user_id}" + + Gitlab::Redis.with do |redis| + unique_ips_count = nil + redis.multi do |r| + r.zadd(key, time, ip) + r.zremrangebyscore(key, 0, time - config.unique_ips_limit_time_window) + unique_ips_count = r.zcard(key) + end + unique_ips_count.value + end + end + end + + def initialize(app) + @app = app + end + + def call(env) + begin + @app.call(env) + rescue TooManyIps => ex + + Rails.logger.info ex.message + [429, {'Content-Type' => 'text/plain', 'Retry-After' => UniqueIpsLimiter.config.unique_ips_limit_time_window }, ["Retry later\n"]] + end + end + end + end +end diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb new file mode 100644 index 00000000000..5daf04dc92b --- /dev/null +++ b/lib/gitlab/request_context.rb @@ -0,0 +1,25 @@ +module Gitlab + class RequestStoreNotActive < StandardError + end + + class RequestContext + class << self + def client_ip + RequestStore[:client_ip] + end + end + + def initialize(app) + @app = app + end + + def call(env) + raise RequestStoreNotActive.new unless RequestStore.active? + req = Rack::Request.new(env) + + RequestStore[:client_ip] = req.ip + + @app.call(env) + end + end +end \ No newline at end of file diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb new file mode 100644 index 00000000000..8e9fea0724a --- /dev/null +++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::Auth::UniqueIpsLimiter, lib: true do + let(:user) { create(:user) } + + before(:each) do + Gitlab::Redis.with do |redis| + redis.del("user_unique_ips:#{user.id}") + end + end + + describe '#count_unique_ips' do + + context 'non unique IPs' do + it 'properly counts them' do + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.1')).to eq(1) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.1')).to eq(1) + end + end + + context 'unique IPs' do + it 'properly counts them' do + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.2')).to eq(1) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.3')).to eq(2) + end + end + + it 'resets count after specified time window' do + cur_time = Time.now.to_i + allow(Time).to receive(:now).and_return(cur_time) + + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.2')).to eq(1) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.3')).to eq(2) + + allow(Time).to receive(:now).and_return(cur_time + Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window) + + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.4')).to eq(1) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.5')).to eq(2) + end + end + + + describe '#limit_user!' do + context 'when unique ips limit is enabled' do + before do + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_enabled).and_return(true) + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(10) + end + + context 'when ip limit is set to 1' do + before do + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(1) + end + + it 'blocks user trying to login from second ip' do + RequestStore[:client_ip] = '192.168.1.1' + expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) + + RequestStore[:client_ip] = '192.168.1.2' + expect { Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps) + end + + it 'allows user trying to login from the same ip twice' do + RequestStore[:client_ip] = '192.168.1.1' + expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) + expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) + end + end + + context 'when ip limit is set to 2' do + before do + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(2) + end + + it 'blocks user trying to login from third ip' do + RequestStore[:client_ip] = '192.168.1.1' + expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) + + RequestStore[:client_ip] = '192.168.1.2' + expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) + + RequestStore[:client_ip] = '192.168.1.3' + expect { Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps) + end + end + end + end +end diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb new file mode 100644 index 00000000000..3565fab6ded --- /dev/null +++ b/spec/lib/gitlab/request_context_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::RequestContext, lib: true do + describe '#client_ip' do + subject { Gitlab::RequestContext.client_ip } + let(:app) { -> env {} } + let(:env) { Hash.new } + + context 'when RequestStore::Middleware is used' do + around(:each) do |example| + RequestStore::Middleware.new(-> env { example.run }).call({}) + end + + context 'request' do + let(:ip) { '192.168.1.11' } + + before do + allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) + Gitlab::RequestContext.new(app).call(env) + end + + it { is_expected.to eq(ip) } + end + + context 'before RequestContext mw run' do + it { is_expected.to be_nil } + end + end + + context 'RequestStore is not active' do + it { is_expected.to be_nil } + + context 'when RequestContext mw is run' do + subject { -> { Gitlab::RequestContext.new(app).call(env) } } + + it { is_expected.to raise_error(Gitlab::RequestStoreNotActive) } + end + end + end +end From 66dc71599cb698d380e14be7230ae3495c78d266 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Thu, 16 Feb 2017 11:21:30 +0100 Subject: [PATCH 84/95] Cleanup formatting --- lib/gitlab/auth/unique_ips_limiter.rb | 2 +- lib/gitlab/request_context.rb | 2 +- spec/lib/gitlab/auth/unique_ips_limiter_spec.rb | 2 -- spec/lib/gitlab/request_context_spec.rb | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb index 21307eb35e4..01850ae31e8 100644 --- a/lib/gitlab/auth/unique_ips_limiter.rb +++ b/lib/gitlab/auth/unique_ips_limiter.rb @@ -62,7 +62,7 @@ module Gitlab rescue TooManyIps => ex Rails.logger.info ex.message - [429, {'Content-Type' => 'text/plain', 'Retry-After' => UniqueIpsLimiter.config.unique_ips_limit_time_window }, ["Retry later\n"]] + [429, { 'Content-Type' => 'text/plain', 'Retry-After' => UniqueIpsLimiter.config.unique_ips_limit_time_window }, ["Retry later\n"]] end end end diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb index 5daf04dc92b..36a5d94d98a 100644 --- a/lib/gitlab/request_context.rb +++ b/lib/gitlab/request_context.rb @@ -22,4 +22,4 @@ module Gitlab @app.call(env) end end -end \ No newline at end of file +end diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb index 8e9fea0724a..ccaddddf98f 100644 --- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb +++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb @@ -10,7 +10,6 @@ describe Gitlab::Auth::UniqueIpsLimiter, lib: true do end describe '#count_unique_ips' do - context 'non unique IPs' do it 'properly counts them' do expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.1')).to eq(1) @@ -39,7 +38,6 @@ describe Gitlab::Auth::UniqueIpsLimiter, lib: true do end end - describe '#limit_user!' do context 'when unique ips limit is enabled' do before do diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb index 3565fab6ded..69c5549c39c 100644 --- a/spec/lib/gitlab/request_context_spec.rb +++ b/spec/lib/gitlab/request_context_spec.rb @@ -3,12 +3,12 @@ require 'spec_helper' describe Gitlab::RequestContext, lib: true do describe '#client_ip' do subject { Gitlab::RequestContext.client_ip } - let(:app) { -> env {} } + let(:app) { -> (env) {} } let(:env) { Hash.new } context 'when RequestStore::Middleware is used' do around(:each) do |example| - RequestStore::Middleware.new(-> env { example.run }).call({}) + RequestStore::Middleware.new(-> (env) { example.run }).call({}) end context 'request' do From 8993801f0cefdc64b46b8fe30622cc78eaa03173 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 17 Feb 2017 12:52:27 +0100 Subject: [PATCH 85/95] Test various login scenarios if the limit gets enforced --- lib/api/api.rb | 4 ++ lib/api/helpers.rb | 15 ++--- lib/gitlab/auth.rb | 2 +- lib/gitlab/auth/unique_ips_limiter.rb | 2 +- spec/controllers/sessions_controller_spec.rb | 30 +++++++++- .../gitlab/auth/unique_ips_limiter_spec.rb | 22 +++---- spec/lib/gitlab/auth_spec.rb | 24 ++++++++ spec/requests/api/doorkeeper_access_spec.rb | 60 +++++++++++++++---- .../unique_ip_check_shared_examples.rb | 27 +++++++++ 9 files changed, 150 insertions(+), 36 deletions(-) create mode 100644 spec/support/unique_ip_check_shared_examples.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index 89449ce8813..6f37fa9d8e9 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -60,6 +60,10 @@ module API error! e.message, e.status, e.headers end + rescue_from Gitlab::Auth::TooManyIps do |e| + rack_response({'message'=>'403 Forbidden'}.to_json, 403) + end + rescue_from :all do |exception| handle_api_exception(exception) end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a43252a4661..f325f0a3050 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -336,16 +336,17 @@ module API def initial_current_user return @initial_current_user if defined?(@initial_current_user) + Gitlab::Auth::UniqueIpsLimiter.limit_user! do + @initial_current_user ||= find_user_by_private_token(scopes: @scopes) + @initial_current_user ||= doorkeeper_guard(scopes: @scopes) + @initial_current_user ||= find_user_from_warden - @initial_current_user ||= find_user_by_private_token(scopes: @scopes) - @initial_current_user ||= doorkeeper_guard(scopes: @scopes) - @initial_current_user ||= find_user_from_warden + unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? + @initial_current_user = nil + end - unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? - @initial_current_user = nil + @initial_current_user end - - @initial_current_user end def sudo! diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index be055080853..8e2aee2d7a0 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -22,7 +22,7 @@ module Gitlab user_with_password_for_git(login, password) || Gitlab::Auth::Result.new - Gitlab::Auth::UniqueIpsLimiter.limit_user! { result.actor } + Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor) rate_limit!(ip, success: result.success?, login: login) diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb index 01850ae31e8..7f849ef4c38 100644 --- a/lib/gitlab/auth/unique_ips_limiter.rb +++ b/lib/gitlab/auth/unique_ips_limiter.rb @@ -62,7 +62,7 @@ module Gitlab rescue TooManyIps => ex Rails.logger.info ex.message - [429, { 'Content-Type' => 'text/plain', 'Retry-After' => UniqueIpsLimiter.config.unique_ips_limit_time_window }, ["Retry later\n"]] + [403, { 'Content-Type' => 'text/plain', 'Retry-After' => UniqueIpsLimiter.config.unique_ips_limit_time_window }, ["Too many logins from different IPs\n"]] end end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index b56c7880b64..f1157d159ab 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -25,9 +25,35 @@ describe SessionsController do expect(subject.current_user). to eq user end - it "creates an audit log record" do + it 'creates an audit log record' do expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1) - expect(SecurityEvent.last.details[:with]).to eq("standard") + expect(SecurityEvent.last.details[:with]).to eq('standard') + end + + context 'unique ip limit is enabled and set to 1', :redis do + before do + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_enabled).and_return(true) + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(10) + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(1) + end + + it 'allows user authenticating from the same ip' do + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip') + post(:create, user: { login: user.username, password: user.password }) + expect(subject.current_user).to eq user + + post(:create, user: { login: user.username, password: user.password }) + expect(subject.current_user).to eq user + end + + it 'blocks user authenticating from two distinct ips' do + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip') + post(:create, user: { login: user.username, password: user.password }) + expect(subject.current_user).to eq user + + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip2') + expect { post(:create, user: { login: user.username, password: user.password }) }.to raise_error(Gitlab::Auth::TooManyIps) + end end end end diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb index ccaddddf98f..f2472b4310f 100644 --- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb +++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb @@ -1,14 +1,8 @@ require 'spec_helper' -describe Gitlab::Auth::UniqueIpsLimiter, lib: true do +describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do let(:user) { create(:user) } - before(:each) do - Gitlab::Redis.with do |redis| - redis.del("user_unique_ips:#{user.id}") - end - end - describe '#count_unique_ips' do context 'non unique IPs' do it 'properly counts them' do @@ -25,7 +19,7 @@ describe Gitlab::Auth::UniqueIpsLimiter, lib: true do end it 'resets count after specified time window' do - cur_time = Time.now.to_i + cur_time = Time.now allow(Time).to receive(:now).and_return(cur_time) expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.2')).to eq(1) @@ -51,15 +45,15 @@ describe Gitlab::Auth::UniqueIpsLimiter, lib: true do end it 'blocks user trying to login from second ip' do - RequestStore[:client_ip] = '192.168.1.1' + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.1') expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) - RequestStore[:client_ip] = '192.168.1.2' + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.2') expect { Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps) end it 'allows user trying to login from the same ip twice' do - RequestStore[:client_ip] = '192.168.1.1' + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.1') expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) end @@ -71,13 +65,13 @@ describe Gitlab::Auth::UniqueIpsLimiter, lib: true do end it 'blocks user trying to login from third ip' do - RequestStore[:client_ip] = '192.168.1.1' + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.1') expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) - RequestStore[:client_ip] = '192.168.1.2' + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.2') expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) - RequestStore[:client_ip] = '192.168.1.3' + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.3') expect { Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps) end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index b234de4c772..ee70ef34f4f 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -58,6 +58,30 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end + + context 'unique ip limit is enabled and set to 1', :redis do + before do + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_enabled).and_return(true) + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(10) + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(1) + end + + it 'allows user authenticating from the same ip' do + user = create(:user, password: 'password') + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip') + expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + end + + it 'blocks user authenticating from two distinct ips' do + user = create(:user, password: 'password') + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip') + expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip2') + expect { gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip2') }.to raise_error(Gitlab::Auth::TooManyIps) + end + end + context 'while using LFS authenticate' do it 'recognizes user lfs tokens' do user = create(:user) diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index bd9ecaf2685..1cd0701d955 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -4,27 +4,65 @@ describe API::API, api: true do include ApiHelpers let!(:user) { create(:user) } - let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } - let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } + let!(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: 'api' } - describe "when unauthenticated" do - it "returns authentication success" do - get api("/user"), access_token: token.token + describe 'when unauthenticated' do + it 'returns authentication success' do + get api('/user'), access_token: token.token expect(response).to have_http_status(200) end + + include_context 'limit login to only one ip' do + it 'allows login twice from the same ip' do + get api('/user'), access_token: token.token + expect(response).to have_http_status(200) + + get api('/user'), access_token: token.token + expect(response).to have_http_status(200) + end + + it 'blocks login from two different ips' do + get api('/user'), access_token: token.token + expect(response).to have_http_status(200) + + change_ip('ip2') + get api('/user'), access_token: token.token + expect(response).to have_http_status(403) + end + end end - describe "when token invalid" do - it "returns authentication error" do - get api("/user"), access_token: "123a" + describe 'when token invalid' do + it 'returns authentication error' do + get api('/user'), access_token: '123a' expect(response).to have_http_status(401) end end - describe "authorization by private token" do - it "returns authentication success" do - get api("/user", user) + describe 'authorization by private token' do + it 'returns authentication success' do + get api('/user', user) expect(response).to have_http_status(200) end + + include_context 'limit login to only one ip' do + it 'allows login twice from the same ip' do + get api('/user', user) + expect(response).to have_http_status(200) + + get api('/user', user) + expect(response).to have_http_status(200) + end + + it 'blocks login from two different ips' do + get api('/user', user) + expect(response).to have_http_status(200) + + change_ip('ip2') + get api('/user', user) + expect(response).to have_http_status(403) + end + end end end diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb new file mode 100644 index 00000000000..ab693b91d4a --- /dev/null +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -0,0 +1,27 @@ +shared_context 'limit login to only one ip', :redis do + before do + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_enabled).and_return(true) + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(1000) + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(1) + end + + def change_ip(ip) + allow(Gitlab::RequestContext).to receive(:client_ip).and_return(ip) + end +end + +shared_examples 'user login operation with unique ip limit' do + include_context 'limit login to only one ip' do + it 'allows user authenticating from the same ip' do + expect { operation }.not_to raise_error + expect { operation }.not_to raise_error + end + + it 'blocks user authenticating from two distinct ips' do + expect { operation }.not_to raise_error + + change_ip('ip2') + expect { operation }.to raise_error(Gitlab::Auth::TooManyIps) + end + end +end From b1da4f7de3019059051e35c806bb03bdacb41fcf Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 17 Feb 2017 13:24:32 +0100 Subject: [PATCH 86/95] Cleanup RSpec tests --- spec/controllers/sessions_controller_spec.rb | 23 +------- spec/lib/gitlab/auth_spec.rb | 28 +++------ spec/requests/api/doorkeeper_access_spec.rb | 59 +++++++++---------- .../unique_ip_check_shared_examples.rb | 10 +++- 4 files changed, 47 insertions(+), 73 deletions(-) diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index f1157d159ab..56ed11737ad 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -30,29 +30,10 @@ describe SessionsController do expect(SecurityEvent.last.details[:with]).to eq('standard') end - context 'unique ip limit is enabled and set to 1', :redis do - before do - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_enabled).and_return(true) - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(10) - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(1) - end - - it 'allows user authenticating from the same ip' do - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip') + include_examples 'user login operation with unique ip limit' do + def operation post(:create, user: { login: user.username, password: user.password }) expect(subject.current_user).to eq user - - post(:create, user: { login: user.username, password: user.password }) - expect(subject.current_user).to eq user - end - - it 'blocks user authenticating from two distinct ips' do - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip') - post(:create, user: { login: user.username, password: user.password }) - expect(subject.current_user).to eq user - - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip2') - expect { post(:create, user: { login: user.username, password: user.password }) }.to raise_error(Gitlab::Auth::TooManyIps) end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index ee70ef34f4f..860189dcf3c 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -58,27 +58,11 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end + include_examples 'user login operation with unique ip limit' do + let(:user) { create(:user, password: 'password') } - context 'unique ip limit is enabled and set to 1', :redis do - before do - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_enabled).and_return(true) - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(10) - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(1) - end - - it 'allows user authenticating from the same ip' do - user = create(:user, password: 'password') - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip') + def operation expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) - expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) - end - - it 'blocks user authenticating from two distinct ips' do - user = create(:user, password: 'password') - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip') - expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('ip2') - expect { gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip2') }.to raise_error(Gitlab::Auth::TooManyIps) end end @@ -220,6 +204,12 @@ describe Gitlab::Auth, lib: true do expect( gl_auth.find_with_user_password(username, password) ).not_to eql user end + include_examples 'user login operation with unique ip limit' do + def operation + expect(gl_auth.find_with_user_password(username, password)).to eql user + end + end + context "with ldap enabled" do before do allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index 1cd0701d955..b2864f448a8 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -1,6 +1,29 @@ require 'spec_helper' -describe API::API, api: true do +shared_examples 'user login request with unique ip limit' do + include_context 'limit login to only one ip' do + it 'allows user authenticating from the same ip' do + change_ip('ip') + request + expect(response).to have_http_status(200) + + request + expect(response).to have_http_status(200) + end + + it 'blocks user authenticating from two distinct ips' do + change_ip('ip') + request + expect(response).to have_http_status(200) + + change_ip('ip2') + request + expect(response).to have_http_status(403) + end + end +end + +describe API::API, api: true do include ApiHelpers let!(:user) { create(:user) } @@ -13,22 +36,9 @@ describe API::API, api: true do expect(response).to have_http_status(200) end - include_context 'limit login to only one ip' do - it 'allows login twice from the same ip' do + include_examples 'user login request with unique ip limit' do + def request get api('/user'), access_token: token.token - expect(response).to have_http_status(200) - - get api('/user'), access_token: token.token - expect(response).to have_http_status(200) - end - - it 'blocks login from two different ips' do - get api('/user'), access_token: token.token - expect(response).to have_http_status(200) - - change_ip('ip2') - get api('/user'), access_token: token.token - expect(response).to have_http_status(403) end end end @@ -46,22 +56,9 @@ describe API::API, api: true do expect(response).to have_http_status(200) end - include_context 'limit login to only one ip' do - it 'allows login twice from the same ip' do + include_examples 'user login request with unique ip limit' do + def request get api('/user', user) - expect(response).to have_http_status(200) - - get api('/user', user) - expect(response).to have_http_status(200) - end - - it 'blocks login from two different ips' do - get api('/user', user) - expect(response).to have_http_status(200) - - change_ip('ip2') - get api('/user', user) - expect(response).to have_http_status(403) end end end diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index ab693b91d4a..c868a1c7a7c 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -1,7 +1,11 @@ -shared_context 'limit login to only one ip', :redis do +shared_context 'limit login to only one ip' do + before(:each) do + Gitlab::Redis.with(&:flushall) + end + before do allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_enabled).and_return(true) - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(1000) + allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(10000) allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(1) end @@ -13,11 +17,13 @@ end shared_examples 'user login operation with unique ip limit' do include_context 'limit login to only one ip' do it 'allows user authenticating from the same ip' do + change_ip('ip') expect { operation }.not_to raise_error expect { operation }.not_to raise_error end it 'blocks user authenticating from two distinct ips' do + change_ip('ip') expect { operation }.not_to raise_error change_ip('ip2') From 5173c0939386b8fbf4632d755849bcafc863872e Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 17 Feb 2017 13:34:27 +0100 Subject: [PATCH 87/95] Add changelog --- .../27520-option-to-prevent-signing-in-from-multiple-ips.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml diff --git a/changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml b/changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml new file mode 100644 index 00000000000..3050b072863 --- /dev/null +++ b/changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml @@ -0,0 +1,4 @@ +--- +title: Option to prevent signing in from multiple ips +merge_request: 8998 +author: From 80fbced2e0b8d291173e1002f150bc5551e87359 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 17 Feb 2017 14:04:10 +0100 Subject: [PATCH 88/95] Add admin settings entries --- .../admin/application_settings_controller.rb | 3 +++ app/models/application_setting.rb | 10 +++++++++ .../application_settings/_form.html.haml | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index d807e6263ee..8d831ffdd70 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -138,6 +138,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :two_factor_grace_period, :user_default_external, :user_oauth_applications, + :unique_ips_limit_per_user, + :unique_ips_limit_time_window, + :unique_ips_limit_enabled, :version_check_enabled, :terminal_max_session_time, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2f64fb1a6a2..be632930895 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -64,6 +64,16 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :akismet_enabled + validates :unique_ips_limit_per_user, + numericality: { greater_than_or_equal_to: 1 }, + presence: true, + if: :unique_ips_limit_enabled + + validates :unique_ips_limit_time_window, + numericality: { greater_than_or_equal_to: 0 }, + presence: true, + if: :unique_ips_limit_enabled + validates :koding_url, presence: true, if: :koding_enabled diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 057b584e1bc..7f25f76d0cb 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -360,6 +360,28 @@ Generate API key at %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :unique_ips_limit_enabled do + = f.check_box :unique_ips_limit_enabled + Limit sign in from multiple ips + %span.help-block#recaptcha_help_block Helps prevent malicious users hide their activity + + .form-group + = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :unique_ips_limit_per_user, class: 'form-control' + .help-block + Maximum number of unique IPs per user + + .form-group + = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :unique_ips_limit_time_window, class: 'form-control' + .help-block + How long an IP will be counted towards the limit + %fieldset %legend Abuse reports .form-group From 9cc0ff8f468c54e23172492d97f6d9b428d3ad2e Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 17 Feb 2017 14:44:57 +0100 Subject: [PATCH 89/95] Cleanup common code in Unique Ips tests --- lib/api/api.rb | 2 +- lib/gitlab/auth/unique_ips_limiter.rb | 2 +- .../gitlab/auth/unique_ips_limiter_spec.rb | 66 +++++++------------ spec/requests/api/doorkeeper_access_spec.rb | 23 ------- .../unique_ip_check_shared_examples.rb | 41 ++++++++++-- 5 files changed, 60 insertions(+), 74 deletions(-) diff --git a/lib/api/api.rb b/lib/api/api.rb index 6f37fa9d8e9..efbb56ecd2c 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -61,7 +61,7 @@ module API end rescue_from Gitlab::Auth::TooManyIps do |e| - rack_response({'message'=>'403 Forbidden'}.to_json, 403) + rack_response({ 'message' => '403 Forbidden' }.to_json, 403) end rescue_from :all do |exception| diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb index 7f849ef4c38..4b2b758be8a 100644 --- a/lib/gitlab/auth/unique_ips_limiter.rb +++ b/lib/gitlab/auth/unique_ips_limiter.rb @@ -27,7 +27,7 @@ module Gitlab end def limit_user!(user = nil) - user = yield if user.nil? + user = yield if user.nil? && block_given? limit_user_id!(user.id) unless user.nil? user end diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb index f2472b4310f..2b21a76c59d 100644 --- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb +++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb @@ -1,20 +1,21 @@ require 'spec_helper' describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do + include_context 'enable unique ips sign in limit' let(:user) { create(:user) } describe '#count_unique_ips' do context 'non unique IPs' do it 'properly counts them' do - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.1')).to eq(1) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.1')).to eq(1) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip1')).to eq(1) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip1')).to eq(1) end end context 'unique IPs' do it 'properly counts them' do - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.2')).to eq(1) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.3')).to eq(2) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip2')).to eq(1) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip3')).to eq(2) end end @@ -22,58 +23,35 @@ describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do cur_time = Time.now allow(Time).to receive(:now).and_return(cur_time) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.2')).to eq(1) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.3')).to eq(2) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip2')).to eq(1) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip3')).to eq(2) allow(Time).to receive(:now).and_return(cur_time + Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.4')).to eq(1) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, '192.168.1.5')).to eq(2) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip4')).to eq(1) + expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip5')).to eq(2) end end describe '#limit_user!' do - context 'when unique ips limit is enabled' do - before do - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_enabled).and_return(true) - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(10) + include_examples 'user login operation with unique ip limit' do + def operation + Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } end + end - context 'when ip limit is set to 1' do - before do - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(1) - end + context 'allow 2 unique ips' do + before { current_application_settings.update!(unique_ips_limit_per_user: 2) } - it 'blocks user trying to login from second ip' do - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.1') - expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) + it 'blocks user trying to login from third ip' do + change_ip('ip1') + expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.2') - expect { Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps) - end + change_ip('ip2') + expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) - it 'allows user trying to login from the same ip twice' do - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.1') - expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) - expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) - end - end - - context 'when ip limit is set to 2' do - before do - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(2) - end - - it 'blocks user trying to login from third ip' do - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.1') - expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) - - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.2') - expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) - - allow(Gitlab::RequestContext).to receive(:client_ip).and_return('192.168.1.3') - expect { Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps) - end + change_ip('ip3') + expect { Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps) end end end diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index b2864f448a8..634b2261c5e 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -1,28 +1,5 @@ require 'spec_helper' -shared_examples 'user login request with unique ip limit' do - include_context 'limit login to only one ip' do - it 'allows user authenticating from the same ip' do - change_ip('ip') - request - expect(response).to have_http_status(200) - - request - expect(response).to have_http_status(200) - end - - it 'blocks user authenticating from two distinct ips' do - change_ip('ip') - request - expect(response).to have_http_status(200) - - change_ip('ip2') - request - expect(response).to have_http_status(403) - end - end -end - describe API::API, api: true do include ApiHelpers diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index c868a1c7a7c..024fb132778 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -1,12 +1,16 @@ -shared_context 'limit login to only one ip' do +shared_context 'enable unique ips sign in limit' do + include StubENV before(:each) do Gitlab::Redis.with(&:flushall) end before do - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_enabled).and_return(true) - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_time_window).and_return(10000) - allow(Gitlab::Auth::UniqueIpsLimiter).to receive_message_chain(:config, :unique_ips_limit_per_user).and_return(1) + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + + current_application_settings.update!( + unique_ips_limit_enabled: true, + unique_ips_limit_time_window: 10000 + ) end def change_ip(ip) @@ -15,7 +19,9 @@ shared_context 'limit login to only one ip' do end shared_examples 'user login operation with unique ip limit' do - include_context 'limit login to only one ip' do + include_context 'enable unique ips sign in limit' do + before { current_application_settings.update!(unique_ips_limit_per_user: 1) } + it 'allows user authenticating from the same ip' do change_ip('ip') expect { operation }.not_to raise_error @@ -31,3 +37,28 @@ shared_examples 'user login operation with unique ip limit' do end end end + +shared_examples 'user login request with unique ip limit' do + include_context 'enable unique ips sign in limit' do + before { current_application_settings.update!(unique_ips_limit_per_user: 1) } + + it 'allows user authenticating from the same ip' do + change_ip('ip') + request + expect(response).to have_http_status(200) + + request + expect(response).to have_http_status(200) + end + + it 'blocks user authenticating from two distinct ips' do + change_ip('ip') + request + expect(response).to have_http_status(200) + + change_ip('ip2') + request + expect(response).to have_http_status(403) + end + end +end From 0ef8a643489ad1a3da4431155326f0a6e206d870 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 20 Feb 2017 15:09:05 +0100 Subject: [PATCH 90/95] Remove unecessary calls to limit_user!, UniqueIps Middleware, and address MR review - cleanup formating in haml - clarify time window is in seconds - cleanup straneous chunks in db/schema - rename count_uniqe_ips to update_and_return_ips_count - other --- app/controllers/application_controller.rb | 4 +++ app/controllers/sessions_controller.rb | 10 +++--- .../application_settings/_form.html.haml | 5 +-- config/application.rb | 4 --- config/initializers/doorkeeper.rb | 6 ++-- ...nique_ips_limit_to_application_settings.rb | 12 +++---- db/schema.rb | 2 +- lib/gitlab/auth.rb | 3 +- lib/gitlab/auth/too_many_ips.rb | 17 ++++++++++ lib/gitlab/auth/unique_ips_limiter.rb | 34 ++----------------- lib/gitlab/request_context.rb | 1 - spec/controllers/sessions_controller_spec.rb | 1 + .../gitlab/auth/unique_ips_limiter_spec.rb | 33 +++++++++--------- spec/lib/gitlab/auth_spec.rb | 2 +- spec/lib/gitlab/request_context_spec.rb | 10 ------ 15 files changed, 59 insertions(+), 85 deletions(-) create mode 100644 lib/gitlab/auth/too_many_ips.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index cc7b7f247e8..0d3ee4f4c1f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base render_403 end + rescue_from Gitlab::Auth::TooManyIps do |e| + head :forbidden, retry_after: UniqueIpsLimiter.config.unique_ips_limit_time_window + end + def redirect_back_or_default(default: root_path, options: {}) redirect_to request.referer.present? ? :back : default, options end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 3f5b92d9a99..7d81c96262f 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -67,12 +67,10 @@ class SessionsController < Devise::SessionsController end def find_user - Gitlab::Auth::UniqueIpsLimiter.limit_user! do - if session[:otp_user_id] - User.find(session[:otp_user_id]) - elsif user_params[:login] - User.by_login(user_params[:login]) - end + if session[:otp_user_id] + User.find(session[:otp_user_id]) + elsif user_params[:login] + User.by_login(user_params[:login]) end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 7f25f76d0cb..00366b0a8c9 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -366,7 +366,8 @@ = f.label :unique_ips_limit_enabled do = f.check_box :unique_ips_limit_enabled Limit sign in from multiple ips - %span.help-block#recaptcha_help_block Helps prevent malicious users hide their activity + %span.help-block#unique_ip_help_block + Helps prevent malicious users hide their activity .form-group = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2' @@ -380,7 +381,7 @@ .col-sm-10 = f.number_field :unique_ips_limit_time_window, class: 'form-control' .help-block - How long an IP will be counted towards the limit + How many seconds an IP will be counted towards the limit %fieldset %legend Abuse reports diff --git a/config/application.rb b/config/application.rb index c4dea9e92b0..8003c055b27 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,8 +8,6 @@ module Gitlab class Application < Rails::Application require_dependency Rails.root.join('lib/gitlab/redis') require_dependency Rails.root.join('lib/gitlab/request_context') - require_dependency Rails.root.join('lib/gitlab/auth') - require_dependency Rails.root.join('lib/gitlab/auth/unique_ips_limiter') # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers @@ -114,8 +112,6 @@ module Gitlab config.middleware.insert_before Warden::Manager, Rack::Attack - config.middleware.insert_before Warden::Manager, Gitlab::Auth::UniqueIpsLimiter - # Allow access to GitLab API from other domains config.middleware.insert_before Warden::Manager, Rack::Cors do allow do diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 44b658e5872..88cd0f5f652 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -12,10 +12,8 @@ Doorkeeper.configure do end resource_owner_from_credentials do |routes| - Gitlab::Auth::UniqueIpsLimiter.limit_user! do - user = Gitlab::Auth.find_with_user_password(params[:username], params[:password]) - user unless user.try(:two_factor_enabled?) - end + user = Gitlab::Auth.find_with_user_password(params[:username], params[:password]) + user unless user.try(:two_factor_enabled?) end # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. diff --git a/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb index 2aa305f6b58..cbcf9a30b3c 100644 --- a/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb +++ b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb @@ -4,14 +4,14 @@ class AddUniqueIpsLimitToApplicationSettings < ActiveRecord::Migration disable_ddl_transaction! def up - add_column_with_default(:application_settings, :unique_ips_limit_per_user, :integer, default: 10) - add_column_with_default(:application_settings, :unique_ips_limit_time_window, :integer, default: 3600) - add_column_with_default(:application_settings, :unique_ips_limit_enabled, :boolean, default: false) + add_column_with_default :application_settings, :unique_ips_limit_per_user, :integer, default: 10 + add_column_with_default :application_settings, :unique_ips_limit_time_window, :integer, default: 3600 + add_column_with_default :application_settings, :unique_ips_limit_enabled, :boolean, default: false end def down - remove_column(:application_settings, :unique_ips_limit_per_user) - remove_column(:application_settings, :unique_ips_limit_time_window) - remove_column(:application_settings, :unique_ips_limit_enabled) + remove_column :application_settings, :unique_ips_limit_per_user + remove_column :application_settings, :unique_ips_limit_time_window + remove_column :application_settings, :unique_ips_limit_enabled end end diff --git a/db/schema.rb b/db/schema.rb index 8aed4e13b28..be54b177fa6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -586,9 +586,9 @@ ActiveRecord::Schema.define(version: 20170305203726) do end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree + add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["title"], name: "index_labels_on_title", using: :btree - add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 8e2aee2d7a0..0a0bd0e781c 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -22,9 +22,8 @@ module Gitlab user_with_password_for_git(login, password) || Gitlab::Auth::Result.new - Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor) - rate_limit!(ip, success: result.success?, login: login) + Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor) result end diff --git a/lib/gitlab/auth/too_many_ips.rb b/lib/gitlab/auth/too_many_ips.rb new file mode 100644 index 00000000000..ed862791551 --- /dev/null +++ b/lib/gitlab/auth/too_many_ips.rb @@ -0,0 +1,17 @@ +module Gitlab + module Auth + class TooManyIps < StandardError + attr_reader :user_id, :ip, :unique_ips_count + + def initialize(user_id, ip, unique_ips_count) + @user_id = user_id + @ip = ip + @unique_ips_count = unique_ips_count + end + + def message + "User #{user_id} from IP: #{ip} tried logging from too many ips: #{unique_ips_count}" + end + end + end +end diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb index 4b2b758be8a..7b1aa736769 100644 --- a/lib/gitlab/auth/unique_ips_limiter.rb +++ b/lib/gitlab/auth/unique_ips_limiter.rb @@ -1,19 +1,5 @@ module Gitlab module Auth - class TooManyIps < StandardError - attr_reader :user_id, :ip, :unique_ips_count - - def initialize(user_id, ip, unique_ips_count) - @user_id = user_id - @ip = ip - @unique_ips_count = unique_ips_count - end - - def message - "User #{user_id} from IP: #{ip} tried logging from too many ips: #{unique_ips_count}" - end - end - class UniqueIpsLimiter USER_UNIQUE_IPS_PREFIX = 'user_unique_ips' @@ -21,7 +7,7 @@ module Gitlab def limit_user_id!(user_id) if config.unique_ips_limit_enabled ip = RequestContext.client_ip - unique_ips = count_unique_ips(user_id, ip) + unique_ips = update_and_return_ips_count(user_id, ip) raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user end end @@ -36,8 +22,8 @@ module Gitlab Gitlab::CurrentSettings.current_application_settings end - def count_unique_ips(user_id, ip) - time = Time.now.to_i + def update_and_return_ips_count(user_id, ip) + time = Time.now.utc.to_i key = "#{USER_UNIQUE_IPS_PREFIX}:#{user_id}" Gitlab::Redis.with do |redis| @@ -51,20 +37,6 @@ module Gitlab end end end - - def initialize(app) - @app = app - end - - def call(env) - begin - @app.call(env) - rescue TooManyIps => ex - - Rails.logger.info ex.message - [403, { 'Content-Type' => 'text/plain', 'Retry-After' => UniqueIpsLimiter.config.unique_ips_limit_time_window }, ["Too many logins from different IPs\n"]] - end - end end end end diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb index 36a5d94d98a..548adf4775a 100644 --- a/lib/gitlab/request_context.rb +++ b/lib/gitlab/request_context.rb @@ -14,7 +14,6 @@ module Gitlab end def call(env) - raise RequestStoreNotActive.new unless RequestStore.active? req = Rack::Request.new(env) RequestStore[:client_ip] = req.ip diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 56ed11737ad..fe11bdf4a78 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -33,6 +33,7 @@ describe SessionsController do include_examples 'user login operation with unique ip limit' do def operation post(:create, user: { login: user.username, password: user.password }) + expect(subject.current_user).to eq user end end diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb index 2b21a76c59d..3214786d172 100644 --- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb +++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb @@ -7,36 +7,35 @@ describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do describe '#count_unique_ips' do context 'non unique IPs' do it 'properly counts them' do - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip1')).to eq(1) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip1')).to eq(1) + expect(described_class.update_and_return_ips_count(user.id, 'ip1')).to eq(1) + expect(described_class.update_and_return_ips_count(user.id, 'ip1')).to eq(1) end end context 'unique IPs' do it 'properly counts them' do - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip2')).to eq(1) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip3')).to eq(2) + expect(described_class.update_and_return_ips_count(user.id, 'ip2')).to eq(1) + expect(described_class.update_and_return_ips_count(user.id, 'ip3')).to eq(2) end end it 'resets count after specified time window' do - cur_time = Time.now - allow(Time).to receive(:now).and_return(cur_time) + Timecop.freeze do + expect(described_class.update_and_return_ips_count(user.id, 'ip2')).to eq(1) + expect(described_class.update_and_return_ips_count(user.id, 'ip3')).to eq(2) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip2')).to eq(1) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip3')).to eq(2) - - allow(Time).to receive(:now).and_return(cur_time + Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window) - - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip4')).to eq(1) - expect(Gitlab::Auth::UniqueIpsLimiter.count_unique_ips(user.id, 'ip5')).to eq(2) + Timecop.travel(Time.now.utc + described_class.config.unique_ips_limit_time_window) do + expect(described_class.update_and_return_ips_count(user.id, 'ip4')).to eq(1) + expect(described_class.update_and_return_ips_count(user.id, 'ip5')).to eq(2) + end + end end end describe '#limit_user!' do include_examples 'user login operation with unique ip limit' do def operation - Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } + described_class.limit_user! { user } end end @@ -45,13 +44,13 @@ describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do it 'blocks user trying to login from third ip' do change_ip('ip1') - expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) + expect(described_class.limit_user! { user }).to eq(user) change_ip('ip2') - expect(Gitlab::Auth::UniqueIpsLimiter.limit_user! { user }).to eq(user) + expect(described_class.limit_user! { user }).to eq(user) change_ip('ip3') - expect { Gitlab::Auth::UniqueIpsLimiter.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps) + expect { described_class.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps) end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 860189dcf3c..daf8f5c1d6c 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -206,7 +206,7 @@ describe Gitlab::Auth, lib: true do include_examples 'user login operation with unique ip limit' do def operation - expect(gl_auth.find_with_user_password(username, password)).to eql user + expect(gl_auth.find_with_user_password(username, password)).to eq(user) end end diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb index 69c5549c39c..b2828f7e5e0 100644 --- a/spec/lib/gitlab/request_context_spec.rb +++ b/spec/lib/gitlab/request_context_spec.rb @@ -26,15 +26,5 @@ describe Gitlab::RequestContext, lib: true do it { is_expected.to be_nil } end end - - context 'RequestStore is not active' do - it { is_expected.to be_nil } - - context 'when RequestContext mw is run' do - subject { -> { Gitlab::RequestContext.new(app).call(env) } } - - it { is_expected.to raise_error(Gitlab::RequestStoreNotActive) } - end - end end end From 2ff139ddee49dca7109b9f547e54c6e472c7195b Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 21 Feb 2017 22:17:23 +0100 Subject: [PATCH 91/95] Make Warden set_user hook validate user ip uniquness + rename shared context --- app/controllers/application_controller.rb | 2 +- config/initializers/warden.rb | 5 +++++ spec/controllers/sessions_controller_spec.rb | 6 ++--- .../gitlab/auth/unique_ips_limiter_spec.rb | 2 +- spec/requests/api/doorkeeper_access_spec.rb | 22 +++++++++---------- .../unique_ip_check_shared_examples.rb | 14 ++++++------ 6 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 config/initializers/warden.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d3ee4f4c1f..1c66c530cd2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -41,7 +41,7 @@ class ApplicationController < ActionController::Base end rescue_from Gitlab::Auth::TooManyIps do |e| - head :forbidden, retry_after: UniqueIpsLimiter.config.unique_ips_limit_time_window + head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window end def redirect_back_or_default(default: root_path, options: {}) diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb new file mode 100644 index 00000000000..3d83fb92d56 --- /dev/null +++ b/config/initializers/warden.rb @@ -0,0 +1,5 @@ +Rails.application.configure do |config| + Warden::Manager.after_set_user do |user, auth, opts| + Gitlab::Auth::UniqueIpsLimiter.limit_user!(user) + end +end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index fe11bdf4a78..a06c29dd91a 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -30,11 +30,11 @@ describe SessionsController do expect(SecurityEvent.last.details[:with]).to eq('standard') end - include_examples 'user login operation with unique ip limit' do - def operation + include_examples 'user login request with unique ip limit', 302 do + def request post(:create, user: { login: user.username, password: user.password }) - expect(subject.current_user).to eq user + subject.sign_out user end end end diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb index 3214786d172..94dcddcc30c 100644 --- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb +++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do - include_context 'enable unique ips sign in limit' + include_context 'unique ips sign in limit' let(:user) { create(:user) } describe '#count_unique_ips' do diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index 634b2261c5e..2974875510a 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -4,12 +4,12 @@ describe API::API, api: true do include ApiHelpers let!(:user) { create(:user) } - let!(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } - let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: 'api' } + let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } - describe 'when unauthenticated' do - it 'returns authentication success' do - get api('/user'), access_token: token.token + describe "unauthenticated" do + it "returns authentication success" do + get api("/user"), access_token: token.token expect(response).to have_http_status(200) end @@ -20,16 +20,16 @@ describe API::API, api: true do end end - describe 'when token invalid' do - it 'returns authentication error' do - get api('/user'), access_token: '123a' + describe "when token invalid" do + it "returns authentication error" do + get api("/user"), access_token: "123a" expect(response).to have_http_status(401) end end - describe 'authorization by private token' do - it 'returns authentication success' do - get api('/user', user) + describe "authorization by private token" do + it "returns authentication success" do + get api("/user", user) expect(response).to have_http_status(200) end diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index 024fb132778..772e6722fc1 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -1,4 +1,4 @@ -shared_context 'enable unique ips sign in limit' do +shared_context 'unique ips sign in limit' do include StubENV before(:each) do Gitlab::Redis.with(&:flushall) @@ -19,7 +19,7 @@ shared_context 'enable unique ips sign in limit' do end shared_examples 'user login operation with unique ip limit' do - include_context 'enable unique ips sign in limit' do + include_context 'unique ips sign in limit' do before { current_application_settings.update!(unique_ips_limit_per_user: 1) } it 'allows user authenticating from the same ip' do @@ -38,23 +38,23 @@ shared_examples 'user login operation with unique ip limit' do end end -shared_examples 'user login request with unique ip limit' do - include_context 'enable unique ips sign in limit' do +shared_examples 'user login request with unique ip limit' do |success_status = 200| + include_context 'unique ips sign in limit' do before { current_application_settings.update!(unique_ips_limit_per_user: 1) } it 'allows user authenticating from the same ip' do change_ip('ip') request - expect(response).to have_http_status(200) + expect(response).to have_http_status(success_status) request - expect(response).to have_http_status(200) + expect(response).to have_http_status(success_status) end it 'blocks user authenticating from two distinct ips' do change_ip('ip') request - expect(response).to have_http_status(200) + expect(response).to have_http_status(success_status) change_ip('ip2') request From 98bd48c66bd45241d857528791f4e2dd21bfc17a Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Wed, 22 Feb 2017 14:13:06 +0100 Subject: [PATCH 92/95] Cleanup test phases by introducing request_from_ip and operation_from_ip helers --- .../unique_ip_check_shared_examples.rb | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index 772e6722fc1..7cf5a65eeed 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -16,6 +16,17 @@ shared_context 'unique ips sign in limit' do def change_ip(ip) allow(Gitlab::RequestContext).to receive(:client_ip).and_return(ip) end + + def request_from_ip(ip) + change_ip(ip) + request + response + end + + def operation_from_ip(ip) + change_ip(ip) + operation + end end shared_examples 'user login operation with unique ip limit' do @@ -23,17 +34,13 @@ shared_examples 'user login operation with unique ip limit' do before { current_application_settings.update!(unique_ips_limit_per_user: 1) } it 'allows user authenticating from the same ip' do - change_ip('ip') - expect { operation }.not_to raise_error - expect { operation }.not_to raise_error + expect { operation_from_ip('ip') }.not_to raise_error + expect { operation_from_ip('ip') }.not_to raise_error end it 'blocks user authenticating from two distinct ips' do - change_ip('ip') - expect { operation }.not_to raise_error - - change_ip('ip2') - expect { operation }.to raise_error(Gitlab::Auth::TooManyIps) + expect { operation_from_ip('ip') }.not_to raise_error + expect { operation_from_ip('ip2') }.to raise_error(Gitlab::Auth::TooManyIps) end end end @@ -43,22 +50,13 @@ shared_examples 'user login request with unique ip limit' do |success_status = 2 before { current_application_settings.update!(unique_ips_limit_per_user: 1) } it 'allows user authenticating from the same ip' do - change_ip('ip') - request - expect(response).to have_http_status(success_status) - - request - expect(response).to have_http_status(success_status) + expect(request_from_ip('ip')).to have_http_status(success_status) + expect(request_from_ip('ip')).to have_http_status(success_status) end it 'blocks user authenticating from two distinct ips' do - change_ip('ip') - request - expect(response).to have_http_status(success_status) - - change_ip('ip2') - request - expect(response).to have_http_status(403) + expect(request_from_ip('ip')).to have_http_status(success_status) + expect(request_from_ip('ip2')).to have_http_status(403) end end end From 8a9bc24ef87739580c19ee8455bd8224d3c18b3e Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 28 Feb 2017 11:12:11 +0100 Subject: [PATCH 93/95] align schema.rb with upstream and fix rubocop warning about not freezing mutable constants and empty error classes --- db/schema.rb | 5 +++-- lib/gitlab/auth/unique_ips_limiter.rb | 2 +- lib/gitlab/request_context.rb | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index be54b177fa6..3898eed81bd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -111,10 +111,11 @@ ActiveRecord::Schema.define(version: 20170305203726) do t.boolean "plantuml_enabled" t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false - t.string "default_artifacts_expire_in", default: "0", null: false +(??) t.string "default_artifacts_expire_in", default: '0', null: false t.integer "unique_ips_limit_per_user", default: 10, null: false t.integer "unique_ips_limit_time_window", default: 3600, null: false t.boolean "unique_ips_limit_enabled", default: false, null: false + t.string "default_artifacts_expire_in", default: "0", null: false end create_table "audit_events", force: :cascade do |t| @@ -255,8 +256,8 @@ ActiveRecord::Schema.define(version: 20170305203726) do t.integer "lock_version" end + add_index "ci_commits", ["gl_project_id", "ref", "status"], name: "index_ci_commits_on_gl_project_id_and_ref_and_status", using: :btree add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree - add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb index 7b1aa736769..4d401eb1b5d 100644 --- a/lib/gitlab/auth/unique_ips_limiter.rb +++ b/lib/gitlab/auth/unique_ips_limiter.rb @@ -1,7 +1,7 @@ module Gitlab module Auth class UniqueIpsLimiter - USER_UNIQUE_IPS_PREFIX = 'user_unique_ips' + USER_UNIQUE_IPS_PREFIX = 'user_unique_ips'.freeze class << self def limit_user_id!(user_id) diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb index 548adf4775a..1dce18d1733 100644 --- a/lib/gitlab/request_context.rb +++ b/lib/gitlab/request_context.rb @@ -1,6 +1,5 @@ module Gitlab - class RequestStoreNotActive < StandardError - end + RequestStoreNotActive = Class.new(StandardError) class RequestContext class << self From 70b9d8da4c24bc2317220bedb81b5d2ecf34c351 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 3 Mar 2017 18:10:22 +0100 Subject: [PATCH 94/95] Remove unecessary defaults for uniq ip block, cleanup refactoring leftovers --- ...31347_add_unique_ips_limit_to_application_settings.rb | 4 ++-- db/schema.rb | 9 ++++----- lib/gitlab/auth/unique_ips_limiter.rb | 3 ++- lib/gitlab/request_context.rb | 2 -- spec/lib/gitlab/request_context_spec.rb | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb index cbcf9a30b3c..9ab970134be 100644 --- a/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb +++ b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb @@ -4,8 +4,8 @@ class AddUniqueIpsLimitToApplicationSettings < ActiveRecord::Migration disable_ddl_transaction! def up - add_column_with_default :application_settings, :unique_ips_limit_per_user, :integer, default: 10 - add_column_with_default :application_settings, :unique_ips_limit_time_window, :integer, default: 3600 + add_column :application_settings, :unique_ips_limit_per_user, :integer + add_column :application_settings, :unique_ips_limit_time_window, :integer add_column_with_default :application_settings, :unique_ips_limit_enabled, :boolean, default: false end diff --git a/db/schema.rb b/db/schema.rb index 3898eed81bd..911cb22c8e5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -111,11 +111,10 @@ ActiveRecord::Schema.define(version: 20170305203726) do t.boolean "plantuml_enabled" t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false -(??) t.string "default_artifacts_expire_in", default: '0', null: false - t.integer "unique_ips_limit_per_user", default: 10, null: false - t.integer "unique_ips_limit_time_window", default: 3600, null: false - t.boolean "unique_ips_limit_enabled", default: false, null: false t.string "default_artifacts_expire_in", default: "0", null: false + t.integer "unique_ips_limit_per_user" + t.integer "unique_ips_limit_time_window" + t.boolean "unique_ips_limit_enabled", default: false, null: false end create_table "audit_events", force: :cascade do |t| @@ -587,9 +586,9 @@ ActiveRecord::Schema.define(version: 20170305203726) do end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree - add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["title"], name: "index_labels_on_title", using: :btree + add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb index 4d401eb1b5d..bf2239ca150 100644 --- a/lib/gitlab/auth/unique_ips_limiter.rb +++ b/lib/gitlab/auth/unique_ips_limiter.rb @@ -8,12 +8,13 @@ module Gitlab if config.unique_ips_limit_enabled ip = RequestContext.client_ip unique_ips = update_and_return_ips_count(user_id, ip) + raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user end end def limit_user!(user = nil) - user = yield if user.nil? && block_given? + user ||= yield if block_given? limit_user_id!(user.id) unless user.nil? user end diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb index 1dce18d1733..fef536ecb0b 100644 --- a/lib/gitlab/request_context.rb +++ b/lib/gitlab/request_context.rb @@ -1,6 +1,4 @@ module Gitlab - RequestStoreNotActive = Class.new(StandardError) - class RequestContext class << self def client_ip diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb index b2828f7e5e0..a91c8655cdd 100644 --- a/spec/lib/gitlab/request_context_spec.rb +++ b/spec/lib/gitlab/request_context_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::RequestContext, lib: true do it { is_expected.to eq(ip) } end - context 'before RequestContext mw run' do + context 'before RequestContext middleware run' do it { is_expected.to be_nil } end end From 699e955324af62d3f940193d9461908fe6226c99 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 6 Mar 2017 16:04:04 +0100 Subject: [PATCH 95/95] Remove docs for removed endpoint --- doc/api/jobs.md | 104 ------------------------------------------------ 1 file changed, 104 deletions(-) diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 43451ccce4c..296f1d025dd 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -115,110 +115,6 @@ Example of response ] ``` -## List commit jobs - -Get a list of jobs for specific commit in a project. - -This endpoint will return all jobs, from all pipelines for a given commit. -If the commit SHA is not found, it will respond with 404, otherwise it will -return an array of jobs (an empty array if there are no jobs for this -particular commit). - -``` -GET /projects/:id/repository/commits/:sha/jobs -``` - -| Attribute | Type | Required | Description | -|-----------|---------|----------|---------------------| -| `id` | integer | yes | The ID of a project | -| `sha` | string | yes | The SHA id of a commit | -| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided | - -``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/jobs?scope%5B0%5D=pending&scope%5B1%5D=running' -``` - -Example of response - -```json -[ - { - "commit": { - "author_email": "admin@example.com", - "author_name": "Administrator", - "created_at": "2015-12-24T16:51:14.000+01:00", - "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", - "message": "Test the CI integration.", - "short_id": "0ff3ae19", - "title": "Test the CI integration." - }, - "coverage": null, - "created_at": "2016-01-11T10:13:33.506Z", - "artifacts_file": null, - "finished_at": "2016-01-11T10:14:09.526Z", - "id": 69, - "name": "rubocop", - "pipeline": { - "id": 6, - "ref": "master", - "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", - "status": "pending" - }, - "ref": "master", - "runner": null, - "stage": "test", - "started_at": null, - "status": "canceled", - "tag": false, - "user": null - }, - { - "commit": { - "author_email": "admin@example.com", - "author_name": "Administrator", - "created_at": "2015-12-24T16:51:14.000+01:00", - "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", - "message": "Test the CI integration.", - "short_id": "0ff3ae19", - "title": "Test the CI integration." - }, - "coverage": null, - "created_at": "2015-12-24T15:51:21.957Z", - "artifacts_file": null, - "finished_at": "2015-12-24T17:54:33.913Z", - "id": 9, - "name": "brakeman", - "pipeline": { - "id": 6, - "ref": "master", - "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", - "status": "pending" - }, - "ref": "master", - "runner": null, - "stage": "test", - "started_at": "2015-12-24T17:54:33.727Z", - "status": "failed", - "tag": false, - "user": { - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "bio": null, - "created_at": "2015-12-21T13:14:24.077Z", - "id": 1, - "is_admin": true, - "linkedin": "", - "name": "Administrator", - "skype": "", - "state": "active", - "twitter": "", - "username": "root", - "web_url": "http://gitlab.dev/root", - "website_url": "" - } - } -] -``` - ## Get a single job Get a single job of a project