From 6a0ea605e8b48deacbb4e93f7bb1d9b9abd2f7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Emin=20=C4=B0NA=C3=87?= Date: Wed, 16 Mar 2016 03:16:25 +0200 Subject: [PATCH 001/199] Change deprecated usage of rendering without response body `render nothing: true` has been deprecated. For more information see [pr](https://github.com/rails/rails/pull/20336) --- app/controllers/admin/abuse_reports_controller.rb | 2 +- app/controllers/admin/broadcast_messages_controller.rb | 2 +- app/controllers/admin/keys_controller.rb | 2 +- app/controllers/admin/spam_logs_controller.rb | 2 +- app/controllers/admin/users_controller.rb | 2 +- app/controllers/concerns/toggle_subscription_action.rb | 2 +- app/controllers/dashboard/todos_controller.rb | 4 ++-- app/controllers/groups/group_members_controller.rb | 2 +- app/controllers/profiles/emails_controller.rb | 2 +- app/controllers/profiles/keys_controller.rb | 2 +- app/controllers/projects/milestones_controller.rb | 2 +- app/controllers/projects/notes_controller.rb | 4 ++-- app/controllers/projects/project_members_controller.rb | 4 ++-- app/controllers/projects/protected_branches_controller.rb | 2 +- spec/controllers/projects/raw_controller_spec.rb | 2 +- 15 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index 2463cfa87b..76fc10bcc1 100644 --- a/app/controllers/admin/abuse_reports_controller.rb +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -9,6 +9,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController abuse_report.remove_user if params[:remove_user] abuse_report.destroy - render nothing: true + head :ok end end diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index fc34292498..82055006ac 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -32,7 +32,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_default(default: { action: 'index' }) } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb index cb33fdd976..054bb52b69 100644 --- a/app/controllers/admin/keys_controller.rb +++ b/app/controllers/admin/keys_controller.rb @@ -6,7 +6,7 @@ class Admin::KeysController < Admin::ApplicationController respond_to do |format| format.html - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index 377e9741e5..3a2f018531 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -11,7 +11,7 @@ class Admin::SpamLogsController < Admin::ApplicationController redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed." else spam_log.destroy - render nothing: true + head :ok end end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 87f4fb455b..39c0c22f9b 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -135,7 +135,7 @@ class Admin::UsersController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb index 8a43c0b93c..9e3b9be2ff 100644 --- a/app/controllers/concerns/toggle_subscription_action.rb +++ b/app/controllers/concerns/toggle_subscription_action.rb @@ -6,7 +6,7 @@ module ToggleSubscriptionAction subscribable_resource.toggle_subscription(current_user) - render nothing: true + head :ok end private diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 43cf8fa71a..d8ba51294c 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -10,7 +10,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' } - format.js { render nothing: true } + format.js { head :ok } end end @@ -19,7 +19,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 0e902c4bb4..68f7012089 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -43,7 +43,7 @@ class Groups::GroupMembersController < Groups::ApplicationController respond_to do |format| format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 0ede9b8e21..1c24c4db99 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -24,7 +24,7 @@ class Profiles::EmailsController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_emails_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index b88c080352..9906493666 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -27,7 +27,7 @@ class Profiles::KeysController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_keys_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index da46731d94..6579f4f8c8 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -68,7 +68,7 @@ class Projects::MilestonesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_milestones_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 1b9dd56804..d91ab1cee1 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -44,7 +44,7 @@ class Projects::NotesController < Projects::ApplicationController end respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end @@ -53,7 +53,7 @@ class Projects::NotesController < Projects::ApplicationController note.update_attribute(:attachment, nil) respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e7bddc4a6f..b150e9ef02 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -55,7 +55,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController format.html do redirect_to namespace_project_project_members_path(@project.namespace, @project) end - format.js { render nothing: true } + format.js { head :ok } end end @@ -81,7 +81,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController respond_to do |format| format.html { redirect_to dashboard_projects_path, notice: "You left the project." } - format.js { render nothing: true } + format.js { head :ok } end else if current_user == @project.owner diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index e49259c34b..efa7bf14d0 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -39,7 +39,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_protected_branches_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 1caa476d37..fb29274c68 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -42,7 +42,7 @@ describe Projects::RawController do before do public_project.lfs_objects << lfs_object allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) - allow(controller).to receive(:send_file) { controller.render nothing: true } + allow(controller).to receive(:send_file) { controller.head :ok } end it 'serves the file' do From 1824fb0603c798ec467ea3529570031e7dbb2986 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 5 Apr 2016 15:20:58 +0000 Subject: [PATCH 002/199] Fix broken link in CI quickstart docs The space between the [label] and the (link) caused it to be interpreted literally. --- doc/ci/quick_start/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 9aba4326e1..aae9ccae1d 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -212,8 +212,8 @@ If you want to receive e-mail notifications about the result status of the builds, you should explicitly enable the **Builds Emails** service under your project's settings. -For more information read the [Builds emails service documentation] -(../../project_services/builds_emails.md). +For more information read the +[Builds emails service documentation](../../project_services/builds_emails.md). ## Builds badge From ff1e7474ed0f210df004c714e1b83c1c2eb0d91c Mon Sep 17 00:00:00 2001 From: Andrew Collett Date: Thu, 21 Apr 2016 10:30:27 +0000 Subject: [PATCH 003/199] Update cas.md to reflect the current syntax, and added that gitlab-ctl reconfigure should be run. --- doc/integration/cas.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/doc/integration/cas.md b/doc/integration/cas.md index e6b2071f19..e34e306f9a 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -27,17 +27,18 @@ To enable the CAS OmniAuth provider you must register your application with your ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "cas3", - label: "cas", - args: { - url: 'CAS_SERVER', - login_url: '/CAS_PATH/login', - service_validate_url: '/CAS_PATH/p3/serviceValidate', - logout_url: '/CAS_PATH/logout'} } - } + "name"=> "cas3", + "label"=> "cas", + "args"=> { + "url"=> 'CAS_SERVER', + "login_url"=> '/CAS_PATH/login', + "service_validate_url"=> '/CAS_PATH/p3/serviceValidate', + "logout_url"=> '/CAS_PATH/logout' + } } ] ``` + For installations from source: @@ -57,6 +58,8 @@ To enable the CAS OmniAuth provider you must register your application with your 1. Save the configuration file. +1. Run `gitlab-ctl reconfigure` for the omnibus package. + 1. Restart GitLab for the changes to take effect. On the sign in page there should now be a CAS tab in the sign in form. From f41a3e24d20b26b53c5321571ef89f441c32aa4d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Apr 2016 08:13:16 -0400 Subject: [PATCH 004/199] Added authentication service for docker registry --- Gemfile | 1 + Gemfile.lock | 1 + app/models/ability.rb | 8 +- app/models/ci/build.rb | 1 + app/models/project.rb | 5 + config/initializers/1_settings.rb | 39 ++++ ...07120251_add_images_enabled_for_project.rb | 5 + db/schema.rb | 1 + lib/api/api.rb | 1 + lib/api/auth.rb | 166 ++++++++++++++++++ 10 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20160407120251_add_images_enabled_for_project.rb create mode 100644 lib/api/auth.rb diff --git a/Gemfile b/Gemfile index 7882e467f8..512c6babd7 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' gem 'rack-oauth2', '~> 1.2.1' +gem 'jwt' # Spam and anti-bot protection gem 'recaptcha', require: 'recaptcha/rails' diff --git a/Gemfile.lock b/Gemfile.lock index 1dcda0daff..2b578429b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -957,6 +957,7 @@ DEPENDENCIES jquery-scrollto-rails (~> 1.4.3) jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 5.0.0) + jwt kaminari (~> 0.16.3) letter_opener_web (~> 1.3.0) licensee (~> 8.0.0) diff --git a/app/models/ability.rb b/app/models/ability.rb index 6103a2947e..ba27b9a9b1 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -203,6 +203,7 @@ class Ability :admin_label, :read_commit_status, :read_build, + :read_image, ] end @@ -216,7 +217,9 @@ class Ability :update_build, :create_merge_request, :create_wiki, - :push_code + :push_code, + :create_image, + :update_image, ] end @@ -242,7 +245,8 @@ class Ability :admin_wiki, :admin_project, :admin_commit_status, - :admin_build + :admin_build, + :admin_image ] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 553cd44797..c2ddee527e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -426,6 +426,7 @@ module Ci variables << { key: :CI_BUILD_NAME, value: name, public: true } variables << { key: :CI_BUILD_STAGE, value: stage, public: true } variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request + variables << { key: :CI_DOCKER_REGISTRY, value: project.registry_repository_url, public: true } if project.registry_repository_url variables end end diff --git a/app/models/project.rb b/app/models/project.rb index 5c6c36e6b3..76265a59ea 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -64,6 +64,7 @@ class Project < ActiveRecord::Base default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets + default_value_for :images_enabled, gitlab_config_features.images default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at @@ -369,6 +370,10 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end + def registry_repository_url + "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" if images_enabled? && Gitlab.config.registry.enabled + end + def commit(id = 'HEAD') repository.commit(id) end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8db2c05fe4..01ee8a0d52 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -27,6 +27,30 @@ class Settings < Settingslogic ].join('') end + def build_registry_api_url + if registry.port.to_i == (registry.https ? 443 : 80) + custom_port = nil + else + custom_port = ":#{registry.port}" + end + [ registry.protocol, + "://", + registry.internal_host, + custom_port + ].join('') + end + + def build_registry_host_with_port + if registry.port.to_i == (registry.https ? 443 : 80) + custom_port = nil + else + custom_port = ":#{registry.port}" + end + [ registry.host, + custom_port + ].join('') + end + def build_gitlab_shell_ssh_path_prefix user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" @@ -211,6 +235,7 @@ Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.g Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? +Settings.gitlab.default_projects_features['images'] = true if Settings.gitlab.default_projects_features['images'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] @@ -242,6 +267,20 @@ Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) Settings.artifacts['max_size'] ||= 100 # in megabytes +# +# Registry +# +Settings['registry'] ||= Settingslogic.new({}) +Settings.registry['registry'] = false if Settings.registry['enabled'].nil? +Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) +Settings.registry['host'] ||= "example.com" +Settings.registry['internal_host']||= "localhost" +Settings.registry['https'] = false if Settings.registry['https'].nil? +Settings.registry['port'] ||= Settings.registry.https ? 443 : 80 +Settings.registry['protocol'] ||= Settings.registry.https ? "https" : "http" +Settings.registry['api_url'] ||= Settings.send(:build_registry_api_url) +Settings.registry['host_port'] ||= Settings.send(:build_registry_host_with_port) + # # Git LFS # diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb new file mode 100644 index 0000000000..6a221a7fb0 --- /dev/null +++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb @@ -0,0 +1,5 @@ +class AddImagesEnabledForProject < ActiveRecord::Migration + def change + add_column :projects, :images_enabled, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 42457d9235..bf46028d23 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -760,6 +760,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" + t.boolean "images_enabled" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree diff --git a/lib/api/api.rb b/lib/api/api.rb index cc1004f800..6ddfe11d98 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -58,5 +58,6 @@ module API mount Variables mount Runners mount Licenses + mount Auth end end diff --git a/lib/api/auth.rb b/lib/api/auth.rb new file mode 100644 index 0000000000..b992e49730 --- /dev/null +++ b/lib/api/auth.rb @@ -0,0 +1,166 @@ +module API + # Projects builds API + class Auth < Grape::API + namespace 'auth' do + get 'token' do + required_attributes! [:scope, :service] + keys = attributes_for_keys [:scope, :service] + + case keys[:service] + when 'docker' + docker_token_auth(keys[:scope]) + else + not_found! + end + end + end + + helpers do + def docker_token_auth(scope) + @type, @path, actions = scope.split(':', 3) + bad_request!("invalid type: #{type}") unless type == 'repository' + + @actions = actions.split(',') + bad_request!('missing actions') if @actions.empty? + + @project = Project.find_with_namespace(path) + not_found!('Project') unless @project + + auth! + + authorize_actions!(@actions) + + { token: encode(docker_payload) } + end + + def auth! + auth = BasicRequest.new(request.env) + return unless auth.provided? + + return bad_request unless auth.basic? + + # Authentication with username and password + login, password = auth.credentials + + if ci_request?(login, password) + @ci = true + return + end + + @user = authenticate_user(login, password) + + if @user + request.env['REMOTE_USER'] = @auth.username + end + end + + def ci_request?(login, password) + matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) + + if @project && matched_login.present? + underscored_service = matched_login['s'].underscore + + if underscored_service == 'gitlab_ci' + return @project.valid_build_token?(password) + end + end + + false + end + + def authenticate_user(login, password) + user = Gitlab::Auth.new.find(login, password) + + unless user + user = oauth_access_token_check(login, password) + end + + # If the user authenticated successfully, we reset the auth failure count + # from Rack::Attack for that IP. A client may attempt to authenticate + # with a username and blank password first, and only after it receives + # a 401 error does it present a password. Resetting the count prevents + # false positives from occurring. + # + # Otherwise, we let Rack::Attack know there was a failed authentication + # attempt from this IP. This information is stored in the Rails cache + # (Redis) and will be used by the Rack::Attack middleware to decide + # whether to block requests from this IP. + config = Gitlab.config.rack_attack.git_basic_auth + + if config.enabled + if user + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(@request.ip, config) + else + banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(@request.ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{@request.ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end + end + + user + end + + def docker_payload + { + access: [ + type: @type, + name: @path, + actions: @actions + ], + exp: Time.now.to_i + 3600 + } + end + + def private_key + @private_key ||= OpenSSL::PKey::RSA.new File.read 'config/registry.key' + end + + def encode(payload) + JWT.encode(payload, private_key, 'RS256') + end + + def authorize_actions!(actions) + actions.each do |action| + forbidden! unless can_access?(action) + end + end + + def can_access?(action) + case action + when 'pull' + @ci || can?(@user, :download_code, @project) + when 'push' + @ci || can?(@user, :push_code, @project) + else + false + end + end + + class BasicRequest < Rack::Auth::AbstractRequest + def basic? + "basic" == scheme + end + + def credentials + @credentials ||= params.unpack("m*").first.split(/:/, 2) + end + + def username + credentials.first + end + end + end + end +end From 03b3fe13f6af67f8117cf4322b605630f55f3136 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Apr 2016 08:23:17 -0400 Subject: [PATCH 005/199] Make images_enabled configurable --- app/controllers/projects_controller.rb | 3 ++- app/views/projects/edit.html.haml | 10 ++++++++++ lib/api/entities.rb | 3 ++- lib/api/projects.rb | 5 +++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3768efe142..52f7b99334 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -235,7 +235,8 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, + :issues_enabled, :merge_requests_enabled, :snippets_enabled, :images_enabled, + :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :public_builds, diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 76a4f41193..5c7960031e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -84,6 +84,16 @@ %br %span.descr Share code pastes with others out of git repository + - if Gitlab.config.registry.enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :images_enabled do + = f.check_box :images_enabled + %strong Images + %br + %span.descr Use Docker Registry for this repository + = render 'builds_settings', f: f %fieldset.features diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 716ca6f7ed..95c3597b03 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -66,7 +66,8 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :images_enabled + expose :created_at, :last_activity_at expose :shared_runners_enabled expose :creator_id expose :namespace diff --git a/lib/api/projects.rb b/lib/api/projects.rb index cc2c7a0c50..6f85bc4b1b 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -94,6 +94,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # images_enabled (optional) # shared_runners_enabled (optional) # namespace_id (optional) - defaults to user namespace # public (optional) - if true same as setting visibility_level = 20 @@ -112,6 +113,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :images_enabled, :shared_runners_enabled, :namespace_id, :public, @@ -143,6 +145,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # images_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) @@ -206,6 +209,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # images_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project @@ -222,6 +226,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :images_enabled, :shared_runners_enabled, :public, :visibility_level, From 0ca8db25f008cd3bc4f2df0f58efd739718323d0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Apr 2016 10:55:10 -0400 Subject: [PATCH 006/199] Try to fix auth service --- lib/api/auth.rb | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/api/auth.rb b/lib/api/auth.rb index b992e49730..ec944b1dc8 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -3,12 +3,12 @@ module API class Auth < Grape::API namespace 'auth' do get 'token' do - required_attributes! [:scope, :service] - keys = attributes_for_keys [:scope, :service] + required_attributes! [:service] + keys = attributes_for_keys [:offline_token, :scope, :service] case keys[:service] when 'docker' - docker_token_auth(keys[:scope]) + docker_token_auth(keys[:scope], keys[:offline_token]) else not_found! end @@ -16,19 +16,23 @@ module API end helpers do - def docker_token_auth(scope) - @type, @path, actions = scope.split(':', 3) - bad_request!("invalid type: #{type}") unless type == 'repository' - - @actions = actions.split(',') - bad_request!('missing actions') if @actions.empty? - - @project = Project.find_with_namespace(path) - not_found!('Project') unless @project - + def docker_token_auth(scope, offline_token) auth! - authorize_actions!(@actions) + if offline_token + forbidden! unless @user + elsif scope + @type, @path, actions = scope.split(':', 3) + bad_request!("invalid type: #{@type}") unless @type == 'repository' + + @actions = actions.split(',') + bad_request!('missing actions') if @actions.empty? + + @project = Project.find_with_namespace(@path) + not_found!('Project') unless @project + + authorize_actions!(@actions) + end { token: encode(docker_payload) } end @@ -50,7 +54,7 @@ module API @user = authenticate_user(login, password) if @user - request.env['REMOTE_USER'] = @auth.username + request.env['REMOTE_USER'] = @user.username end end @@ -71,10 +75,6 @@ module API def authenticate_user(login, password) user = Gitlab::Auth.new.find(login, password) - unless user - user = oauth_access_token_check(login, password) - end - # If the user authenticated successfully, we reset the auth failure count # from Rack::Attack for that IP. A client may attempt to authenticate # with a username and blank password first, and only after it receives From 72611f9cfa9014653c0894115af6223687c2eab4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Apr 2016 13:57:35 -0400 Subject: [PATCH 007/199] Auth token --- config/gitlab.yml.example | 10 ++++++++++ config/initializers/1_settings.rb | 1 + lib/api/auth.rb | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 07ce4b6d71..e55ca6f9c6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -176,6 +176,16 @@ production: &base repository_archive_cache_worker: cron: "0 * * * *" + registry: + # enabled: true + # host: localhost + # port: 5000 + # https: false + # internal_host: localhost + # key: config/registry.key + # issuer: omnibus-certificate + # path: shared/registry + # # 2. GitLab CI settings # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 01ee8a0d52..b94f3f2f90 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -275,6 +275,7 @@ Settings.registry['registry'] = false if Settings.registry['enabled'].nil? Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) Settings.registry['host'] ||= "example.com" Settings.registry['internal_host']||= "localhost" +Settings.registry['key'] ||= nil Settings.registry['https'] = false if Settings.registry['https'].nil? Settings.registry['port'] ||= Settings.registry.https ? 443 : 80 Settings.registry['protocol'] ||= Settings.registry.https ? "https" : "http" diff --git a/lib/api/auth.rb b/lib/api/auth.rb index ec944b1dc8..d769c69275 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -119,12 +119,13 @@ module API name: @path, actions: @actions ], + iss: Gitlab.config.registry.issuer, exp: Time.now.to_i + 3600 } end def private_key - @private_key ||= OpenSSL::PKey::RSA.new File.read 'config/registry.key' + @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key end def encode(payload) From 8aac802eaf417a4f484f099089410934cdfdb0b7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Apr 2016 14:16:17 -0400 Subject: [PATCH 008/199] Audience --- lib/api/auth.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/api/auth.rb b/lib/api/auth.rb index d769c69275..e4ce9bf122 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -120,6 +120,7 @@ module API actions: @actions ], iss: Gitlab.config.registry.issuer, + aud: "docker", exp: Time.now.to_i + 3600 } end From 5fc310b440a7bb3ead91760ac2b7cbb1cee72f2a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 21 Apr 2016 10:02:24 +0200 Subject: [PATCH 009/199] Missing parameters of docker payload --- lib/api/auth.rb | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/api/auth.rb b/lib/api/auth.rb index e4ce9bf122..dab04bca81 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -113,6 +113,7 @@ module API end def docker_payload + issued_at = Time.now { access: [ type: @type, @@ -121,8 +122,14 @@ module API ], iss: Gitlab.config.registry.issuer, aud: "docker", + sub: @user.try(:username), + aud: @service, + iat: issued_at, + nbf: issued_at - 5.seconds, + exp: issued_at + 60.minutes, + jti: SecureRandom.uuid, exp: Time.now.to_i + 3600 - } + }.compact end def private_key @@ -130,7 +137,10 @@ module API end def encode(payload) - JWT.encode(payload, private_key, 'RS256') + headers = { + kid: kid(private_key) + } + JWT.encode(payload, private_key, 'RS256', headers) end def authorize_actions!(actions) @@ -150,6 +160,15 @@ module API end end + def kid(private_key) + sha256 = Digest::SHA256.new + sha256.update(private_key.public_key.to_der) + payload = StringIO.new(sha256.digest).read(30) + Base32.encode(payload).split("").each_slice(4).each_with_object([]) do |slice, mem| + mem << slice.join + end.join(":") + end + class BasicRequest < Rack::Auth::AbstractRequest def basic? "basic" == scheme From 0a280158efeb7f681589ae7af24f0ed9052de809 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 15 Apr 2016 19:23:33 +0530 Subject: [PATCH 010/199] Eager load `lib/api` - So that the server doesn't have to be restarted for every change in dev. --- config/application.rb | 2 + config/routes.rb | 1 - lib/api/api.rb | 4 +- lib/api/api_guard.rb | 316 +++++++++++++++++++++--------------------- lib/ci/api/api.rb | 2 +- 5 files changed, 163 insertions(+), 162 deletions(-) diff --git a/config/application.rb b/config/application.rb index 2e2ed48db0..abe22691ad 100644 --- a/config/application.rb +++ b/config/application.rb @@ -79,6 +79,8 @@ module Gitlab # This is needed for gitlab-shell ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH'] + config.eager_load_paths += ["#{Rails.root}/lib"] + config.generators do |g| g.factory_girl false end diff --git a/config/routes.rb b/config/routes.rb index 5ce1f49ec6..adf4bb18b3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ require 'sidekiq/web' require 'sidekiq/cron/web' -require 'api/api' Rails.application.routes.draw do if Gitlab::Sherlock.enabled? diff --git a/lib/api/api.rb b/lib/api/api.rb index 6ddfe11d98..d41b4b7186 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,8 +1,6 @@ -Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} - module API class API < Grape::API - include APIGuard + include ::API::APIGuard version 'v3', using: :path rescue_from ActiveRecord::RecordNotFound do diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index b9994fcefd..6dfd6e4396 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -2,171 +2,173 @@ require 'rack/oauth2' -module APIGuard - extend ActiveSupport::Concern +module API + module APIGuard + extend ActiveSupport::Concern - included do |base| - # OAuth2 Resource Server Authentication - use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| - # The authenticator only fetches the raw token string + included do |base| + # OAuth2 Resource Server Authentication + use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| + # The authenticator only fetches the raw token string - # Must yield access token to store it in the env - request.access_token - end - - helpers HelperMethods - - install_error_responders(base) - end - - # Helper Methods for Grape Endpoint - module HelperMethods - # Invokes the doorkeeper guard. - # - # If token is presented and valid, then it sets @current_user. - # - # If the token does not have sufficient scopes to cover the requred scopes, - # then it raises InsufficientScopeError. - # - # If the token is expired, then it raises ExpiredError. - # - # If the token is revoked, then it raises RevokedError. - # - # If the token is not found (nil), then it raises TokenNotFoundError. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def doorkeeper_guard!(scopes: []) - if (access_token = find_access_token).nil? - raise TokenNotFoundError - - else - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end + # Must yield access token to store it in the env + request.access_token end + + helpers HelperMethods + + install_error_responders(base) end - def doorkeeper_guard(scopes: []) - if access_token = find_access_token - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) + # Helper Methods for Grape Endpoint + module HelperMethods + # Invokes the doorkeeper guard. + # + # If token is presented and valid, then it sets @current_user. + # + # If the token does not have sufficient scopes to cover the requred scopes, + # then it raises InsufficientScopeError. + # + # If the token is expired, then it raises ExpiredError. + # + # If the token is revoked, then it raises RevokedError. + # + # If the token is not found (nil), then it raises TokenNotFoundError. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def doorkeeper_guard!(scopes: []) + if (access_token = find_access_token).nil? + raise TokenNotFoundError - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end - end - end - - def current_user - @current_user - end - - private - def find_access_token - @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) - end - - def doorkeeper_request - @doorkeeper_request ||= ActionDispatch::Request.new(env) - end - - def validate_access_token(access_token, scopes) - Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) - end - end - - module ClassMethods - # Installs the doorkeeper guard on the whole Grape API endpoint. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def guard_all!(scopes: []) - before do - guard! scopes: scopes - end - end - - private - def install_error_responders(base) - error_classes = [ MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] - - base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler - end - - def oauth2_bearer_token_error_handler - Proc.new do |e| - response = - case e - when MissingTokenError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new - - when TokenNotFoundError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Bad Access Token.") - - when ExpiredError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token is expired. You can either do re-authorization or token refresh.") - - when RevokedError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token was revoked. You have to re-authorize from the user.") - - when InsufficientScopeError - # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) - # does not include WWW-Authenticate header, which breaks the standard. - Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( - :insufficient_scope, - Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], - { scope: e.scopes }) + else + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) end + end + end - response.finish + def doorkeeper_guard(scopes: []) + if access_token = find_access_token + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end + + def current_user + @current_user + end + + private + def find_access_token + @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + end + + def doorkeeper_request + @doorkeeper_request ||= ActionDispatch::Request.new(env) + end + + def validate_access_token(access_token, scopes) + Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) + end + end + + module ClassMethods + # Installs the doorkeeper guard on the whole Grape API endpoint. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def guard_all!(scopes: []) + before do + guard! scopes: scopes + end + end + + private + def install_error_responders(base) + error_classes = [ MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] + + base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + end + + def oauth2_bearer_token_error_handler + Proc.new do |e| + response = + case e + when MissingTokenError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new + + when TokenNotFoundError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Bad Access Token.") + + when ExpiredError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token is expired. You can either do re-authorization or token refresh.") + + when RevokedError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token was revoked. You have to re-authorize from the user.") + + when InsufficientScopeError + # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) + # does not include WWW-Authenticate header, which breaks the standard. + Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( + :insufficient_scope, + Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], + { scope: e.scopes }) + end + + response.finish + end + end + end + + # + # Exceptions + # + + class MissingTokenError < StandardError; end + + class TokenNotFoundError < StandardError; end + + class ExpiredError < StandardError; end + + class RevokedError < StandardError; end + + class InsufficientScopeError < StandardError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes end end end - - # - # Exceptions - # - - class MissingTokenError < StandardError; end - - class TokenNotFoundError < StandardError; end - - class ExpiredError < StandardError; end - - class RevokedError < StandardError; end - - class InsufficientScopeError < StandardError - attr_reader :scopes - def initialize(scopes) - @scopes = scopes - end - end -end +end \ No newline at end of file diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 353c4ddebf..7cd8b6fbae 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -3,7 +3,7 @@ Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file} module Ci module API class API < Grape::API - include APIGuard + include ::API::APIGuard version 'v1', using: :path rescue_from ActiveRecord::RecordNotFound do From c0f02aad4a1a178109a235d34bd70218c0aec86c Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Mon, 2 May 2016 16:37:12 +0700 Subject: [PATCH 011/199] Add snippet tab under user profile --- app/assets/javascripts/user_tabs.js.coffee | 9 ++++++++- app/controllers/users_controller.rb | 22 ++++++++++++++++++++++ app/views/users/show.html.haml | 6 ++++++ config/routes.rb | 5 +++-- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee index 09b7eec910..aa798b96ed 100644 --- a/app/assets/javascripts/user_tabs.js.coffee +++ b/app/assets/javascripts/user_tabs.js.coffee @@ -26,6 +26,10 @@ # Personal projects # # +#
  • +# +# +#
  • # # #
    @@ -41,6 +45,9 @@ #
    # Projects content #
    +#
    +# Snippets content +#
    #
    # #
    @@ -100,7 +107,7 @@ class @UserTabs if action is 'activity' @loadActivities(source) - if action in ['groups', 'contributed', 'projects'] + if action in ['groups', 'contributed', 'projects', 'snippets'] @loadTab(source, action) loadTab: (source, action) -> diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2ae180c8a1..799421c185 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -58,6 +58,19 @@ class UsersController < ApplicationController end end + def snippets + load_snippets + + respond_to do |format| + format.html { render 'show' } + format.json do + render json: { + html: view_to_html_string("snippets/_snippets", collection: @snippets) + } + end + end + end + def calendar calendar = contributions_calendar @timestamps = calendar.timestamps @@ -116,6 +129,15 @@ class UsersController < ApplicationController @groups = JoinedGroupsFinder.new(user).execute(current_user) end + def load_snippets + @snippets = SnippetsFinder.new.execute( + current_user, + filter: :by_user, + user: user, + scope: params[:scope] + ).page(params[:page]) + end + def projects_for_current_user ProjectsFinder.new.execute(current_user) end diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 3028491e5b..a453a7fedb 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -81,6 +81,9 @@ %li.projects-tab = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects + %li.snippets-tab + = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + Snippets %div{ class: container_class } .tab-content @@ -104,6 +107,9 @@ #projects.tab-pane - # This tab is always loaded via AJAX + #snippets.tab-pane + - # This tab is always loaded via AJAX + .loading-status = spinner diff --git a/config/routes.rb b/config/routes.rb index 2f820aafed..f6a41331ec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -89,8 +89,6 @@ Rails.application.routes.draw do end end - get '/s/:username' => 'snippets#index', as: :user_snippets, constraints: { username: /.*/ } - # # Invites # @@ -355,6 +353,9 @@ Rails.application.routes.draw do get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects, constraints: { username: /.*/ } + get 'u/:username/snippets' => 'users#snippets', as: :user_snippets, + constraints: { username: /.*/ } + get '/u/:username' => 'users#show', as: :user, constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Mon, 2 May 2016 13:29:17 +0200 Subject: [PATCH 012/199] Added JWT controller --- Gemfile | 1 + Gemfile.lock | 2 + app/controllers/jwt_controller.rb | 173 ++++++++++++++++++++++++++++++ config/routes.rb | 3 + 4 files changed, 179 insertions(+) create mode 100644 app/controllers/jwt_controller.rb diff --git a/Gemfile b/Gemfile index 512c6babd7..0301f6fe06 100644 --- a/Gemfile +++ b/Gemfile @@ -225,6 +225,7 @@ gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' +gem 'base32', '~> 0.3.0' # Sentry integration gem 'sentry-raven', '~> 0.15' diff --git a/Gemfile.lock b/Gemfile.lock index 2b578429b3..2b1cfdc9bb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,7 @@ GEM ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) + base32 (0.3.2) bcrypt (3.1.10) benchmark-ips (2.3.0) better_errors (1.0.1) @@ -897,6 +898,7 @@ DEPENDENCIES attr_encrypted (~> 1.3.4) awesome_print (~> 1.2.0) babosa (~> 1.0.2) + base32 (~> 0.3.0) benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb new file mode 100644 index 0000000000..7e70c70c89 --- /dev/null +++ b/app/controllers/jwt_controller.rb @@ -0,0 +1,173 @@ +class JwtController < ApplicationController + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + + def auth + @authenticated = authenticate_with_http_basic do |login, password| + @ci_project = ci_project(login, password) + @user = authenticate_user(login, password) unless @ci_project + end + + unless @authenticated + return render_403 if has_basic_credentials? + end + + case params[:service] + when 'docker' + docker_token_auth(params[:scope], params[:offline_token]) + else + return render_404 + end + end + + private + + def render_400 + head :invalid_request + end + + def render_404 + head :not_found + end + + def render_403 + head :forbidden + end + + def docker_token_auth(scope, offline_token) + payload = { + aud: params[:service], + sub: @user.try(:username) + } + + if offline_token + return render_403 unless @user + elsif scope + access = process_access(scope) + return render_404 unless access + payload[:access] = [access] + end + + render json: { token: encode(payload) } + end + + def ci_project(login, password) + matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) + + if matched_login.present? + underscored_service = matched_login['s'].underscore + + if underscored_service == 'gitlab_ci' + Project.find_by(builds_enabled: true, runners_token: password) + end + end + end + + def authenticate_user(login, password) + user = Gitlab::Auth.new.find(login, password) + + # If the user authenticated successfully, we reset the auth failure count + # from Rack::Attack for that IP. A client may attempt to authenticate + # with a username and blank password first, and only after it receives + # a 401 error does it present a password. Resetting the count prevents + # false positives from occurring. + # + # Otherwise, we let Rack::Attack know there was a failed authentication + # attempt from this IP. This information is stored in the Rails cache + # (Redis) and will be used by the Rack::Attack middleware to decide + # whether to block requests from this IP. + config = Gitlab.config.rack_attack.git_basic_auth + + if config.enabled + if user + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(request.ip, config) + else + banned = Rack::Attack::Allow2Ban.filter(request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(request.ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{request.ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end + end + + user + end + + def process_access(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + project = Project.find_with_namespace(name) + return unless project + + actions = actions.select do |action| + can_access?(project, action) + end + + { type: 'repository', name: name, actions: actions } if actions + end + + def default_payload + { + aud: 'docker', + sub: @user.try(:username), + aud: params[:service], + } + end + + def private_key + @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key + end + + def encode(payload) + issued_at = Time.now + payload = payload.merge( + iss: Gitlab.config.registry.issuer, + iat: issued_at.to_i, + nbf: issued_at.to_i - 5.seconds.to_i, + exp: issued_at.to_i + 60.minutes.to_i, + jti: SecureRandom.uuid, + ) + headers = { + kid: kid(private_key) + } + JWT.encode(payload, private_key, 'RS256', headers) + end + + def can_access?(project, action) + case action + when 'pull' + project == @ci_project || can?(@user, :download_code, project) + when 'push' + project == @ci_project || can?(@user, :push_code, project) + else + false + end + end + + def kid(private_key) + sha256 = Digest::SHA256.new + sha256.update(private_key.public_key.to_der) + payload = StringIO.new(sha256.digest).read(30) + Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| + mem << slice.join + end.join(':') + end +end diff --git a/config/routes.rb b/config/routes.rb index adf4bb18b3..5b48819dd9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -63,6 +63,9 @@ Rails.application.routes.draw do get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete + # JSON Web Token + get 'jwt/auth' => 'jwt#auth' + # API API::API.logger Rails.logger mount API::API => '/api' From 011a905a821e2ff0cd2d9885ef93764018eb8346 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 2 May 2016 14:32:16 +0200 Subject: [PATCH 013/199] Split docker authentication service --- app/controllers/jwt_controller.rb | 116 +++--------------- .../jwt/docker_authentication_service.rb | 65 ++++++++++ lib/jwt/rsa_token.rb | 36 ++++++ lib/jwt/token.rb | 48 ++++++++ 4 files changed, 163 insertions(+), 102 deletions(-) create mode 100644 app/services/jwt/docker_authentication_service.rb create mode 100644 lib/jwt/rsa_token.rb create mode 100644 lib/jwt/token.rb diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 7e70c70c89..2a92627cb1 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -2,6 +2,10 @@ class JwtController < ApplicationController skip_before_action :authenticate_user! skip_before_action :verify_authenticity_token + SERVICES = { + 'docker' => Jwt::DockerAuthenticationService, + } + def auth @authenticated = authenticate_with_http_basic do |login, password| @ci_project = ci_project(login, password) @@ -9,46 +13,22 @@ class JwtController < ApplicationController end unless @authenticated - return render_403 if has_basic_credentials? + head :forbidden if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) end - case params[:service] - when 'docker' - docker_token_auth(params[:scope], params[:offline_token]) - else - return render_404 - end + service = SERVICES[params[:service]] + head :not_found unless service + + result = service.new(@ci_project, @user, auth_params).execute + return head result[:http_status] if result[:http_status] + + render json: result end private - def render_400 - head :invalid_request - end - - def render_404 - head :not_found - end - - def render_403 - head :forbidden - end - - def docker_token_auth(scope, offline_token) - payload = { - aud: params[:service], - sub: @user.try(:username) - } - - if offline_token - return render_403 unless @user - elsif scope - access = process_access(scope) - return render_404 unless access - payload[:access] = [access] - end - - render json: { token: encode(payload) } + def auth_params + params.permit(:service, :scope, :offline_token, :account, :client_id) end def ci_project(login, password) @@ -102,72 +82,4 @@ class JwtController < ApplicationController user end - - def process_access(scope) - type, name, actions = scope.split(':', 3) - actions = actions.split(',') - - case type - when 'repository' - process_repository_access(type, name, actions) - end - end - - def process_repository_access(type, name, actions) - project = Project.find_with_namespace(name) - return unless project - - actions = actions.select do |action| - can_access?(project, action) - end - - { type: 'repository', name: name, actions: actions } if actions - end - - def default_payload - { - aud: 'docker', - sub: @user.try(:username), - aud: params[:service], - } - end - - def private_key - @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key - end - - def encode(payload) - issued_at = Time.now - payload = payload.merge( - iss: Gitlab.config.registry.issuer, - iat: issued_at.to_i, - nbf: issued_at.to_i - 5.seconds.to_i, - exp: issued_at.to_i + 60.minutes.to_i, - jti: SecureRandom.uuid, - ) - headers = { - kid: kid(private_key) - } - JWT.encode(payload, private_key, 'RS256', headers) - end - - def can_access?(project, action) - case action - when 'pull' - project == @ci_project || can?(@user, :download_code, project) - when 'push' - project == @ci_project || can?(@user, :push_code, project) - else - false - end - end - - def kid(private_key) - sha256 = Digest::SHA256.new - sha256.update(private_key.public_key.to_der) - payload = StringIO.new(sha256.digest).read(30) - Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| - mem << slice.join - end.join(':') - end end diff --git a/app/services/jwt/docker_authentication_service.rb b/app/services/jwt/docker_authentication_service.rb new file mode 100644 index 0000000000..ce28085e5d --- /dev/null +++ b/app/services/jwt/docker_authentication_service.rb @@ -0,0 +1,65 @@ +module Jwt + class DockerAuthenticationService < BaseService + def execute + if params[:offline_token] + return error('forbidden', 403) unless current_user + end + + { token: token.encoded } + end + + private + + def token + token = ::Jwt::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token[:access] = access + token + end + + def access + return unless params[:scope] + + scope = process_scope(params[:scope]) + [scope].compact + end + + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + current_project = Project.find_with_namespace(name) + return unless current_project + + actions = actions.select do |action| + can_access?(current_project, action) + end + + { type: type, name: name, actions: actions } if actions + end + + def can_access?(current_project, action) + case action + when 'pull' + current_project == project || can?(current_user, :download_code, current_project) + when 'push' + current_project == project || can?(current_user, :push_code, current_project) + else + false + end + end + + def registry + Gitlab.config.registry + end + end +end diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb new file mode 100644 index 0000000000..cc265e3b31 --- /dev/null +++ b/lib/jwt/rsa_token.rb @@ -0,0 +1,36 @@ +module Jwt + class RSAToken < Token + attr_reader :key_file + + def initialize(key_file) + super() + @key_file = key_file + end + + def encoded + headers = { + kid: kid + } + JWT.encode(payload, key, 'RS256', headers) + end + + private + + def key_data + @key_data ||= File.read(key_file) + end + + def key + @key ||= OpenSSL::PKey::RSA.new(key_data) + end + + def kid + sha256 = Digest::SHA256.new + sha256.update(key.public_key.to_der) + payload = StringIO.new(sha256.digest).read(30) + Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| + mem << slice.join + end.join(':') + end + end +end diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb new file mode 100644 index 0000000000..38cbc8004e --- /dev/null +++ b/lib/jwt/token.rb @@ -0,0 +1,48 @@ +module Jwt + class Token + attr_accessor :issuer, :subject, :audience, :id + attr_accessor :issued_at, :not_before, :expire_time + + def initialize + @payload = {} + @id = SecureRandom.uuid + @issued_at = Time.now + @not_before = issued_at - 5.seconds + @expire_time = issued_at + 1.minute + end + + def [](key) + @payload[key] + end + + def []=(key, value) + @payload[key] = value + end + + def encoded + raise NotImplementedError + end + + def payload + @payload.merge(default_payload) + end + + def to_json + payload.to_json + end + + private + + def default_payload + { + jti: id, + aud: audience, + sub: subject, + iss: issuer, + iat: issued_at.to_i, + nbf: not_before.to_i, + exp: expire_time.to_i + }.compact + end + end +end \ No newline at end of file From 8b813277b0cbf57b2a07ad2e1b4cb87dadfb66c5 Mon Sep 17 00:00:00 2001 From: Yatish Mehta Date: Wed, 4 May 2016 14:04:13 -0700 Subject: [PATCH 014/199] Fixed typo in zen.scss and corresponding views --- app/assets/stylesheets/framework/zen.scss | 2 +- app/views/projects/_md_preview.html.haml | 2 +- app/views/projects/_zen.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index f870ea0d87..ff02ebdd34 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -32,7 +32,7 @@ } } -.zen-cotrol { +.zen-control { padding: 0; color: #555; background: none; diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 8de44a6c91..81afea2c60 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -8,7 +8,7 @@ %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview %li.pull-right - %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } + %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } Go full screen .md-write-holder diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index e1e3501396..413477a2d3 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -4,5 +4,5 @@ = f.text_area attr, class: classes, placeholder: placeholder - else = text_area_tag attr, nil, class: classes, placeholder: placeholder - %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } + %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') From 8dc19494c3fdae366daa8849b5e2a3f58f98878c Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Thu, 5 May 2016 13:26:36 +0700 Subject: [PATCH 015/199] Remove unused code, update spec, and update changelog --- CHANGELOG | 1 + app/controllers/snippets_controller.rb | 20 +------------------- spec/routing/routing_spec.rb | 8 -------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 558897ad89..6c044192d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.0 (unreleased) + - Snippets tab under user profile. !4001 (Long Nguyen) - Remove future dates from contribution calendar graph. - Fix error when visiting commit builds page before build was updated - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2daceed039..f0bd842ca5 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -10,29 +10,11 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :user_index, :show, :raw] + skip_before_action :authenticate_user!, only: [:show, :raw] layout 'snippets' respond_to :html - def index - if params[:username].present? - @user = User.find_by(username: params[:username]) - - render_404 and return unless @user - - @snippets = SnippetsFinder.new.execute(current_user, { - filter: :by_user, - user: @user, - scope: params[:scope] }). - page(params[:page]) - - render 'index' - else - redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path) - end - end - def new @snippet = PersonalSnippet.new end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1527eddfa4..9deffd0a1e 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -27,18 +27,10 @@ end # PUT /snippets/:id(.:format) snippets#update # DELETE /snippets/:id(.:format) snippets#destroy describe SnippetsController, "routing" do - it "to #user_index" do - expect(get("/s/User")).to route_to('snippets#index', username: 'User') - end - it "to #raw" do expect(get("/snippets/1/raw")).to route_to('snippets#raw', id: '1') end - it "to #index" do - expect(get("/snippets")).to route_to('snippets#index') - end - it "to #create" do expect(post("/snippets")).to route_to('snippets#create') end From ae29ec31e4f71d722e975bfce945aaed7e0d0bd1 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Thu, 5 May 2016 14:57:34 +0700 Subject: [PATCH 016/199] Remove unused view and update redirect when destroy snippet --- app/controllers/snippets_controller.rb | 2 +- app/views/snippets/index.html.haml | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 app/views/snippets/index.html.haml diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index f0bd842ca5..2c038bdfda 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -43,7 +43,7 @@ class SnippetsController < ApplicationController @snippet.destroy - redirect_to snippets_path + redirect_to dashboard_snippets_path end def raw diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml deleted file mode 100644 index 7e4918a608..0000000000 --- a/app/views/snippets/index.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- page_title "By #{@user.name}", "Snippets" - -%ol.breadcrumb - %li - = link_to snippets_path do - Snippets - %li - = @user.name - .pull-right.hidden-xs - = link_to user_path(@user) do - #{@user.name} profile page - -= render 'snippets' From ffda8a1a0eb273e62fcb0197f352400946571778 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Sun, 8 May 2016 15:27:33 +0700 Subject: [PATCH 017/199] user routings refactor --- app/controllers/snippets_controller.rb | 22 ++++++++++- .../search/results/_snippet_blob.html.haml | 2 +- .../search/results/_snippet_title.html.haml | 2 +- app/views/shared/snippets/_snippet.html.haml | 2 +- app/views/snippets/index.html.haml | 13 +++++++ app/views/users/calendar.html.haml | 2 +- app/views/users/show.html.haml | 12 +++--- config/routes.rb | 37 +++++++++---------- spec/routing/routing_spec.rb | 4 ++ 9 files changed, 64 insertions(+), 32 deletions(-) create mode 100644 app/views/snippets/index.html.haml diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2c038bdfda..2a17c1f34d 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -10,11 +10,29 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:show, :raw] + skip_before_action :authenticate_user!, only: [:index, :show, :raw] layout 'snippets' respond_to :html + def index + if params[:username].present? + @user = User.find_by(username: params[:username]) + + render_404 and return unless @user + + @snippets = SnippetsFinder.new.execute(current_user, { + filter: :by_user, + user: @user, + scope: params[:scope] }). + page(params[:page]) + + render 'index' + else + redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path) + end + end + def new @snippet = PersonalSnippet.new end @@ -43,7 +61,7 @@ class SnippetsController < ApplicationController @snippet.destroy - redirect_to dashboard_snippets_path + redirect_to snippets_path end def raw diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index c9b7bd154a..6b7e06f2b2 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -6,7 +6,7 @@ %span = snippet.title by - = link_to user_snippets_path(snippet.author) do + = link_to snippets_user_path(snippet.author) do = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' = snippet.author_name %span.light #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index c414acb6a1..8c884926db 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -17,7 +17,7 @@ = "##{snippet_title.id}" %span by - = link_to user_snippets_path(snippet_title.author) do + = link_to snippets_user_path(snippet_title.author) do = image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: '' = snippet_title.author_name %span.light #{time_ago_with_tooltip(snippet_title.created_at)} diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index c96dfefe17..e6a7a7777b 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -16,6 +16,6 @@ = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) .snippet-info - = link_to user_snippets_path(snippet.author) do + = link_to snippets_user_path(snippet.author) do = snippet.author_name authored #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml new file mode 100644 index 0000000000..8aa3d490fa --- /dev/null +++ b/app/views/snippets/index.html.haml @@ -0,0 +1,13 @@ + - page_title "By #{@user.name}", "Snippets" + + %ol.breadcrumb + %li + = link_to snippets_path do + Snippets + %li + = @user.name + .pull-right.hidden-xs + = link_to user_path(@user) do + #{@user.name} profile page + + = render 'snippets' diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 1de71f37d1..6ff4eb79b0 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -4,7 +4,7 @@ #{@timestamps.to_json}, #{@starting_year}, #{@starting_month}, - '#{user_calendar_activities_path}' + '#{calendar_activities_user_path}' ); .calendar-hint Summary of issues, merge requests, and push events diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 2ccdf843a6..e894a8e939 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -70,19 +70,19 @@ %ul.nav-links.center.user-profile-nav %li.js-activity-tab - = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do + = link_to calendar_activities_user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do Activity %li.js-groups-tab - = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do + = link_to groups_user_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do Groups %li.js-contributed-tab - = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do + = link_to contributed_projects_user_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do Contributed projects %li.projects-tab - = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do + = link_to projects_user_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects %li.snippets-tab - = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + = link_to snippets_user_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do Snippets %div{ class: container_class } @@ -90,7 +90,7 @@ #activity.tab-pane .gray-content-block.calender-block.white.second-block.hidden-xs %div{ class: container_class } - .user-calendar{data: {href: user_calendar_path}} + .user-calendar{data: {href: calendar_user_path}} %h4.center.light %i.fa.fa-spinner.fa-spin .user-calendar-activities diff --git a/config/routes.rb b/config/routes.rb index 2bfaa2c2af..533c3b98e8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,6 +91,9 @@ Rails.application.routes.draw do end end + get '/s/:username', to: redirect('/u/:username/snippets'), + constraints: { username: /[a-zA-Z.0-9_\-]+(? 'users#calendar', as: :user_calendar, - constraints: { username: /.*/ } - - get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities, - constraints: { username: /.*/ } - - get 'u/:username/groups' => 'users#groups', as: :user_groups, - constraints: { username: /.*/ } - - get 'u/:username/projects' => 'users#projects', as: :user_projects, - constraints: { username: /.*/ } - - get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects, - constraints: { username: /.*/ } - - get 'u/:username/snippets' => 'users#snippets', as: :user_snippets, - constraints: { username: /.*/ } - - get '/u/:username' => 'users#show', as: :user, - constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Sun, 8 May 2016 15:29:51 +0700 Subject: [PATCH 018/199] Fix routing error --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 533c3b98e8..299b0dd1ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,7 +91,7 @@ Rails.application.routes.draw do end end - get '/s/:username', to: redirect('/u/:username/snippets'), + get '/s/:username', to: redirect('/u/%{username}/snippets'), constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Sun, 8 May 2016 20:48:07 +0700 Subject: [PATCH 019/199] Routing refactoring --- .../search/results/_snippet_blob.html.haml | 2 +- .../search/results/_snippet_title.html.haml | 2 +- app/views/shared/snippets/_snippet.html.haml | 2 +- app/views/users/calendar.html.haml | 2 +- app/views/users/show.html.haml | 12 +++++----- config/routes.rb | 23 ++++++++----------- 6 files changed, 20 insertions(+), 23 deletions(-) diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 6b7e06f2b2..c9b7bd154a 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -6,7 +6,7 @@ %span = snippet.title by - = link_to snippets_user_path(snippet.author) do + = link_to user_snippets_path(snippet.author) do = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' = snippet.author_name %span.light #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 8c884926db..c414acb6a1 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -17,7 +17,7 @@ = "##{snippet_title.id}" %span by - = link_to snippets_user_path(snippet_title.author) do + = link_to user_snippets_path(snippet_title.author) do = image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: '' = snippet_title.author_name %span.light #{time_ago_with_tooltip(snippet_title.created_at)} diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index e6a7a7777b..c96dfefe17 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -16,6 +16,6 @@ = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) .snippet-info - = link_to snippets_user_path(snippet.author) do + = link_to user_snippets_path(snippet.author) do = snippet.author_name authored #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 6ff4eb79b0..1de71f37d1 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -4,7 +4,7 @@ #{@timestamps.to_json}, #{@starting_year}, #{@starting_month}, - '#{calendar_activities_user_path}' + '#{user_calendar_activities_path}' ); .calendar-hint Summary of issues, merge requests, and push events diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index d5f482f716..9017fd54fc 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -70,19 +70,19 @@ %ul.nav-links.center.user-profile-nav %li.js-activity-tab - = link_to calendar_activities_user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do + = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do Activity %li.js-groups-tab - = link_to groups_user_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do + = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do Groups %li.js-contributed-tab - = link_to contributed_projects_user_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do + = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do Contributed projects %li.projects-tab - = link_to projects_user_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do + = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects %li.snippets-tab - = link_to snippets_user_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do Snippets %div{ class: container_class } @@ -90,7 +90,7 @@ #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs %div{ class: container_class } - .user-calendar{data: {href: calendar_user_path}} + .user-calendar{data: {href: user_calendar_path}} %h4.center.light %i.fa.fa-spinner.fa-spin .user-calendar-activities diff --git a/config/routes.rb b/config/routes.rb index 299b0dd1ac..594bab204f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -343,19 +343,16 @@ Rails.application.routes.draw do end end - resources(:users, - path: 'u', - param: :username, - constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Sun, 8 May 2016 22:06:19 +0700 Subject: [PATCH 020/199] Add specs for user routing and update spec for user controller --- spec/controllers/users_controller_spec.rb | 22 ++++++++++++++ spec/routing/routing_spec.rb | 37 +++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 8045c8b940..c61ec17466 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -112,4 +112,26 @@ describe UsersController do expect(response).to render_template('calendar_activities') end end + + describe 'GET #snippets' do + before do + sign_in(user) + end + + context 'format html' do + it 'renders snippets page' do + get :snippets, username: user.username + expect(response.status).to eq(200) + expect(response).to render_template('show') + end + end + + context 'format json' do + it 'response with snippets json data' do + get :snippets, username: user.username, format: :json + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to have_key('html') + end + end + end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 543088fa08..8530a2f31d 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -1,5 +1,42 @@ require 'spec_helper' +# user GET /u/:username/ +# user_groups GET /u/:username/groups(.:format) +# user_projects GET /u/:username/projects(.:format) +# user_contributed_projects GET /u/:username/contributed(.:format) +# user_snippets GET /u/:username/snippets(.:format) +# user_calendar GET /u/:username/calendar(.:format) +# user_calendar_activities GET /u/:username/calendar_activities(.:format) +describe UsersController, "routing" do + it "to #show" do + expect(get("/u/User")).to route_to('users#show', username: 'User') + end + + it "to #groups" do + expect(get("/u/User/groups")).to route_to('users#groups', username: 'User') + end + + it "to #projects" do + expect(get("/u/User/projects")).to route_to('users#projects', username: 'User') + end + + it "to #contributed" do + expect(get("/u/User/contributed")).to route_to('users#contributed', username: 'User') + end + + it "to #snippets" do + expect(get("/u/User/snippets")).to route_to('users#snippets', username: 'User') + end + + it "to #calendar" do + expect(get("/u/User/calendar")).to route_to('users#calendar', username: 'User') + end + + it "to #calendar_activities" do + expect(get("/u/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') + end +end + # search GET /search(.:format) search#show describe SearchController, "routing" do it "to #show" do From 9cc0937b3a41caca89fa6722149248a8f7b0a447 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sun, 8 May 2016 15:33:34 -0600 Subject: [PATCH 021/199] Enable the Rubocop DeprecatedClassMethods cop This reports uses of `File.exists?` and `Dir.exists?`, which were both deprecated in Ruby and will eventually be removed in favor of `.exist?`. Also fixes all existing uses of the deprecated methods. --- .rubocop.yml | 2 +- app/models/ci/build.rb | 2 +- app/models/merge_request.rb | 2 +- config/boot.rb | 2 +- config/initializers/carrierwave.rb | 2 +- lib/gitlab/backend/shell.rb | 2 +- lib/gitlab/redis.rb | 2 +- spec/config/mail_room_spec.rb | 2 +- spec/services/projects/create_service_spec.rb | 4 ++-- spec/services/projects/destroy_service_spec.rb | 8 ++++---- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 9f179efa3c..b49b60d676 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -770,7 +770,7 @@ Lint/DefEndAlignment: # Check for deprecated class method calls. Lint/DeprecatedClassMethods: - Enabled: false + Enabled: true # Check for duplicate method definitions. Lint/DuplicateMethods: diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4bc3a225e2..073ec9dd89 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -238,7 +238,7 @@ module Ci end def recreate_trace_dir - unless Dir.exists?(dir_to_trace) + unless Dir.exist?(dir_to_trace) FileUtils.mkdir_p(dir_to_trace) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4175e1e5fb..365ccc90b4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -544,7 +544,7 @@ class MergeRequest < ActiveRecord::Base end def ref_is_fetched? - File.exists?(File.join(project.repository.path_to_repo, ref_path)) + File.exist?(File.join(project.repository.path_to_repo, ref_path)) end def ensure_ref_fetched diff --git a/config/boot.rb b/config/boot.rb index 4489e58688..f2830ae316 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -3,4 +3,4 @@ require 'rubygems' # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index df28d30d75..0d278717d6 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -2,7 +2,7 @@ CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/ aws_file = Rails.root.join('config', 'aws.yml') -if File.exists?(aws_file) +if File.exist?(aws_file) AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env] CarrierWave.configure do |config| diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 132f9cd196..3e3986d638 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -180,7 +180,7 @@ module Gitlab # exists?('gitlab/cookies.git') # def exists?(dir_name) - File.exists?(full_path(dir_name)) + File.exist?(full_path(dir_name)) end protected diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 5c352c96de..f7db7a6391 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -42,7 +42,7 @@ module Gitlab config_file = File.expand_path('../../../config/resque.yml', __FILE__) @url = "redis://localhost:6379" - if File.exists?(config_file) + if File.exist?(config_file) @url =YAML.load_file(config_file)[rails_env] end end diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 462afb24f0..6fad7e2b9e 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -43,7 +43,7 @@ describe "mail_room.yml" do redis_config_file = Rails.root.join('config', 'resque.yml') redis_url = - if File.exists?(redis_config_file) + if File.exist?(redis_config_file) YAML.load_file(redis_config_file)[Rails.env] else "redis://localhost:6379" diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index e43903dbd3..fd11435946 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -64,7 +64,7 @@ describe Projects::CreateService, services: true do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { expect(File.exists?(@path)).to be_truthy } + it { expect(File.exist?(@path)).to be_truthy } end context 'wiki_enabled false does not create wiki repository directory' do @@ -74,7 +74,7 @@ describe Projects::CreateService, services: true do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { expect(File.exists?(@path)).to be_falsey } + it { expect(File.exist?(@path)).to be_falsey } end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 1ec2707771..a5cb6f382e 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -13,8 +13,8 @@ describe Projects::DestroyService, services: true do end it { expect(Project.all).not_to include(project) } - it { expect(Dir.exists?(path)).to be_falsey } - it { expect(Dir.exists?(remove_path)).to be_falsey } + it { expect(Dir.exist?(path)).to be_falsey } + it { expect(Dir.exist?(remove_path)).to be_falsey } end context 'Sidekiq fake' do @@ -24,8 +24,8 @@ describe Projects::DestroyService, services: true do end it { expect(Project.all).not_to include(project) } - it { expect(Dir.exists?(path)).to be_falsey } - it { expect(Dir.exists?(remove_path)).to be_truthy } + it { expect(Dir.exist?(path)).to be_falsey } + it { expect(Dir.exist?(remove_path)).to be_truthy } end def destroy_project(project, user, params) From baef6728fa4e8e515ccdeba1ea54da996f322aab Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 19:59:45 +0300 Subject: [PATCH 022/199] Send trace to a browser incrementally when build is running We send a state of ansi2html to client, client needs to send this state back. The state describes the configuration of generator and position within trace. --- app/controllers/projects/builds_controller.rb | 15 +++ app/models/ci/build.rb | 8 +- app/views/projects/builds/show.html.haml | 7 +- config/routes.rb | 1 + lib/ci/ansi2html.rb | 78 ++++++++---- spec/lib/ci/ansi2html_spec.rb | 111 ++++++++++++------ 6 files changed, 162 insertions(+), 58 deletions(-) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index b8b9e78427..3c9a52a5dd 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -38,6 +38,14 @@ class Projects::BuildsController < Projects::ApplicationController end end + def trace + respond_to do |format| + format.json do + render json: @build.trace_with_state(params_state).merge!(id: @build.id, status: @build.status) + end + end + end + def retry unless @build.retryable? return render_404 @@ -72,6 +80,13 @@ class Projects::BuildsController < Projects::ApplicationController private + def params_state + begin + JSON.parse(params[:state], symbolize_names: true) + rescue + end + end + def build @build ||= project.builds.unscoped.find_by!(id: params[:id]) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4bc3a225e2..1eb6a0d902 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -132,8 +132,12 @@ module Ci end def trace_html - html = Ci::Ansi2html::convert(trace) if trace.present? - html || '' + trace_with_state[:html] + end + + def trace_with_state(state = nil) + trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present? + trace_with_state || {} end def timeout diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c0f7a7686f..0da0477bdd 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,5 +1,6 @@ - page_title "#{@build.name} (##{@build.id})", "Builds" = render "header_title" +- trace = build.trace_for_state .build-page .row-content-block.top-block @@ -85,7 +86,9 @@ %pre.trace#build-trace %code.bash = preserve do - = raw @build.trace_html + = raw trace[:html] + - if @build.active? + %i{:class => "fa fa-refresh fa-spin"} %div#down-build-trace @@ -216,4 +219,4 @@ :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}") + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace[:state]}") diff --git a/config/routes.rb b/config/routes.rb index dafecc9464..e5c7d656da 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -672,6 +672,7 @@ Rails.application.routes.draw do post :cancel post :retry post :erase + get :trace get :raw end diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index ac6d667cf8..d29e68570f 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -23,8 +23,8 @@ module Ci cross: 0x10, } - def self.convert(ansi) - Converter.new().convert(ansi) + def self.convert(ansi, state = nil) + Converter.new.convert(ansi, state) end class Converter @@ -84,22 +84,36 @@ module Ci def on_107(s) set_bg_color(7, 'l') end def on_109(s) set_bg_color(9, 'l') end - def convert(ansi) - @out = "" - @n_open_tags = 0 - reset() + attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask - s = StringScanner.new(ansi.gsub("<", "<")) + STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask] + + def convert(raw, new_state) + reset_state + restore_state(new_state) if new_state && new_state[:offset].to_i < raw.length + + start = @offset + ansi = raw[@offset..-1] + + open_new_tag + + s = StringScanner.new(ansi) while(!s.eos?) if s.scan(/\e([@-_])(.*?)([@-~])/) handle_sequence(s) + elsif s.scan(/\e(([@-_])(.*?)?)?$/) + break + elsif s.scan(/ 0 } end def handle_sequence(s) @@ -121,6 +135,20 @@ module Ci evaluate_command_stack(commands) + open_new_tag + end + + def evaluate_command_stack(stack) + return unless command = stack.shift() + + if self.respond_to?("on_#{command}", true) + self.send("on_#{command}", stack) + end + + evaluate_command_stack(stack) + end + + def open_new_tag css_classes = [] unless @fg_color.nil? @@ -138,20 +166,8 @@ module Ci css_classes << "term-#{css_class}" if @style_mask & flag != 0 end - open_new_tag(css_classes) if css_classes.length > 0 - end + return if css_classes.empty? - def evaluate_command_stack(stack) - return unless command = stack.shift() - - if self.respond_to?("on_#{command}", true) - self.send("on_#{command}", stack) - end - - evaluate_command_stack(stack) - end - - def open_new_tag(css_classes) @out << %{} @n_open_tags += 1 end @@ -163,6 +179,26 @@ module Ci end end + def reset_state + @offset = 0 + @n_open_tags = 0 + @out = '' + reset + end + + def state + STATE_PARAMS.inject({}) do |h, param| + h[param] = send(param) + h + end + end + + def restore_state(new_state) + STATE_PARAMS.each do |param| + send("#{param}=".to_sym, new_state[param]) + end + end + def reset @fg_color = nil @bg_color = nil diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb index 3a2b568f4c..04afbd0692 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/ci/ansi2html_spec.rb @@ -4,131 +4,176 @@ describe Ci::Ansi2html, lib: true do subject { Ci::Ansi2html } it "prints non-ansi as-is" do - expect(subject.convert("Hello")).to eq('Hello') + expect(subject.convert("Hello")[:html]).to eq('Hello') end it "strips non-color-changing controll sequences" do - expect(subject.convert("Hello \e[2Kworld")).to eq('Hello world') + expect(subject.convert("Hello \e[2Kworld")[:html]).to eq('Hello world') end it "prints simply red" do - expect(subject.convert("\e[31mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[31mHello\e[0m")[:html]).to eq('Hello') end it "prints simply red without trailing reset" do - expect(subject.convert("\e[31mHello")).to eq('Hello') + expect(subject.convert("\e[31mHello")[:html]).to eq('Hello') end it "prints simply yellow" do - expect(subject.convert("\e[33mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[33mHello\e[0m")[:html]).to eq('Hello') end it "prints default on blue" do - expect(subject.convert("\e[39;44mHello")).to eq('Hello') + expect(subject.convert("\e[39;44mHello")[:html]).to eq('Hello') end it "prints red on blue" do - expect(subject.convert("\e[31;44mHello")).to eq('Hello') + expect(subject.convert("\e[31;44mHello")[:html]).to eq('Hello') end it "resets colors after red on blue" do - expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello\e[0m world")[:html]).to eq('Hello world') end it "performs color change from red/blue to yellow/blue" do - expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello \e[33mworld")[:html]).to eq('Hello world') end it "performs color change from red/blue to yellow/green" do - expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello \e[33;42mworld")[:html]).to eq('Hello world') end it "performs color change from red/blue to reset to yellow/green" do - expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")[:html]).to eq('Hello world') end it "ignores unsupported codes" do - expect(subject.convert("\e[51mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[51mHello\e[0m")[:html]).to eq('Hello') end it "prints light red" do - expect(subject.convert("\e[91mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[91mHello\e[0m")[:html]).to eq('Hello') end it "prints default on light red" do - expect(subject.convert("\e[101mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[101mHello\e[0m")[:html]).to eq('Hello') end it "performs color change from red/blue to default/blue" do - expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello \e[39mworld")[:html]).to eq('Hello world') end it "performs color change from light red/blue to default/blue" do - expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('Hello world') + expect(subject.convert("\e[91;44mHello \e[39mworld")[:html]).to eq('Hello world') end it "prints bold text" do - expect(subject.convert("\e[1mHello")).to eq('Hello') + expect(subject.convert("\e[1mHello")[:html]).to eq('Hello') end it "resets bold text" do - expect(subject.convert("\e[1mHello\e[21m world")).to eq('Hello world') - expect(subject.convert("\e[1mHello\e[22m world")).to eq('Hello world') + expect(subject.convert("\e[1mHello\e[21m world")[:html]).to eq('Hello world') + expect(subject.convert("\e[1mHello\e[22m world")[:html]).to eq('Hello world') end it "prints italic text" do - expect(subject.convert("\e[3mHello")).to eq('Hello') + expect(subject.convert("\e[3mHello")[:html]).to eq('Hello') end it "resets italic text" do - expect(subject.convert("\e[3mHello\e[23m world")).to eq('Hello world') + expect(subject.convert("\e[3mHello\e[23m world")[:html]).to eq('Hello world') end it "prints underlined text" do - expect(subject.convert("\e[4mHello")).to eq('Hello') + expect(subject.convert("\e[4mHello")[:html]).to eq('Hello') end it "resets underlined text" do - expect(subject.convert("\e[4mHello\e[24m world")).to eq('Hello world') + expect(subject.convert("\e[4mHello\e[24m world")[:html]).to eq('Hello world') end it "prints concealed text" do - expect(subject.convert("\e[8mHello")).to eq('Hello') + expect(subject.convert("\e[8mHello")[:html]).to eq('Hello') end it "resets concealed text" do - expect(subject.convert("\e[8mHello\e[28m world")).to eq('Hello world') + expect(subject.convert("\e[8mHello\e[28m world")[:html]).to eq('Hello world') end it "prints crossed-out text" do - expect(subject.convert("\e[9mHello")).to eq('Hello') + expect(subject.convert("\e[9mHello")[:html]).to eq('Hello') end it "resets crossed-out text" do - expect(subject.convert("\e[9mHello\e[29m world")).to eq('Hello world') + expect(subject.convert("\e[9mHello\e[29m world")[:html]).to eq('Hello world') end it "can print 256 xterm fg colors" do - expect(subject.convert("\e[38;5;16mHello")).to eq('Hello') + expect(subject.convert("\e[38;5;16mHello")[:html]).to eq('Hello') end it "can print 256 xterm fg colors on normal magenta background" do - expect(subject.convert("\e[38;5;16;45mHello")).to eq('Hello') + expect(subject.convert("\e[38;5;16;45mHello")[:html]).to eq('Hello') end it "can print 256 xterm bg colors" do - expect(subject.convert("\e[48;5;240mHello")).to eq('Hello') + expect(subject.convert("\e[48;5;240mHello")[:html]).to eq('Hello') end it "can print 256 xterm bg colors on normal magenta foreground" do - expect(subject.convert("\e[48;5;16;35mHello")).to eq('Hello') + expect(subject.convert("\e[48;5;16;35mHello")[:html]).to eq('Hello') end it "prints bold colored text vividly" do - expect(subject.convert("\e[1;31mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[1;31mHello\e[0m")[:html]).to eq('Hello') end it "prints bold light colored text correctly" do - expect(subject.convert("\e[1;91mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[1;91mHello\e[0m")[:html]).to eq('Hello') + end + + it "prints <" do + expect(subject.convert("<")[:html]).to eq('<') + end + + describe "incremental update" do + shared_examples 'stateable converter' do + let(:pass1) { subject.convert(pre_text) } + let(:pass2) { subject.convert(pre_text + text, pass1[:state]) } + + it "to returns html to append" do + expect(pass2[:append]).to be_truthy + expect(pass2[:html]).to eq(html) + expect(pass1[:text] + pass2[:text]).to eq(pre_text + text) + expect(pass1[:html] + pass2[:html]).to eq(pre_html + html) + end + end + + context "with split word" do + let(:pre_text) { "\e[1mHello" } + let(:pre_html) { "Hello" } + let(:text) { "\e[1mWorld" } + let(:html) { "World" } + + it_behaves_like 'stateable converter' + end + + context "with split sequence" do + let(:pre_text) { "\e[1m" } + let(:pre_html) { "" } + let(:text) { "Hello" } + let(:html) { "Hello" } + + it_behaves_like 'stateable converter' + end + + context "with partial sequence" do + let(:pre_text) { "Hello\e" } + let(:pre_html) { "Hello" } + let(:text) { "[1m World" } + let(:html) { " World" } + + it_behaves_like 'stateable converter' + end end end From 74520f23db51c95b4aea8856fb51c4246785f776 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:19:27 +0300 Subject: [PATCH 023/199] Encode state as base64 string --- app/assets/javascripts/ci/build.coffee | 21 ++++++++++++------- app/controllers/projects/builds_controller.rb | 9 +------- app/views/projects/builds/show.html.haml | 6 +++--- lib/ci/ansi2html.rb | 13 ++++++++---- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 7afe8bf79e..fca0c3bae5 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -1,9 +1,12 @@ class CiBuild @interval: null + @state: null - constructor: (build_url, build_status) -> + constructor: (build_url, build_status, build_state) -> clearInterval(CiBuild.interval) + @state = build_state + @initScrollButtonAffix() if build_status == "running" || build_status == "pending" @@ -26,14 +29,18 @@ class CiBuild CiBuild.interval = setInterval => if window.location.href.split("#").first() is build_url $.ajax - url: build_url + url: build_url + "/trace.json?state=" + encodeURIComponent(@state) dataType: "json" - success: (build) => - if build.status == "running" - $('#build-trace code').html build.trace_html - $('#build-trace code').append '' + success: (log) => + @state = log.state + if log.status is "running" + if log.append + $('.fa-refresh').before log.html + else + $('#build-trace code').html log.html + $('#build-trace code').append '' @checkAutoscroll() - else if build.status != build_status + else if log.status isnt build_status Turbolinks.visit build_url , 4000 diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 3c9a52a5dd..bb1f6c5e98 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -41,7 +41,7 @@ class Projects::BuildsController < Projects::ApplicationController def trace respond_to do |format| format.json do - render json: @build.trace_with_state(params_state).merge!(id: @build.id, status: @build.status) + render json: @build.trace_with_state(params[:state]).merge!(id: @build.id, status: @build.status) end end end @@ -80,13 +80,6 @@ class Projects::BuildsController < Projects::ApplicationController private - def params_state - begin - JSON.parse(params[:state], symbolize_names: true) - rescue - end - end - def build @build ||= project.builds.unscoped.find_by!(id: params[:id]) end diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 0da0477bdd..c7b9c36a3a 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,6 +1,6 @@ - page_title "#{@build.name} (##{@build.id})", "Builds" = render "header_title" -- trace = build.trace_for_state +- trace_with_state = @build.trace_with_state .build-page .row-content-block.top-block @@ -86,7 +86,7 @@ %pre.trace#build-trace %code.bash = preserve do - = raw trace[:html] + = raw trace_with_state[:html] - if @build.active? %i{:class => "fa fa-refresh fa-spin"} @@ -219,4 +219,4 @@ :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace[:state]}") + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace_with_state[:state]}") diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index d29e68570f..5fed43aaeb 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -90,7 +90,7 @@ module Ci def convert(raw, new_state) reset_state - restore_state(new_state) if new_state && new_state[:offset].to_i < raw.length + restore_state(raw, new_state) if new_state start = @offset ansi = raw[@offset..-1] @@ -187,15 +187,20 @@ module Ci end def state - STATE_PARAMS.inject({}) do |h, param| + state = STATE_PARAMS.inject({}) do |h, param| h[param] = send(param) h end + Base64.urlsafe_encode64(state.to_json) end - def restore_state(new_state) + def restore_state(raw, new_state) + state = Base64.urlsafe_decode64(new_state) + state = JSON.parse(state, symbolize_names: true) + return if state[:offset].to_i > raw.length + STATE_PARAMS.each do |param| - send("#{param}=".to_sym, new_state[param]) + send("#{param}=".to_sym, state[param]) end end From 51a8811e262c48e2d3aaa426b3c87693dda87b37 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:26:14 +0300 Subject: [PATCH 024/199] Revert "Eager load `lib/api`" This reverts commit 0a280158efeb7f681589ae7af24f0ed9052de809. --- config/application.rb | 2 - config/routes.rb | 1 + lib/api/api.rb | 4 +- lib/api/api_guard.rb | 306 +++++++++++++++++++++--------------------- lib/ci/api/api.rb | 2 +- 5 files changed, 157 insertions(+), 158 deletions(-) diff --git a/config/application.rb b/config/application.rb index d4b86bb38b..b602e2b616 100644 --- a/config/application.rb +++ b/config/application.rb @@ -102,8 +102,6 @@ module Gitlab # This is needed for gitlab-shell ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH'] - config.eager_load_paths += ["#{Rails.root}/lib"] - config.generators do |g| g.factory_girl false end diff --git a/config/routes.rb b/config/routes.rb index bfc6818a8d..3c855cfafd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ require 'sidekiq/web' require 'sidekiq/cron/web' +require 'api/api' Rails.application.routes.draw do if Gitlab::Sherlock.enabled? diff --git a/lib/api/api.rb b/lib/api/api.rb index d41b4b7186..6ddfe11d98 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,6 +1,8 @@ +Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} + module API class API < Grape::API - include ::API::APIGuard + include APIGuard version 'v3', using: :path rescue_from ActiveRecord::RecordNotFound do diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 6dfd6e4396..b9994fcefd 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -2,173 +2,171 @@ require 'rack/oauth2' -module API - module APIGuard - extend ActiveSupport::Concern +module APIGuard + extend ActiveSupport::Concern - included do |base| - # OAuth2 Resource Server Authentication - use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| - # The authenticator only fetches the raw token string + included do |base| + # OAuth2 Resource Server Authentication + use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| + # The authenticator only fetches the raw token string - # Must yield access token to store it in the env - request.access_token - end - - helpers HelperMethods - - install_error_responders(base) + # Must yield access token to store it in the env + request.access_token end - # Helper Methods for Grape Endpoint - module HelperMethods - # Invokes the doorkeeper guard. - # - # If token is presented and valid, then it sets @current_user. - # - # If the token does not have sufficient scopes to cover the requred scopes, - # then it raises InsufficientScopeError. - # - # If the token is expired, then it raises ExpiredError. - # - # If the token is revoked, then it raises RevokedError. - # - # If the token is not found (nil), then it raises TokenNotFoundError. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def doorkeeper_guard!(scopes: []) - if (access_token = find_access_token).nil? - raise TokenNotFoundError + helpers HelperMethods - else - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end - end - end - - def doorkeeper_guard(scopes: []) - if access_token = find_access_token - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end - end - end - - def current_user - @current_user - end - - private - def find_access_token - @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) - end - - def doorkeeper_request - @doorkeeper_request ||= ActionDispatch::Request.new(env) - end - - def validate_access_token(access_token, scopes) - Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) - end - end - - module ClassMethods - # Installs the doorkeeper guard on the whole Grape API endpoint. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def guard_all!(scopes: []) - before do - guard! scopes: scopes - end - end - - private - def install_error_responders(base) - error_classes = [ MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] - - base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler - end - - def oauth2_bearer_token_error_handler - Proc.new do |e| - response = - case e - when MissingTokenError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new - - when TokenNotFoundError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Bad Access Token.") - - when ExpiredError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token is expired. You can either do re-authorization or token refresh.") - - when RevokedError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token was revoked. You have to re-authorize from the user.") - - when InsufficientScopeError - # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) - # does not include WWW-Authenticate header, which breaks the standard. - Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( - :insufficient_scope, - Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], - { scope: e.scopes }) - end - - response.finish - end - end - end + install_error_responders(base) + end + # Helper Methods for Grape Endpoint + module HelperMethods + # Invokes the doorkeeper guard. # - # Exceptions + # If token is presented and valid, then it sets @current_user. # + # If the token does not have sufficient scopes to cover the requred scopes, + # then it raises InsufficientScopeError. + # + # If the token is expired, then it raises ExpiredError. + # + # If the token is revoked, then it raises RevokedError. + # + # If the token is not found (nil), then it raises TokenNotFoundError. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def doorkeeper_guard!(scopes: []) + if (access_token = find_access_token).nil? + raise TokenNotFoundError - class MissingTokenError < StandardError; end + else + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end - class TokenNotFoundError < StandardError; end + def doorkeeper_guard(scopes: []) + if access_token = find_access_token + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) - class ExpiredError < StandardError; end + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError - class RevokedError < StandardError; end + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError - class InsufficientScopeError < StandardError - attr_reader :scopes - def initialize(scopes) - @scopes = scopes + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end + + def current_user + @current_user + end + + private + def find_access_token + @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + end + + def doorkeeper_request + @doorkeeper_request ||= ActionDispatch::Request.new(env) + end + + def validate_access_token(access_token, scopes) + Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) + end + end + + module ClassMethods + # Installs the doorkeeper guard on the whole Grape API endpoint. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def guard_all!(scopes: []) + before do + guard! scopes: scopes + end + end + + private + def install_error_responders(base) + error_classes = [ MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] + + base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + end + + def oauth2_bearer_token_error_handler + Proc.new do |e| + response = + case e + when MissingTokenError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new + + when TokenNotFoundError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Bad Access Token.") + + when ExpiredError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token is expired. You can either do re-authorization or token refresh.") + + when RevokedError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token was revoked. You have to re-authorize from the user.") + + when InsufficientScopeError + # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) + # does not include WWW-Authenticate header, which breaks the standard. + Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( + :insufficient_scope, + Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], + { scope: e.scopes }) + end + + response.finish end end end -end \ No newline at end of file + + # + # Exceptions + # + + class MissingTokenError < StandardError; end + + class TokenNotFoundError < StandardError; end + + class ExpiredError < StandardError; end + + class RevokedError < StandardError; end + + class InsufficientScopeError < StandardError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes + end + end +end diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 7cd8b6fbae..353c4ddebf 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -3,7 +3,7 @@ Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file} module Ci module API class API < Grape::API - include ::API::APIGuard + include APIGuard version 'v1', using: :path rescue_from ActiveRecord::RecordNotFound do From 0094d8f19644152a66b9d21b8cd86f797199311f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:29:57 +0300 Subject: [PATCH 025/199] Rename `images` to `container_registry` --- app/controllers/projects_controller.rb | 2 +- app/models/ability.rb | 12 ++++++++---- app/models/ci/build.rb | 1 - app/models/project.rb | 8 +++++--- app/views/projects/edit.html.haml | 8 ++++---- config/gitlab.yml.example | 1 + config/initializers/1_settings.rb | 12 ++++++------ .../20160407120251_add_images_enabled_for_project.rb | 2 +- db/schema.rb | 2 +- doc/permissions/permissions.md | 2 ++ lib/api/entities.rb | 2 +- lib/api/projects.rb | 10 +++++----- 12 files changed, 35 insertions(+), 27 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 52f7b99334..f4ec60ad2c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -235,7 +235,7 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :images_enabled, + :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled, :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, diff --git a/app/models/ability.rb b/app/models/ability.rb index ba27b9a9b1..59d5195f5b 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -203,7 +203,7 @@ class Ability :admin_label, :read_commit_status, :read_build, - :read_image, + :read_container_registry, ] end @@ -218,8 +218,8 @@ class Ability :create_merge_request, :create_wiki, :push_code, - :create_image, - :update_image, + :create_container_registry, + :update_container_registry, ] end @@ -246,7 +246,7 @@ class Ability :admin_project, :admin_commit_status, :admin_build, - :admin_image + :admin_container_registry, ] end @@ -291,6 +291,10 @@ class Ability rules += named_abilities('build') end + unless project.container_registry_enabled + rules += named_abilities('container_registry') + end + rules end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 2fea804714..4bc3a225e2 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -426,7 +426,6 @@ module Ci variables << { key: :CI_BUILD_NAME, value: name, public: true } variables << { key: :CI_BUILD_STAGE, value: stage, public: true } variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request - variables << { key: :CI_DOCKER_REGISTRY, value: project.registry_repository_url, public: true } if project.registry_repository_url variables end end diff --git a/app/models/project.rb b/app/models/project.rb index bce2545537..ab9ee9bad0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -69,7 +69,7 @@ class Project < ActiveRecord::Base default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets - default_value_for :images_enabled, gitlab_config_features.images + default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at @@ -375,8 +375,10 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end - def registry_repository_url - "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" if images_enabled? && Gitlab.config.registry.enabled + def container_registry_url + if container_registry_enabled? && Gitlab.config.registry.enabled + "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" + end end def commit(id = 'HEAD') diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 5c7960031e..f6a53fddf1 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -88,11 +88,11 @@ .form-group .col-sm-offset-2.col-sm-10 .checkbox - = f.label :images_enabled do - = f.check_box :images_enabled - %strong Images + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry %br - %span.descr Use Docker Registry for this repository + %span.descr Enable Container Registry for this repository = render 'builds_settings', f: f diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index cbb7c656fe..f7a5875342 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -98,6 +98,7 @@ production: &base wiki: true snippets: false builds: true + container_registry: true ## Webhook settings # Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b94f3f2f90..140d086054 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -230,12 +230,12 @@ Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 Settings.gitlab['session_expire_delay'] ||= 10080 -Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? -Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? -Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? -Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? -Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? -Settings.gitlab.default_projects_features['images'] = true if Settings.gitlab.default_projects_features['images'].nil? +Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? +Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? +Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? +Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? +Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? +Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb index 6a221a7fb0..47f0ca8e8d 100644 --- a/db/migrate/20160407120251_add_images_enabled_for_project.rb +++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb @@ -1,5 +1,5 @@ class AddImagesEnabledForProject < ActiveRecord::Migration def change - add_column :projects, :images_enabled, :boolean + add_column :projects, :container_registry_enabled, :boolean end end diff --git a/db/schema.rb b/db/schema.rb index 7ea16e2135..103a18d362 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -760,7 +760,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" - t.boolean "images_enabled" + t.boolean "container_registry_enabled" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 6219693b8a..6be5ea0b48 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -27,6 +27,7 @@ documentation](../workflow/add-user/add-user.md). | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | | See a commit status | | ✓ | ✓ | ✓ | ✓ | +| See a container registry | | ✓ | ✓ | ✓ | ✓ | | Manage merge requests | | | ✓ | ✓ | ✓ | | Create new merge request | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ | @@ -37,6 +38,7 @@ documentation](../workflow/add-user/add-user.md). | Write a wiki | | | ✓ | ✓ | ✓ | | Cancel and retry builds | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ | +| Update a container registry | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 95c3597b03..d62575e0a3 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -66,7 +66,7 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :images_enabled + expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled expose :created_at, :last_activity_at expose :shared_runners_enabled expose :creator_id diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6f85bc4b1b..d14b28e17f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -94,7 +94,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) - # images_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # namespace_id (optional) - defaults to user namespace # public (optional) - if true same as setting visibility_level = 20 @@ -113,7 +113,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, - :images_enabled, + :container_registry_enabled, :shared_runners_enabled, :namespace_id, :public, @@ -145,7 +145,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) - # images_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) @@ -209,7 +209,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) - # images_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project @@ -226,7 +226,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, - :images_enabled, + :container_registry_enabled, :shared_runners_enabled, :public, :visibility_level, From 1dea54c2201ec5f51bcd8772757dbc6612cc9fdc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:35:01 +0300 Subject: [PATCH 026/199] Remove unused parts --- config/gitlab.yml.example | 1 - config/initializers/1_settings.rb | 1 - lib/api/api.rb | 1 - lib/api/auth.rb | 187 ------------------------------ 4 files changed, 190 deletions(-) delete mode 100644 lib/api/auth.rb diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index f7a5875342..37639e52e6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -184,7 +184,6 @@ production: &base # internal_host: localhost # key: config/registry.key # issuer: omnibus-certificate - # path: shared/registry # # 2. GitLab CI settings diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 140d086054..1040d840e3 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -272,7 +272,6 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes # Settings['registry'] ||= Settingslogic.new({}) Settings.registry['registry'] = false if Settings.registry['enabled'].nil? -Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) Settings.registry['host'] ||= "example.com" Settings.registry['internal_host']||= "localhost" Settings.registry['key'] ||= nil diff --git a/lib/api/api.rb b/lib/api/api.rb index 6ddfe11d98..cc1004f800 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -58,6 +58,5 @@ module API mount Variables mount Runners mount Licenses - mount Auth end end diff --git a/lib/api/auth.rb b/lib/api/auth.rb deleted file mode 100644 index dab04bca81..0000000000 --- a/lib/api/auth.rb +++ /dev/null @@ -1,187 +0,0 @@ -module API - # Projects builds API - class Auth < Grape::API - namespace 'auth' do - get 'token' do - required_attributes! [:service] - keys = attributes_for_keys [:offline_token, :scope, :service] - - case keys[:service] - when 'docker' - docker_token_auth(keys[:scope], keys[:offline_token]) - else - not_found! - end - end - end - - helpers do - def docker_token_auth(scope, offline_token) - auth! - - if offline_token - forbidden! unless @user - elsif scope - @type, @path, actions = scope.split(':', 3) - bad_request!("invalid type: #{@type}") unless @type == 'repository' - - @actions = actions.split(',') - bad_request!('missing actions') if @actions.empty? - - @project = Project.find_with_namespace(@path) - not_found!('Project') unless @project - - authorize_actions!(@actions) - end - - { token: encode(docker_payload) } - end - - def auth! - auth = BasicRequest.new(request.env) - return unless auth.provided? - - return bad_request unless auth.basic? - - # Authentication with username and password - login, password = auth.credentials - - if ci_request?(login, password) - @ci = true - return - end - - @user = authenticate_user(login, password) - - if @user - request.env['REMOTE_USER'] = @user.username - end - end - - def ci_request?(login, password) - matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) - - if @project && matched_login.present? - underscored_service = matched_login['s'].underscore - - if underscored_service == 'gitlab_ci' - return @project.valid_build_token?(password) - end - end - - false - end - - def authenticate_user(login, password) - user = Gitlab::Auth.new.find(login, password) - - # If the user authenticated successfully, we reset the auth failure count - # from Rack::Attack for that IP. A client may attempt to authenticate - # with a username and blank password first, and only after it receives - # a 401 error does it present a password. Resetting the count prevents - # false positives from occurring. - # - # Otherwise, we let Rack::Attack know there was a failed authentication - # attempt from this IP. This information is stored in the Rails cache - # (Redis) and will be used by the Rack::Attack middleware to decide - # whether to block requests from this IP. - config = Gitlab.config.rack_attack.git_basic_auth - - if config.enabled - if user - # A successful login will reset the auth failure count from this IP - Rack::Attack::Allow2Ban.reset(@request.ip, config) - else - banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do - # Unless the IP is whitelisted, return true so that Allow2Ban - # increments the counter (stored in Rails.cache) for the IP - if config.ip_whitelist.include?(@request.ip) - false - else - true - end - end - - if banned - Rails.logger.info "IP #{@request.ip} failed to login " \ - "as #{login} but has been temporarily banned from Git auth" - end - end - end - - user - end - - def docker_payload - issued_at = Time.now - { - access: [ - type: @type, - name: @path, - actions: @actions - ], - iss: Gitlab.config.registry.issuer, - aud: "docker", - sub: @user.try(:username), - aud: @service, - iat: issued_at, - nbf: issued_at - 5.seconds, - exp: issued_at + 60.minutes, - jti: SecureRandom.uuid, - exp: Time.now.to_i + 3600 - }.compact - end - - def private_key - @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key - end - - def encode(payload) - headers = { - kid: kid(private_key) - } - JWT.encode(payload, private_key, 'RS256', headers) - end - - def authorize_actions!(actions) - actions.each do |action| - forbidden! unless can_access?(action) - end - end - - def can_access?(action) - case action - when 'pull' - @ci || can?(@user, :download_code, @project) - when 'push' - @ci || can?(@user, :push_code, @project) - else - false - end - end - - def kid(private_key) - sha256 = Digest::SHA256.new - sha256.update(private_key.public_key.to_der) - payload = StringIO.new(sha256.digest).read(30) - Base32.encode(payload).split("").each_slice(4).each_with_object([]) do |slice, mem| - mem << slice.join - end.join(":") - end - - class BasicRequest < Rack::Auth::AbstractRequest - def basic? - "basic" == scheme - end - - def credentials - @credentials ||= params.unpack("m*").first.split(/:/, 2) - end - - def username - credentials.first - end - end - end - end -end From 04badd28f969ea2cf7b2d767f34fa67f0dcdc980 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:35:10 +0300 Subject: [PATCH 027/199] Added CHANGELOG entry --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index fed3caef7e..7cccb9c946 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.8.0 (unreleased) - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project - Update SVG sanitizer to conform to SVG 1.1 - Updated search UI + - Added authentication service for Container Registry - Display informative message when new milestone is created - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view From 9f679ac2079dc1d412aaaed806c3d06bdc071046 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:35:25 +0300 Subject: [PATCH 028/199] Update docs/api/projects.md with container registry enabled option --- doc/api/projects.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/api/projects.md b/doc/api/projects.md index de1faadebf..f5f195b97d 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -424,6 +424,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -447,6 +448,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -472,6 +474,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `public_builds` (optional) From daca2144c80546169fb35fcf76b1f3d052b643cc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:47:06 +0300 Subject: [PATCH 029/199] Make code more clear in what is done --- app/controllers/jwt_controller.rb | 9 +++++---- .../jwt/docker_authentication_service.rb | 18 +++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 2a92627cb1..9bf1ddbba2 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -8,8 +8,9 @@ class JwtController < ApplicationController def auth @authenticated = authenticate_with_http_basic do |login, password| - @ci_project = ci_project(login, password) - @user = authenticate_user(login, password) unless @ci_project + # if it's possible we first try to authenticate project with login and password + @project = authenticate_project(login, password) + @user = authenticate_user(login, password) unless @project end unless @authenticated @@ -19,7 +20,7 @@ class JwtController < ApplicationController service = SERVICES[params[:service]] head :not_found unless service - result = service.new(@ci_project, @user, auth_params).execute + result = service.new(@project, @user, auth_params).execute return head result[:http_status] if result[:http_status] render json: result @@ -31,7 +32,7 @@ class JwtController < ApplicationController params.permit(:service, :scope, :offline_token, :account, :client_id) end - def ci_project(login, password) + def authenticate_project(login, password) matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) if matched_login.present? diff --git a/app/services/jwt/docker_authentication_service.rb b/app/services/jwt/docker_authentication_service.rb index ce28085e5d..fb0c41a12f 100644 --- a/app/services/jwt/docker_authentication_service.rb +++ b/app/services/jwt/docker_authentication_service.rb @@ -5,12 +5,12 @@ module Jwt return error('forbidden', 403) unless current_user end - { token: token.encoded } + { token: authorized_token.encoded } end private - def token + def authorized_token token = ::Jwt::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = params[:service] @@ -37,22 +37,22 @@ module Jwt end def process_repository_access(type, name, actions) - current_project = Project.find_with_namespace(name) - return unless current_project + requested_project = Project.find_with_namespace(name) + return unless requested_project actions = actions.select do |action| - can_access?(current_project, action) + can_access?(requested_project, action) end { type: type, name: name, actions: actions } if actions end - def can_access?(current_project, action) - case action + def can_access?(requested_project, requested_action) + case requested_action when 'pull' - current_project == project || can?(current_user, :download_code, current_project) + requested_project.public? || requested_project == project || can?(current_user, :download_code, requested_project) when 'push' - current_project == project || can?(current_user, :push_code, current_project) + requested_project == project || can?(current_user, :push_code, requested_project) else false end From b180d79cdca2ce0f6aa7425baf47db5b9c1ec2e3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 22:04:42 +0300 Subject: [PATCH 030/199] Rename DockerAuthenticationService to ContainerRegistryAuthenticationService --- app/controllers/jwt_controller.rb | 2 +- ...tainer_registry_authentication_service.rb} | 22 +++++++++++-------- lib/jwt/token.rb | 14 +++++------- 3 files changed, 20 insertions(+), 18 deletions(-) rename app/services/jwt/{docker_authentication_service.rb => container_registry_authentication_service.rb} (68%) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 9bf1ddbba2..0048a1a31e 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -3,7 +3,7 @@ class JwtController < ApplicationController skip_before_action :verify_authenticity_token SERVICES = { - 'docker' => Jwt::DockerAuthenticationService, + 'container_registry' => Jwt::ContainerRegistryAuthenticationService, } def auth diff --git a/app/services/jwt/docker_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb similarity index 68% rename from app/services/jwt/docker_authentication_service.rb rename to app/services/jwt/container_registry_authentication_service.rb index fb0c41a12f..b9fcd38047 100644 --- a/app/services/jwt/docker_authentication_service.rb +++ b/app/services/jwt/container_registry_authentication_service.rb @@ -1,16 +1,18 @@ module Jwt - class DockerAuthenticationService < BaseService + class ContainerRegistryAuthenticationService < BaseService def execute if params[:offline_token] return error('forbidden', 403) unless current_user end - { token: authorized_token.encoded } + return error('forbidden', 401) if scopes.empty? + + { token: authorized_token(scopes).encoded } end private - def authorized_token + def authorized_token(access) token = ::Jwt::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = params[:service] @@ -19,11 +21,13 @@ module Jwt token end - def access + def scopes return unless params[:scope] - scope = process_scope(params[:scope]) - [scope].compact + @scopes ||= begin + scope = process_scope(params[:scope]) + [scope].compact + end end def process_scope(scope) @@ -44,15 +48,15 @@ module Jwt can_access?(requested_project, action) end - { type: type, name: name, actions: actions } if actions + { type: type, name: name, actions: actions } if actions.present? end def can_access?(requested_project, requested_action) case requested_action when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :download_code, requested_project) + requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) when 'push' - requested_project == project || can?(current_user, :push_code, requested_project) + requested_project == project || can?(current_user, :create_container_registry, requested_project) else false end diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb index 38cbc8004e..765ab0d60c 100644 --- a/lib/jwt/token.rb +++ b/lib/jwt/token.rb @@ -4,19 +4,21 @@ module Jwt attr_accessor :issued_at, :not_before, :expire_time def initialize - @payload = {} @id = SecureRandom.uuid @issued_at = Time.now + # we give a few seconds for time shift @not_before = issued_at - 5.seconds + # default 60 seconds should be more than enough for this authentication token @expire_time = issued_at + 1.minute + @custom_payload = {} end def [](key) - @payload[key] + @custom_payload[key] end def []=(key, value) - @payload[key] = value + @custom_payload[key] = value end def encoded @@ -24,11 +26,7 @@ module Jwt end def payload - @payload.merge(default_payload) - end - - def to_json - payload.to_json + @custom_payload.merge(default_payload) end private From c78b97df0eb275415d6ed5ef297841ee2f61b473 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 22:06:32 +0300 Subject: [PATCH 031/199] Added rspec for testing container registry authentication service --- spec/lib/jwt/rsa_token_spec.rb | 31 +++ spec/lib/jwt/token_spec.rb | 18 ++ ...er_registry_authentication_service_spec.rb | 192 ++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 spec/lib/jwt/rsa_token_spec.rb create mode 100644 spec/lib/jwt/token_spec.rb create mode 100644 spec/services/jwt/container_registry_authentication_service_spec.rb diff --git a/spec/lib/jwt/rsa_token_spec.rb b/spec/lib/jwt/rsa_token_spec.rb new file mode 100644 index 0000000000..710801923e --- /dev/null +++ b/spec/lib/jwt/rsa_token_spec.rb @@ -0,0 +1,31 @@ +describe Jwt::RSAToken do + let(:rsa_key) { generate_key } + let(:rsa_token) { described_class.new(nil) } + let(:rsa_encoded) { rsa_token.encoded } + + before { allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) } + + context 'token' do + context 'for valid key to be validated' do + before { rsa_token['key'] = 'value' } + + subject { JWT.decode(rsa_encoded, rsa_key) } + + it { expect{subject}.to_not raise_error } + it { expect(subject.first).to include('key' => 'value') } + end + + context 'for invalid key to raise an exception' do + let(:new_key) { generate_key } + subject { JWT.decode(rsa_encoded, new_key) } + + it { expect{subject}.to raise_error(JWT::DecodeError) } + end + end + + private + + def generate_key + OpenSSL::PKey::RSA.generate(512) + end +end diff --git a/spec/lib/jwt/token_spec.rb b/spec/lib/jwt/token_spec.rb new file mode 100644 index 0000000000..a56b4cf39b --- /dev/null +++ b/spec/lib/jwt/token_spec.rb @@ -0,0 +1,18 @@ +describe Jwt::Token do + let(:token) { described_class.new } + + context 'custom parameters' do + let(:value) { 'value' } + before { token[:key] = value } + + it { expect(token[:key]).to eq(value) } + it { expect(token.payload).to include(key: value) } + end + + context 'embeds default payload' do + subject { token.payload } + let(:default) { token.send(:default_payload) } + + it { is_expected.to include(default) } + end +end diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/jwt/container_registry_authentication_service_spec.rb new file mode 100644 index 0000000000..ea91f499d0 --- /dev/null +++ b/spec/services/jwt/container_registry_authentication_service_spec.rb @@ -0,0 +1,192 @@ +require 'spec_helper' + +describe Jwt::ContainerRegistryAuthenticationService, services: true do + let(:current_project) { nil } + let(:current_user) { nil } + let(:current_params) { {} } + let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } + let(:registry_settings) { + { + issuer: 'rspec', + key: nil + } + } + let(:payload) { JWT.decode(subject[:token], rsa_key).first } + + subject { described_class.new(current_project, current_user, current_params).execute } + + before do + allow(Gitlab.config.registry).to receive_messages(registry_settings) + allow_any_instance_of(Jwt::RSAToken).to receive(:key).and_return(rsa_key) + end + + shared_examples 'an authenticated' do + it { is_expected.to include(:token) } + it { expect(payload).to include('access') } + end + + shared_examples 'a accessible' do + let(:access) { + [{ + 'type' => 'repository', + 'name' => project.path_with_namespace, + 'actions' => actions, + }] + } + + it_behaves_like 'an authenticated' + it { expect(payload).to include('access' => access) } + end + + shared_examples 'a pullable' do + it_behaves_like 'a accessible' do + let(:actions) { ['pull'] } + end + end + + shared_examples 'a pushable' do + it_behaves_like 'a accessible' do + let(:actions) { ['push'] } + end + end + + shared_examples 'a pullable and pushable' do + it_behaves_like 'a accessible' do + let(:actions) { ['pull', 'push'] } + end + end + + shared_examples 'a forbidden' do + it { is_expected.to include(http_status: 401) } + it { is_expected.to_not include(:token) } + end + + context 'user authorization' do + let(:project) { create(:project) } + let(:current_user) { create(:user) } + + context 'allow developer to push images' do + before { project.team << [current_user, :developer] } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:push" } + } + + it_behaves_like 'a pushable' + end + + context 'allow reporter to pull images' do + before { project.team << [current_user, :reporter] } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull" } + } + + it_behaves_like 'a pullable' + end + + context 'return a least of privileges' do + before { project.team << [current_user, :reporter] } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:push,pull" } + } + + it_behaves_like 'a pullable' + end + + context 'disallow guest to pull or push images' do + before { project.team << [current_user, :guest] } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull,push" } + } + + it_behaves_like 'a forbidden' + end + end + + context 'project authorization' do + let(:current_project) { create(:empty_project) } + + context 'allow to pull and push images' do + let(:current_params) { + { scope: "repository:#{current_project.path_with_namespace}:pull,push" } + } + + it_behaves_like 'a pullable and pushable' do + let(:project) { current_project } + end + end + + context 'for other projects' do + context 'when pulling' do + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull" } + } + + context 'allow for public' do + let(:project) { create(:empty_project, :public) } + it_behaves_like 'a pullable' + end + + context 'disallow for private' do + let(:project) { create(:empty_project, :private) } + it_behaves_like 'a forbidden' + end + end + + context 'when pushing' do + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:push" } + } + + context 'disallow for all' do + let(:project) { create(:empty_project, :public) } + it_behaves_like 'a forbidden' + end + end + + end + end + + context 'unauthorized' do + context 'for invalid scope' do + let(:current_params) { + { scope: 'invalid:aa:bb' } + } + + it_behaves_like 'a forbidden' + end + + context 'for private project' do + let(:project) { create(:empty_project, :private) } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull" } + } + + it_behaves_like 'a forbidden' + end + + context 'for public project' do + let(:project) { create(:empty_project, :public) } + + context 'when pulling and pushing' do + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull,push" } + } + + it_behaves_like 'a pullable' + end + + context 'when pushing' do + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:push" } + } + + it_behaves_like 'a forbidden' + end + end + end +end From e56e3cdc62f96541b9bd8b7814204e92f1909253 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 9 May 2016 19:35:37 -0300 Subject: [PATCH 032/199] Fix api leaking notes when user is not authorized to read noteable --- CHANGELOG | 1 + lib/api/notes.rb | 29 +++++++++++++++++------------ spec/requests/api/notes_spec.rb | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 63077523aa..2c44944f66 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.8.0 (unreleased) - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. - Support e-mail notifications for comments on project snippets + - Fix API leak of notes of unauthorized issues, snippets and merge requests - Use ActionDispatch Remote IP for Akismet checking - Fix error when visiting commit builds page before build was updated - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 71a53e6f0d..4ac08a3e8c 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -20,19 +20,24 @@ module API # GET /projects/:id/snippets/:noteable_id/notes get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + read_ability_name = "read_#{@noteable.class.to_s.underscore.downcase}".to_sym - # We exclude notes that are cross-references and that cannot be viewed - # by the current user. By doing this exclusion at this level and not - # at the DB query level (which we cannot in that case), the current - # page can have less elements than :per_page even if - # there's more than one page. - notes = - # paginate() only works with a relation. This could lead to a - # mismatch between the pagination headers info and the actual notes - # array returned, but this is really a edge-case. - paginate(@noteable.notes). - reject { |n| n.cross_reference_not_visible_for?(current_user) } - present notes, with: Entities::Note + if can?(current_user, read_ability_name, @noteable) + # We exclude notes that are cross-references and that cannot be viewed + # by the current user. By doing this exclusion at this level and not + # at the DB query level (which we cannot in that case), the current + # page can have less elements than :per_page even if + # there's more than one page. + notes = + # paginate() only works with a relation. This could lead to a + # mismatch between the pagination headers info and the actual notes + # array returned, but this is really a edge-case. + paginate(@noteable.notes). + reject { |n| n.cross_reference_not_visible_for?(current_user) } + present notes, with: Entities::Note + else + render_api_error!("Not found.", 404) + end end # Get a single +noteable+ note diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 49091fc0f4..f5b31be1ba 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -57,6 +57,15 @@ describe API::API, api: true do expect(json_response).to be_empty end + context "and issue is confidential" do + before { ext_issue.update_attributes(confidential: true) } + + it "returns 404" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + expect(response.status).to eq(404) + end + end + context "and current user can view the note" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) @@ -80,6 +89,11 @@ describe API::API, api: true do get api("/projects/#{project.id}/snippets/42/notes", user) expect(response.status).to eq(404) end + + it "returns 404 when not authorized" do + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user) + expect(response.status).to eq(404) + end end context "when noteable is a Merge Request" do @@ -94,6 +108,11 @@ describe API::API, api: true do get api("/projects/#{project.id}/merge_requests/4444/notes", user) expect(response.status).to eq(404) end + + it "returns 404 when not authorized" do + get api("/projects/#{project.id}/merge_requests/4444/notes", private_user) + expect(response.status).to eq(404) + end end end From dd1ad6bf3e5a378eb8698e95b73e58f935eb0956 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Tue, 10 May 2016 09:56:12 +0700 Subject: [PATCH 033/199] Code improve --- app/views/snippets/index.html.haml | 22 +++++++++++----------- config/routes.rb | 15 ++++++++------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index 8aa3d490fa..7e4918a608 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,13 +1,13 @@ - - page_title "By #{@user.name}", "Snippets" +- page_title "By #{@user.name}", "Snippets" - %ol.breadcrumb - %li - = link_to snippets_path do - Snippets - %li - = @user.name - .pull-right.hidden-xs - = link_to user_path(@user) do - #{@user.name} profile page +%ol.breadcrumb + %li + = link_to snippets_path do + Snippets + %li + = @user.name + .pull-right.hidden-xs + = link_to user_path(@user) do + #{@user.name} profile page - = render 'snippets' += render 'snippets' diff --git a/config/routes.rb b/config/routes.rb index 594bab204f..881de37f10 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -344,15 +344,16 @@ Rails.application.routes.draw do end scope(path: 'u/:username', + as: :user, constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Tue, 10 May 2016 11:10:51 +0200 Subject: [PATCH 034/199] create import data in service --- app/services/projects/create_service.rb | 2 ++ lib/gitlab/bitbucket_import/project_creator.rb | 7 ++----- lib/gitlab/fogbugz_import/project_creator.rb | 9 +++------ lib/gitlab/google_code_import/project_creator.rb | 9 +++------ 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 501e58c140..a77652480b 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -6,6 +6,7 @@ module Projects def execute forked_from_project_id = params.delete(:forked_from_project_id) + import_data = params.delete(:import_data) @project = Project.new(params) @@ -50,6 +51,7 @@ module Projects end Project.transaction do + @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data @project.save if @project.persisted? && !@project.import? diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index 941f818b84..b90ef0b0fb 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -11,7 +11,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo["name"], path: repo["slug"], @@ -21,11 +21,8 @@ module Gitlab import_type: "bitbucket", import_source: "#{repo["owner"]}/#{repo["slug"]}", import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git", + import_data: { credentials: { bb_session: session_data } } ).execute - - project.create_or_update_import_data(credentials: { bb_session: session_data }) - - project end end end diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb index 3840765db8..1918d5b208 100644 --- a/lib/gitlab/fogbugz_import/project_creator.rb +++ b/lib/gitlab/fogbugz_import/project_creator.rb @@ -12,7 +12,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo.safe_name, path: repo.path, @@ -21,12 +21,9 @@ module Gitlab visibility_level: Gitlab::VisibilityLevel::INTERNAL, import_type: 'fogbugz', import_source: repo.name, - import_url: Project::UNKNOWN_IMPORT_URL + import_url: Project::UNKNOWN_IMPORT_URL, + import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session } } ).execute - - project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session }) - - project end end end diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb index 0abb7a64c1..326cfcaa8a 100644 --- a/lib/gitlab/google_code_import/project_creator.rb +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -11,7 +11,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo.name, path: repo.name, @@ -21,12 +21,9 @@ module Gitlab visibility_level: Gitlab::VisibilityLevel::PUBLIC, import_type: "google_code", import_source: repo.name, - import_url: repo.import_url + import_url: repo.import_url, + import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map } } ).execute - - project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }) - - project end end end From 732e98eee7da718d282903844649d02f935fd2be Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 9 May 2016 12:47:44 +0200 Subject: [PATCH 035/199] Reuse runners edit form in admin and project area --- app/views/admin/runners/show.html.haml | 22 +++--------------- app/views/projects/runners/_form.html.haml | 25 +++++++++++++++++++++ app/views/projects/runners/edit.html.haml | 26 +--------------------- 3 files changed, 29 insertions(+), 44 deletions(-) create mode 100644 app/views/projects/runners/_form.html.haml diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 8700b4820c..4dfb3ed05b 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -22,25 +22,9 @@ %h4 This runner will process builds only from ASSIGNED projects %p You can't make this a shared runner. %hr -= form_for @runner, url: admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| - .form-group - = label_tag :token, class: 'control-label' do - Token - .col-sm-10 - = f.text_field :token, class: 'form-control', readonly: true - .form-group - = label_tag :description, class: 'control-label' do - Description - .col-sm-10 - = f.text_field :description, class: 'form-control' - .form-group - = label_tag :tag_list, class: 'control-label' do - Tags - .col-sm-10 - = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' - .help-block You can setup builds to only use runners with specific tags - .form-actions - = f.submit 'Save', class: 'btn btn-save' + +.append-bottom-20 + = render '/projects/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner) .row .col-md-6 diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml new file mode 100644 index 0000000000..2d6c964ae9 --- /dev/null +++ b/app/views/projects/runners/_form.html.haml @@ -0,0 +1,25 @@ += form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f| + .form-group + = label :active, "Active", class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :active + %span.light Paused runners don't accept new builds + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, value: runner.tag_list.to_s, class: 'form-control' + .help-block You can setup jobs to only use runners with specific tags + .form-actions + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index eba03028af..771947d790 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -2,28 +2,4 @@ %h4 Runner ##{@runner.id} %hr -= form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f| - .form-group - = label :active, "Active", class: 'control-label' - .col-sm-10 - .checkbox - = f.check_box :active - %span.light Paused runners don't accept new builds - .form-group - = label_tag :token, class: 'control-label' do - Token - .col-sm-10 - = f.text_field :token, class: 'form-control', readonly: true - .form-group - = label_tag :description, class: 'control-label' do - Description - .col-sm-10 - = f.text_field :description, class: 'form-control' - .form-group - = label_tag :tag_list, class: 'control-label' do - Tags - .col-sm-10 - = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' - .help-block You can setup jobs to only use runners with specific tags - .form-actions - = f.submit 'Save changes', class: 'btn btn-save' + = render 'form', runner: @runner, runner_form_url: runner_path(@runner) From 7d6d3421d64085c05c8b7a17dcf0b70fbdd478b5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 9 May 2016 13:07:10 +0200 Subject: [PATCH 036/199] Share form editable attributes mf runner via model --- app/controllers/admin/runners_controller.rb | 2 +- app/controllers/projects/runners_controller.rb | 2 +- app/models/ci/runner.rb | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index a701d49b84..8b8a732007 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -58,6 +58,6 @@ class Admin::RunnersController < Admin::ApplicationController end def runner_params - params.require(:runner).permit(:token, :description, :tag_list, :active) + params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 0dd2d6a99b..3a9d67aff6 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -64,6 +64,6 @@ class Projects::RunnersController < Projects::ApplicationController end def runner_params - params.require(:runner).permit(:description, :tag_list, :active) + params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index add59a0889..d27963e8ec 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -23,6 +23,7 @@ module Ci LAST_CONTACT_TIME = 5.minutes.ago AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] + FORM_EDITABLE = [:description, :tag_list, :active] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' From 67dc3b9c1131b5a7f3f861a0fe35169fda5cd398 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Fri, 22 Apr 2016 09:50:08 -0700 Subject: [PATCH 037/199] Add the health_check gem end expose the health_check route --- Gemfile | 3 +++ Gemfile.lock | 3 +++ config/initializers/health_check.rb | 28 ++++++++++++++++++++++++++++ config/routes.rb | 3 +++ 4 files changed, 37 insertions(+) create mode 100644 config/initializers/health_check.rb diff --git a/Gemfile b/Gemfile index 3e5c604ae0..204d19f711 100644 --- a/Gemfile +++ b/Gemfile @@ -333,3 +333,6 @@ gem 'oauth2', '~> 1.0.0' # Soft deletion gem "paranoia", "~> 2.0" + +# Health check +gem 'health_check', '~> 1.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index 86b9142ef2..de0f536641 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -402,6 +402,8 @@ GEM html2haml (>= 1.0.1) railties (>= 4.0.1) hashie (3.4.3) + health_check (1.5.1) + rails (>= 2.3.0) highline (1.7.8) hipchat (1.5.2) httparty @@ -947,6 +949,7 @@ DEPENDENCIES grape (~> 0.13.0) grape-entity (~> 0.4.2) haml-rails (~> 0.9.0) + health_check (~> 1.5.1) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty (~> 0.13.3) diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb new file mode 100644 index 0000000000..c111cfdf5c --- /dev/null +++ b/config/initializers/health_check.rb @@ -0,0 +1,28 @@ +# HealthCheck.setup do |config| +# # Text output upon success +# config.success = 'success' +# +# # Timeout in seconds used when checking smtp server +# config.smtp_timeout = 30.0 +# +# # http status code used when plain text error message is output +# # Set to 200 if you want your want to distinguish between partial (text does not include success) and +# # total failure of rails application (http status of 500 etc) +# config.http_status_for_error_text = 500 +# +# # http status code used when an error object is output (json or xml) +# # Set to 200 if you want your want to distinguish between partial (healthy property == false) and +# # total failure of rails application (http status of 500 etc) +# config.http_status_for_error_object = 500 +# +# # You can customize which checks happen on a standard health check +# config.standard_checks = [ 'database', 'migrations', 'custom' ] +# +# # You can set what tests are run with the 'full' or 'all' parameter +# config.full_checks = ['database', 'migrations', 'custom', 'email', 'cache'] +# +# # Add one or more custom checks that return a blank string if ok, or an error message if there is an error +# config.add_custom_check do +# any code that returns blank on success and non blank string upon failure +# end +# end diff --git a/config/routes.rb b/config/routes.rb index dafecc9464..cac800b9a9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -73,6 +73,9 @@ Rails.application.routes.draw do mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq end + # Health check + health_check_routes + # Enable Grack support mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] From 9898f9b4e6b80edaa914675edfa9b229498b31fe Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 5 May 2016 12:25:02 -0700 Subject: [PATCH 038/199] Set the standard health_check options to include the cache check. --- config/initializers/health_check.rb | 31 +++-------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb index c111cfdf5c..a93e427303 100644 --- a/config/initializers/health_check.rb +++ b/config/initializers/health_check.rb @@ -1,28 +1,3 @@ -# HealthCheck.setup do |config| -# # Text output upon success -# config.success = 'success' -# -# # Timeout in seconds used when checking smtp server -# config.smtp_timeout = 30.0 -# -# # http status code used when plain text error message is output -# # Set to 200 if you want your want to distinguish between partial (text does not include success) and -# # total failure of rails application (http status of 500 etc) -# config.http_status_for_error_text = 500 -# -# # http status code used when an error object is output (json or xml) -# # Set to 200 if you want your want to distinguish between partial (healthy property == false) and -# # total failure of rails application (http status of 500 etc) -# config.http_status_for_error_object = 500 -# -# # You can customize which checks happen on a standard health check -# config.standard_checks = [ 'database', 'migrations', 'custom' ] -# -# # You can set what tests are run with the 'full' or 'all' parameter -# config.full_checks = ['database', 'migrations', 'custom', 'email', 'cache'] -# -# # Add one or more custom checks that return a blank string if ok, or an error message if there is an error -# config.add_custom_check do -# any code that returns blank on success and non blank string upon failure -# end -# end +HealthCheck.setup do |config| + config.standard_checks = [ 'database', 'migrations', 'cache' ] +end From 160ef66d1bbbbc593516c7575d6b02ddb019c000 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Mon, 9 May 2016 16:21:22 -0700 Subject: [PATCH 039/199] Add health_check access token, and enforce on the health_check endpoint Also added a health check page to the admin section for resetting the token. --- .../admin/application_settings_controller.rb | 6 +++ .../admin/health_check_controller.rb | 9 +++++ app/controllers/health_check_controller.rb | 13 ++++++ app/models/application_setting.rb | 6 +++ app/views/admin/health_check/show.html.haml | 40 +++++++++++++++++++ app/views/layouts/nav/_admin.html.haml | 5 +++ config/routes.rb | 4 +- ...ck_access_token_to_application_settings.rb | 5 +++ db/schema.rb | 3 +- 9 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 app/controllers/admin/health_check_controller.rb create mode 100644 app/controllers/health_check_controller.rb create mode 100644 app/views/admin/health_check/show.html.haml create mode 100644 db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ec22548dde..7b9a88cd31 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -19,6 +19,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to admin_runners_path end + def reset_health_check_token + @application_setting.reset_health_check_access_token! + flash[:notice] = 'New health check access token has been generated!' + redirect_to :back + end + def clear_repository_check_states RepositoryCheck::ClearWorker.perform_async diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb new file mode 100644 index 0000000000..3153a765e3 --- /dev/null +++ b/app/controllers/admin/health_check_controller.rb @@ -0,0 +1,9 @@ +class Admin::HealthCheckController < Admin::ApplicationController + def show + begin + @errors = HealthCheck::Utils.process_checks('standard') + rescue => e + @errors = e.message.blank? ? e.class.to_s : e.message.to_s + end + end +end diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb new file mode 100644 index 0000000000..b974489836 --- /dev/null +++ b/app/controllers/health_check_controller.rb @@ -0,0 +1,13 @@ +class HealthCheckController < HealthCheck::HealthCheckController + before_action :validate_health_check_access! + + protected + + def validate_health_check_access! + return render_404 unless params[:token].presence && params[:token] == current_application_settings.health_check_access_token + end + + def render_404 + render file: Rails.root.join("public", "404"), layout: false, status: "404" + end +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 7039db2d41..bf88326c11 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,6 +1,7 @@ class ApplicationSetting < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :runners_registration_token + add_authentication_token_field :health_check_access_token CACHE_KEY = 'application_setting.last' @@ -70,6 +71,7 @@ class ApplicationSetting < ActiveRecord::Base end before_save :ensure_runners_registration_token + before_save :ensure_health_check_access_token after_commit do Rails.cache.write(CACHE_KEY, self) @@ -133,4 +135,8 @@ class ApplicationSetting < ActiveRecord::Base def runners_registration_token ensure_runners_registration_token! end + + def health_check_access_token + ensure_health_check_access_token! + end end diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml new file mode 100644 index 0000000000..70e5d04e35 --- /dev/null +++ b/app/views/admin/health_check/show.html.haml @@ -0,0 +1,40 @@ +- page_title "Health Check" + +%h3.page-title + Health Check +%p.light + Health information can be reteived as plain text, json, or xml using: + %ul + %li + %code= "/health_check?token=#{current_application_settings.health_check_access_token}" + %li + %code= "/health_check.json?token=#{current_application_settings.health_check_access_token}" + %li + %code= "/health_check.xml?token=#{current_application_settings.health_check_access_token}" + +.bs-callout.clearfix + .pull-left + %p + You can reset the health check access token by pressing the button below. + %p + = button_to reset_health_check_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset the health check token?' } do + = icon('refresh') + Reset health check access token + +%hr +.panel.panel-default + .panel-heading + Current Status: + - if @errors.blank? + = icon('circle', class: 'cgreen') + Healthy + - else + = icon('warning', class: 'cred') + Unhealthy + .panel-body + - if @errors.blank? + No Health Problems Detected + - else + = @errors diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 280a1b9372..f292730fe4 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -41,6 +41,11 @@ = icon('file-text fw') %span Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + = icon('medkit fw') + %span + Health Check = nav_link(controller: :broadcast_messages) do = link_to admin_broadcast_messages_path, title: 'Messages' do = icon('bullhorn fw') diff --git a/config/routes.rb b/config/routes.rb index cac800b9a9..c81bf294a5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,7 +74,7 @@ Rails.application.routes.draw do end # Health check - health_check_routes + get 'health_check(/:checks)(.:format)' => 'health_check#index' # Enable Grack support mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] @@ -256,6 +256,7 @@ Rails.application.routes.draw do end resource :logs, only: [:show] + resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do @@ -287,6 +288,7 @@ Rails.application.routes.draw do resource :application_settings, only: [:show, :update] do resources :services put :reset_runners_token + put :reset_health_check_token put :clear_repository_check_states end diff --git a/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb new file mode 100644 index 0000000000..9d729fec18 --- /dev/null +++ b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddHealthCheckAccessTokenToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :health_check_access_token, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 71d953afe3..8ac3eeae62 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: 20160508194200) do +ActiveRecord::Schema.define(version: 20160509201028) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -80,6 +80,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.boolean "repository_checks_enabled", default: false t.text "shared_runners_text" t.integer "metrics_packet_size", default: 1 + t.string "health_check_access_token" end create_table "audit_events", force: :cascade do |t| From 93ca5c9964a26fbf31fcc794348b30193f4dff9f Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 10 May 2016 16:06:02 -0300 Subject: [PATCH 040/199] Fix notes API calls symbol convertions --- lib/api/notes.rb | 6 +++--- spec/requests/api/notes_spec.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 4ac08a3e8c..f0116acd90 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -19,7 +19,7 @@ module API # GET /projects/:id/issues/:noteable_id/notes # GET /projects/:id/snippets/:noteable_id/notes get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do - @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) read_ability_name = "read_#{@noteable.class.to_s.underscore.downcase}".to_sym if can?(current_user, read_ability_name, @noteable) @@ -36,7 +36,7 @@ module API reject { |n| n.cross_reference_not_visible_for?(current_user) } present notes, with: Entities::Note else - render_api_error!("Not found.", 404) + not_found!("Notes") end end @@ -50,7 +50,7 @@ module API # GET /projects/:id/issues/:noteable_id/notes/:note_id # GET /projects/:id/snippets/:noteable_id/notes/:note_id get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do - @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) @note = @noteable.notes.find(params[:note_id]) if @note.cross_reference_not_visible_for?(current_user) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index f5b31be1ba..f9bfee9442 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -39,6 +39,7 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should return an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(issue_note.note) @@ -46,12 +47,14 @@ describe API::API, api: true do it "should return a 404 error when issue id not found" do get api("/projects/#{project.id}/issues/12345/notes", user) + expect(response.status).to eq(404) end context "that references a private issue" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response).to be_empty @@ -62,6 +65,7 @@ describe API::API, api: true do it "returns 404" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + expect(response.status).to eq(404) end end @@ -69,6 +73,7 @@ describe API::API, api: true do context "and current user can view the note" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(cross_reference_note.note) @@ -80,6 +85,7 @@ describe API::API, api: true do context "when noteable is a Snippet" do it "should return an array of snippet notes" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(snippet_note.note) @@ -87,11 +93,13 @@ describe API::API, api: true do it "should return a 404 error when snippet id not found" do get api("/projects/#{project.id}/snippets/42/notes", user) + expect(response.status).to eq(404) end it "returns 404 when not authorized" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user) + expect(response.status).to eq(404) end end @@ -99,6 +107,7 @@ describe API::API, api: true do context "when noteable is a Merge Request" do it "should return an array of merge_requests notes" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(merge_request_note.note) @@ -106,11 +115,13 @@ describe API::API, api: true do it "should return a 404 error if merge request id not found" do get api("/projects/#{project.id}/merge_requests/4444/notes", user) + expect(response.status).to eq(404) end it "returns 404 when not authorized" do get api("/projects/#{project.id}/merge_requests/4444/notes", private_user) + expect(response.status).to eq(404) end end @@ -120,24 +131,28 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should return an issue note by id" do get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) + expect(response.status).to eq(200) expect(json_response['body']).to eq(issue_note.note) end it "should return a 404 error if issue note not found" do get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + expect(response.status).to eq(404) end context "that references a private issue" do it "should return a 404 error" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) + expect(response.status).to eq(404) end context "and current user can view the note" do it "should return an issue note by id" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) + expect(response.status).to eq(200) expect(json_response['body']).to eq(cross_reference_note.note) end @@ -148,12 +163,14 @@ describe API::API, api: true do context "when noteable is a Snippet" do it "should return a snippet note by id" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) + expect(response.status).to eq(200) expect(json_response['body']).to eq(snippet_note.note) end it "should return a 404 error if snippet note not found" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user) + expect(response.status).to eq(404) end end @@ -163,6 +180,7 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should create a new issue note" do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + expect(response.status).to eq(201) expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) @@ -170,11 +188,13 @@ describe API::API, api: true do it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + expect(response.status).to eq(400) end it "should return a 401 unauthorized error if user not authenticated" do post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' + expect(response.status).to eq(401) end @@ -183,6 +203,7 @@ describe API::API, api: true do creation_time = 2.weeks.ago post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!', created_at: creation_time + expect(response.status).to eq(201) expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) @@ -195,6 +216,7 @@ describe API::API, api: true do context "when noteable is a Snippet" do it "should create a new snippet note" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' + expect(response.status).to eq(201) expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) @@ -202,11 +224,13 @@ describe API::API, api: true do it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + expect(response.status).to eq(400) end it "should return a 401 unauthorized error if user not authenticated" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' + expect(response.status).to eq(401) end end @@ -246,6 +270,7 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user), body: 'Hello!' + expect(response.status).to eq(200) expect(json_response['body']).to eq('Hello!') end @@ -253,12 +278,14 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), body: 'Hello!' + expect(response.status).to eq(404) end it 'should return a 400 bad request error if body not given' do put api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) + expect(response.status).to eq(400) end end @@ -267,6 +294,7 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user), body: 'Hello!' + expect(response.status).to eq(200) expect(json_response['body']).to eq('Hello!') end @@ -274,6 +302,7 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/12345", user), body: "Hello!" + expect(response.status).to eq(404) end end @@ -282,6 +311,7 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ "notes/#{merge_request_note.id}", user), body: 'Hello!' + expect(response.status).to eq(200) expect(json_response['body']).to eq('Hello!') end @@ -289,6 +319,7 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ "notes/12345", user), body: "Hello!" + expect(response.status).to eq(404) end end From e689d9fb70d7484de708f8f436b294b40a9f6486 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 10 May 2016 16:32:59 -0500 Subject: [PATCH 041/199] Reduce width of side nav --- app/assets/stylesheets/framework/sidebar.scss | 11 +++++++---- app/assets/stylesheets/framework/variables.scss | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index bd91f51708..a509ef2f14 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -63,7 +63,7 @@ float: left; height: $header-height; width: 100%; - padding-left: 22px; + padding-left: 16px; overflow: hidden; outline: none; transition-duration: .3s; @@ -78,7 +78,7 @@ } .gitlab-text-container { - width: 230px; + width: 220px; h3 { width: 158px; @@ -146,12 +146,12 @@ a { padding: 7px 15px; + transition-duration: .3s; font-size: $gl-font-size; line-height: 24px; color: $gray; display: block; text-decoration: none; - padding-left: 23px; font-weight: normal; outline: none; @@ -253,6 +253,9 @@ width: auto; a { + padding-left: 23px; + transition-duration: .3s; + span { display: none; } @@ -312,7 +315,7 @@ } .nav-sidebar li a { - width: 230px; + width: 220px; &.back-link { i { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 84e74db06b..5fa4c26660 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -2,7 +2,7 @@ * Layout */ $sidebar_collapsed_width: 62px; -$sidebar_width: 230px; +$sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 258px; From a51064906bd4dcb29ea61c2278e2c32e848a5de6 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 10 May 2016 16:51:57 -0500 Subject: [PATCH 042/199] No extra link padding at xs screen size --- app/assets/stylesheets/framework/sidebar.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index a509ef2f14..c243dd8a42 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -253,8 +253,10 @@ width: auto; a { - padding-left: 23px; - transition-duration: .3s; + @media (min-width: $screen-xs-min) { + padding-left: 23px; + transition-duration: .3s; + } span { display: none; From 0e0caf4d17c28b6b0f3488b25efa265ce2804cc4 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Tue, 10 May 2016 16:19:16 -0700 Subject: [PATCH 043/199] Add tests for the health check feature --- app/views/admin/health_check/show.html.haml | 3 + .../health_check_controller_spec.rb | 90 +++++++++++++++++++ .../features/admin/admin_health_check_spec.rb | 55 ++++++++++++ spec/routing/admin_routing_spec.rb | 7 ++ spec/routing/routing_spec.rb | 10 +++ 5 files changed, 165 insertions(+) create mode 100644 spec/controllers/health_check_controller_spec.rb create mode 100644 spec/features/admin/admin_health_check_spec.rb diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 70e5d04e35..23a931995a 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -2,6 +2,9 @@ %h3.page-title Health Check +%p.light + Access token is + %code{ id:'health-check-token' }= "#{current_application_settings.health_check_access_token}" %p.light Health information can be reteived as plain text, json, or xml using: %ul diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb new file mode 100644 index 0000000000..3b9cc5c98f --- /dev/null +++ b/spec/controllers/health_check_controller_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe HealthCheckController do + let(:token) { current_application_settings.health_check_access_token } + let(:json_response) { JSON.parse(response.body) } + let(:xml_response) { Hash.from_xml(response.body)['hash'] } + + describe 'GET #index' do + context 'when services are up but NO access token' do + it 'returns a not found page' do + get :index + expect(response).to be_not_found + end + end + + context 'when services are up and an access token is provided' do + it 'supports successful plaintest response' do + get :index, token: token + expect(response).to be_success + expect(response.content_type).to eq 'text/plain' + end + + it 'supports successful json response' do + get :index, token: token, format: :json + expect(response).to be_success + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be true + end + + it 'supports successful xml response' do + get :index, token: token, format: :xml + expect(response).to be_success + expect(response.content_type).to eq 'application/xml' + expect(xml_response['healthy']).to be true + end + + it 'supports successful responses for specific checks' do + get :index, token: token, checks: 'email', format: :json + expect(response).to be_success + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be true + end + end + + context 'when a service is down but NO access token' do + it 'returns a not found page' do + get :index + expect(response).to be_not_found + end + end + + 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') + end + + it 'supports failure plaintest response' do + get :index, token: token + expect(response.status).to eq(500) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to include('The server is on fire') + end + + it 'supports failure json response' do + get :index, token: token, format: :json + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be false + expect(json_response['message']).to include('The server is on fire') + end + + it 'supports failure xml response' do + get :index, token: token, format: :xml + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/xml' + expect(xml_response['healthy']).to be false + expect(xml_response['message']).to include('The server is on fire') + end + + it 'supports failure responses for specific checks' do + get :index, token: token, checks: 'email', format: :json + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be false + expect(json_response['message']).to include('Email is on fire') + end + end + end +end diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb new file mode 100644 index 0000000000..4fde04b609 --- /dev/null +++ b/spec/features/admin/admin_health_check_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +feature "Admin Health Check", feature: true do + include WaitForAjax + + before do + login_as :admin + end + + describe '#show' do + before do + visit admin_health_check_path + end + + it { page.has_text? 'Health Check' } + it { page.has_text? 'Health information can be reteived' } + + it 'has a health check access token' do + token = current_application_settings.health_check_access_token + expect(page).to have_content("Access token is #{token}") + expect(page).to have_selector('#health-check-token', text: token) + end + + describe 'reload access token', js: true do + it 'changes the access token' do + orig_token = current_application_settings.health_check_access_token + click_button 'Reset health check access token' + wait_for_ajax + expect(find('#health-check-token').text).not_to eq orig_token + end + end + end + + context 'when services are up' do + before do + visit admin_health_check_path + end + + it 'shows healthy status' do + expect(page).to have_content('Current Status: Healthy') + end + end + + context 'when a service is down' do + before do + allow(HealthCheck::Utils).to receive(:process_checks).and_return('The server is on fire') + visit admin_health_check_path + end + + it 'shows unhealthy status' do + expect(page).to have_content('Current Status: Unhealthy') + expect(page).to have_content('The server is on fire') + end + end +end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index cd16a8e632..b5ed8584c8 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -118,3 +118,10 @@ describe Admin::DashboardController, "routing" do expect(get("/admin")).to route_to('admin/dashboard#index') end end + +# admin_health_check GET /admin/health_check(.:format) admin/health_check#show +describe Admin::HealthCheckController, "routing" do + it "to #show" do + expect(get("/admin/health_check")).to route_to('admin/health_check#show') + end +end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1527eddfa4..e4dfd4bca3 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -243,3 +243,13 @@ describe "Groups", "routing" do expect(get('/1')).to route_to('namespaces#show', id: '1') end end + +describe HealthCheckController, 'routing' do + it 'to #index' do + expect(get('/health_check')).to route_to('health_check#index') + end + + it 'also supports passing checks in the url' do + expect(get('/health_check/email')).to route_to('health_check#index', checks: 'email') + end +end From b42f39d2c27ec2a87e21dea7414d9845f2512144 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 28 Apr 2016 19:44:56 -0600 Subject: [PATCH 044/199] Remove activerecord-deprecated_finders gem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t use any of the deprecated finders, so it should be safe to remove. Resolves #17015. --- Gemfile | 1 - Gemfile.lock | 2 -- 2 files changed, 3 deletions(-) diff --git a/Gemfile b/Gemfile index 77d351419f..8de9602510 100644 --- a/Gemfile +++ b/Gemfile @@ -324,7 +324,6 @@ gem "mail_room", "~> 0.7" gem 'email_reply_parser', '~> 0.5.8' ## CI -gem 'activerecord-deprecated_finders', '~> 1.0.3' gem 'activerecord-session_store', '~> 0.1.0' gem "nested_form", '~> 0.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index c02698bcca..45d6de18f2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,7 +33,6 @@ GEM activemodel (= 4.2.6) activesupport (= 4.2.6) arel (~> 6.0) - activerecord-deprecated_finders (1.0.4) activerecord-session_store (0.1.2) actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) @@ -883,7 +882,6 @@ PLATFORMS DEPENDENCIES RedCloth (~> 4.2.9) ace-rails-ap (~> 4.0.2) - activerecord-deprecated_finders (~> 1.0.3) activerecord-session_store (~> 0.1.0) acts-as-taggable-on (~> 3.4) addressable (~> 2.3.8) From d9574a7b93de7e88c77ed1351650843749da60f0 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 5 May 2016 10:00:31 +0100 Subject: [PATCH 045/199] Group commits by date in server timezone `Time#to_date` just takes the (timezone-less) year, date, and month, and creates a new date from that. Because the commits in the list are grouped by date, rather than chunked when the date changes, a commit can be shown in the wrong order if its CommitDate has a timezone-less date that's different to other commits around it. Convert all CommitDates to the server timezone before grouping, as that will at least produce consistent results. Users can still see a timestamp on the commit that doesn't match the date it's grouped under, because the timestamp shown uses the user's local timezone, and the grouping uses the server's timezone, but that was an issue anyway. --- CHANGELOG | 1 + app/views/projects/commits/_commits.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e989e622b9..ab41545548 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ v 8.8.0 (unreleased) - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) - Added multiple colors for labels in dropdowns when dups happen. + - Always group commits by server timezone, not commit timestamp - Improve description for the Two-factor Authentication sign-in screen. (Connor Shea) - API support for the 'since' and 'until' operators on commit requests (Paco Guzman) - Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko) diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 64e8da9201..82f39e5928 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -3,7 +3,7 @@ - commits, hidden = limited_commits(@commits) -- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| +- commits.group_by { |c| c.committed_date.in_time_zone.to_date }.sort.reverse.each do |day, commits| .row.commits-row .col-md-2.hidden-xs.hidden-sm %h5.commits-row-date From 68aca6f6635090a33cf4d96c6dbc7ceab6ac2385 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 11 May 2016 11:37:49 +0200 Subject: [PATCH 046/199] trying to fix timing issue with import status --- app/controllers/projects/imports_controller.rb | 5 ++++- app/models/project.rb | 5 ----- app/services/projects/create_service.rb | 6 ++++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 7756f0f0ed..c70b9bf2f5 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -19,7 +19,10 @@ class Projects::ImportsController < Projects::ApplicationController if @project.import_failed? @project.import_retry else - @project.import_start + Project.transaction do + @project.import_start + end + @project.add_import_job if @project.persisted? end end diff --git a/app/models/project.rb b/app/models/project.rb index 05e14185a3..d807802ce1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -246,7 +246,6 @@ class Project < ActiveRecord::Base state :finished state :failed - after_transition any => :started, do: :schedule_add_import_job after_transition any => :finished, do: :clear_import_data end @@ -385,10 +384,6 @@ class Project < ActiveRecord::Base id && persisted? end - def schedule_add_import_job - run_after_commit(:add_import_job) - end - def add_import_job if forked? job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index a77652480b..53a16e95f5 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -59,6 +59,10 @@ module Projects end end + Project.transaction do + @project.import_start if @project.import? + end + after_create_actions if @project.persisted? @project @@ -95,8 +99,6 @@ module Projects unless @project.group @project.team << [current_user, :master, current_user] end - - @project.import_start if @project.import? end end end From a2934f45317ed64beb948b8487199251a987c4a1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 11 May 2016 12:39:07 +0200 Subject: [PATCH 047/199] Use % notation for arrays in runner model constants --- app/models/ci/runner.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index d27963e8ec..e598a13286 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -22,8 +22,8 @@ module Ci extend Ci::Model LAST_CONTACT_TIME = 5.minutes.ago - AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] - FORM_EDITABLE = [:description, :tag_list, :active] + AVAILABLE_SCOPES = %w[specific shared active paused online] + FORM_EDITABLE = %i[description tag_list active] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' From 5c59ba35c9ae6a7e5d6d6cd20d0b2ba51166f60f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 11 May 2016 15:08:27 +0200 Subject: [PATCH 048/199] fix complexity of method --- app/services/projects/create_service.rb | 30 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 53a16e95f5..1693ae609c 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -50,18 +50,9 @@ module Projects @project.build_forked_project_link(forked_from_project_id: forked_from_project_id) end - Project.transaction do - @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data - @project.save + process_import_data(import_data) - if @project.persisted? && !@project.import? - raise 'Failed to create repository' unless @project.create_repository - end - end - - Project.transaction do - @project.import_start if @project.import? - end + start_import if @project.import? after_create_actions if @project.persisted? @@ -100,5 +91,22 @@ module Projects @project.team << [current_user, :master, current_user] end end + + def start_import + Project.transaction do + @project.import_start + end + end + + def process_import_data(import_data) + Project.transaction do + @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data + @project.save + + if @project.persisted? && !@project.import? + raise 'Failed to create repository' unless @project.create_repository + end + end + end end end From 973cd0fec7b69d1398a86df67d10f7c38a9a4ced Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 11 May 2016 18:08:17 +0300 Subject: [PATCH 049/199] Update 8.8 guides --- doc/install/installation.md | 2 +- doc/update/8.7-to-8.8.md | 154 ++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 doc/update/8.7-to-8.8.md diff --git a/doc/install/installation.md b/doc/install/installation.md index e3af302226..44ae0be406 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -269,7 +269,7 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-7-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-8-stable gitlab **Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md new file mode 100644 index 0000000000..b4d9212289 --- /dev/null +++ b/doc/update/8.7-to-8.8.md @@ -0,0 +1,154 @@ +# From 8.7 to 8.8 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-8-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v2.7.2 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.1 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### Git configuration + +Disable `git gc --auto` because GitLab runs `git gc` for us already. + +```sh +sudo -u git -H git config --global gc.auto 0 +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-7-stable:lib/support/nginx/gitlab-ssl origin/8-8-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-7-stable:lib/support/nginx/gitlab origin/8-8-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.6) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.6 to 8.7](8.6-to-8.7.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. From b523dec83dc832029c6e40daaeb121920bbcd394 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 14:11:28 -0400 Subject: [PATCH 050/199] Remove unused `icon_for_event` helper --- app/helpers/events_helper.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 592bad8ba2..0bf328e7d1 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -39,15 +39,6 @@ module EventsHelper end end - def icon_for_event - { - EventFilter.push => 'upload', - EventFilter.merged => 'check-square-o', - EventFilter.comments => 'comments', - EventFilter.team => 'user', - } - end - def event_preposition(event) if event.push? || event.commented? || event.target "at" From c8f23bd2edc19f968446b149120df1f7798eb4b1 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 11 May 2016 17:27:08 -0700 Subject: [PATCH 051/199] Support token header for health check token, and general cleanup of the health_check feature. --- app/controllers/health_check_controller.rb | 15 ++++++-- app/views/admin/health_check/show.html.haml | 36 +++++++++++-------- config/initializers/health_check.rb | 2 +- config/routes.rb | 2 +- .../health_check_controller_spec.rb | 15 ++++++++ 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb index b974489836..037da7d2bc 100644 --- a/app/controllers/health_check_controller.rb +++ b/app/controllers/health_check_controller.rb @@ -1,13 +1,22 @@ class HealthCheckController < HealthCheck::HealthCheckController before_action :validate_health_check_access! - protected + private def validate_health_check_access! - return render_404 unless params[:token].presence && params[:token] == current_application_settings.health_check_access_token + render_404 unless token_valid? + end + + def token_valid? + token = params[:token].presence || request.headers['TOKEN'] + token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare( + token, + current_application_settings.health_check_access_token + ) end def render_404 - render file: Rails.root.join("public", "404"), layout: false, status: "404" + render file: Rails.root.join('public', '404'), layout: false, status: '404' end end diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 23a931995a..ed7025f7a0 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -2,29 +2,35 @@ %h3.page-title Health Check -%p.light +.bs-callout.clearfix + .pull-left + %p Access token is - %code{ id:'health-check-token' }= "#{current_application_settings.health_check_access_token}" + %code#health-check-token= current_application_settings.health_check_access_token + = button_to reset_health_check_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset the health check token?' } do + = icon('refresh') + Reset health check access token %p.light Health information can be reteived as plain text, json, or xml using: %ul %li - %code= "/health_check?token=#{current_application_settings.health_check_access_token}" + %code= health_check_url(token:current_application_settings.health_check_access_token) %li - %code= "/health_check.json?token=#{current_application_settings.health_check_access_token}" + %code= health_check_url(token:current_application_settings.health_check_access_token, format: :json) %li - %code= "/health_check.xml?token=#{current_application_settings.health_check_access_token}" + %code= health_check_url(token:current_application_settings.health_check_access_token, format: :xml) -.bs-callout.clearfix - .pull-left - %p - You can reset the health check access token by pressing the button below. - %p - = button_to reset_health_check_token_admin_application_settings_path, - method: :put, class: 'btn btn-default', - data: { confirm: 'Are you sure you want to reset the health check token?' } do - = icon('refresh') - Reset health check access token +%p.light + You can also ask for the status of specific services: + %ul + %li + %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :cache) + %li + %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :database) + %li + %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :migrations) %hr .panel.panel-default diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb index a93e427303..79e2d23ab2 100644 --- a/config/initializers/health_check.rb +++ b/config/initializers/health_check.rb @@ -1,3 +1,3 @@ HealthCheck.setup do |config| - config.standard_checks = [ 'database', 'migrations', 'cache' ] + config.standard_checks = ['database', 'migrations', 'cache'] end diff --git a/config/routes.rb b/config/routes.rb index c81bf294a5..f794a881f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,7 +74,7 @@ Rails.application.routes.draw do end # Health check - get 'health_check(/:checks)(.:format)' => 'health_check#index' + get 'health_check(/:checks)' => 'health_check#index', as: :health_check # Enable Grack support mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb index 3b9cc5c98f..0d8a68bb51 100644 --- a/spec/controllers/health_check_controller_spec.rb +++ b/spec/controllers/health_check_controller_spec.rb @@ -14,6 +14,13 @@ describe HealthCheckController do end context 'when services are up and an access token is provided' do + it 'supports passing the token in the header' do + request.headers['TOKEN'] = token + get :index + expect(response).to be_success + expect(response.content_type).to eq 'text/plain' + end + it 'supports successful plaintest response' do get :index, token: token expect(response).to be_success @@ -55,6 +62,14 @@ describe HealthCheckController do 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 + request.headers['TOKEN'] = token + get :index + expect(response.status).to eq(500) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to include('The server is on fire') + end + it 'supports failure plaintest response' do get :index, token: token expect(response.status).to eq(500) From a59ad3936a0bdbfd64d9c54af631a272317fe680 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 5 May 2016 15:38:01 +0530 Subject: [PATCH 052/199] Add a spec for `WikiLinkFilter` - And fix behavior for non-file hierarchical links. --- lib/banzai/filter/wiki_link_filter.rb | 11 ++- .../banzai/filter/wiki_link_filter_spec.rb | 77 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 spec/lib/banzai/filter/wiki_link_filter_spec.rb diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb index 06d10c9850..7dc771afd7 100644 --- a/lib/banzai/filter/wiki_link_filter.rb +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -25,7 +25,7 @@ module Banzai end def process_link_attr(html_attr) - return if html_attr.blank? || file_reference?(html_attr) + return if html_attr.blank? || file_reference?(html_attr) || hierarchical_link?(html_attr) uri = URI(html_attr.value) if uri.relative? && uri.path.present? @@ -40,12 +40,17 @@ module Banzai uri end + def project_wiki + context[:project_wiki] + end + def file_reference?(html_attr) !File.extname(html_attr.value).blank? end - def project_wiki - context[:project_wiki] + # Of the form `./link`, `../link`, or similar + def hierarchical_link?(html_attr) + html_attr.value[0] == '.' end def project_wiki_base_path diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb new file mode 100644 index 0000000000..56b1a26739 --- /dev/null +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Banzai::Filter::WikiLinkFilter, lib: true do + include FilterSpecHelper + + let(:namespace) { build(:namespace, name: "wiki_link_ns") } + let(:project) { build(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } + let(:user) { double } + let(:project_wiki) { ProjectWiki.new(project, user) } + + describe "links within the wiki (relative)" do + describe "hierarchical links to the current directory" do + it "doesn't rewrite non-file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./page.md') + end + end + + describe "hierarchical links to the parent directory" do + it "doesn't rewrite non-file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('../page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('../page.md') + end + end + + describe "hierarchical links to a sub-directory" do + it "doesn't rewrite non-file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page.md') + end + end + + describe "non-hierarchical links" do + it 'rewrites non-file links to be at the scope of the wiki root' do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to match('/wiki_link_ns/wiki_link_project/wikis/page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('page.md') + end + end + end + + describe "links outside the wiki (absolute)" do + it "doesn't rewrite links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('http://example.com/page') + end + end +end From a4ee7d25e398f9d2e2311703fbbcf5f6b9bdf728 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 9 May 2016 16:04:05 +0530 Subject: [PATCH 053/199] Implement @rymai's feedback after review. - Separate 'exercise' and 'verify' steps of tests. - Use `build_stubbed` instead of `build` --- spec/lib/banzai/filter/wiki_link_filter_spec.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb index 56b1a26739..185abbb210 100644 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Banzai::Filter::WikiLinkFilter, lib: true do include FilterSpecHelper - let(:namespace) { build(:namespace, name: "wiki_link_ns") } - let(:project) { build(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } + let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") } + let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } let(:user) { double } let(:project_wiki) { ProjectWiki.new(project, user) } @@ -13,12 +13,14 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite non-file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./page') end it "doesn't rewrite file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./page.md') end end @@ -27,12 +29,14 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite non-file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('../page') end it "doesn't rewrite file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('../page.md') end end @@ -41,12 +45,14 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite non-file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page') end it "doesn't rewrite file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page.md') end end @@ -62,6 +68,7 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('page.md') end end @@ -71,6 +78,7 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('http://example.com/page') end end From 555610b619b552ca058a7656f373743db4d34765 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 10 May 2016 09:04:14 +0530 Subject: [PATCH 054/199] Add CHANGELOG entry. --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d5ab79602f..187011c601 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,9 @@ v 8.8.0 (unreleased) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 +v 8.7.5 + - Fix relative links in wiki pages. !4050 + v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) - Fix setting trusted proxies !3970 From ef7f793dc4f0425e1fd5e358c162f2b08a160142 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 10:12:29 +0200 Subject: [PATCH 055/199] fix silly typo --- app/services/projects/create_service.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 1693ae609c..c61a515a6b 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -56,6 +56,8 @@ module Projects after_create_actions if @project.persisted? + @project.add_import_job if @project.import? + @project rescue => e message = "Unable to save project: #{e.message}" From 7db3fb5a0b6c07b65c61c920e18f056f6c127932 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:05:34 +0300 Subject: [PATCH 056/199] disabled_oauth_sign_in_sources column --- ...disabled_oauth_sign_in_sources_to_application_settings.rb | 5 +++++ db/schema.rb | 1 + 2 files changed, 6 insertions(+) create mode 100644 db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb diff --git a/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb new file mode 100644 index 0000000000..facd33875b --- /dev/null +++ b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddDisabledOauthSignInSourcesToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :disabled_oauth_sign_in_sources, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 71d953afe3..74facd1208 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -80,6 +80,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.boolean "repository_checks_enabled", default: false t.text "shared_runners_text" t.integer "metrics_packet_size", default: 1 + t.text "disabled_oauth_sign_in_sources" end create_table "audit_events", force: :cascade do |t| From ca288587256b8995105b601ef02850f388d59ff9 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:06:18 +0300 Subject: [PATCH 057/199] serialize the disabled_oauth_sign_in_sources --- app/models/application_setting.rb | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 7039db2d41..39f74b2082 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,3 +1,52 @@ +# == Schema Information +# +# Table name: application_settings +# +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) +# max_attachment_size :integer default(10), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# default_group_visibility :integer +# restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null +# import_sources :text +# disabled_oauth_sign_in_sources :text +# help_page_text :text +# admin_notification_email :string(255) +# shared_runners_enabled :boolean default(TRUE), not null +# max_artifacts_size :integer default(100), not null +# runners_registration_token :string +# require_two_factor_authentication :boolean default(FALSE) +# two_factor_grace_period :integer default(48) +# metrics_enabled :boolean default(FALSE) +# metrics_host :string default("localhost") +# metrics_username :string +# metrics_password :string +# metrics_pool_size :integer default(16) +# metrics_timeout :integer default(10) +# metrics_method_call_threshold :integer default(10) +# recaptcha_enabled :boolean default(FALSE) +# recaptcha_site_key :string +# recaptcha_private_key :string +# metrics_port :integer default(8089) +# sentry_enabled :boolean default(FALSE) +# sentry_dsn :string +# email_author_in_body :boolean default(FALSE) +# + class ApplicationSetting < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :runners_registration_token @@ -6,6 +55,7 @@ class ApplicationSetting < ActiveRecord::Base serialize :restricted_visibility_levels serialize :import_sources + serialize :disabled_oauth_sign_in_sources serialize :restricted_signup_domains, Array attr_accessor :restricted_signup_domains_raw From e5ddd6d21eb6125605d3f47ea5db884b54514796 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:08:06 +0300 Subject: [PATCH 058/199] disabled_oauth_sign_in_sources parameter handling in ApplicationSettingsController --- .../admin/application_settings_controller.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ec22548dde..cebc01a2af 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -53,6 +53,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end + disabled_oauth_sign_in_sources = params[:application_setting][:disabled_oauth_sign_in_sources] + if disabled_oauth_sign_in_sources.nil? + params[:application_setting][:disabled_oauth_sign_in_sources] = [] + else + disabled_oauth_sign_in_sources.map! do |source| + source.to_str + end + end + params.require(:application_setting).permit( :default_projects_limit, :default_branch_protection, @@ -95,7 +104,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :repository_checks_enabled, :metrics_packet_size, restricted_visibility_levels: [], - import_sources: [] + import_sources: [], + disabled_oauth_sign_in_sources: [] ) end end From d8085d8e128e4ef50443f7a58bfbe7bf21565ab0 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:32:33 +0300 Subject: [PATCH 059/199] default value for disabled_oauth_sign_in_sources in create_from_defaults @ ApplicationSetting --- app/models/application_setting.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 39f74b2082..a48deccb02 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -157,6 +157,7 @@ class ApplicationSetting < ActiveRecord::Base recaptcha_enabled: false, akismet_enabled: false, repository_checks_enabled: true, + disabled_oauth_sign_in_sources: [] ) end From cf300443147f1a9ba3acf83ce839afdffa5173bd Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:33:33 +0300 Subject: [PATCH 060/199] OAuth Providers disable from the ApplicationSettings page --- app/helpers/application_settings_helper.rb | 15 +++++++++++++++ .../admin/application_settings/_form.html.haml | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 914b0ef604..17d7389987 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -60,4 +60,19 @@ module ApplicationSettingsHelper end end end + + def oauth_providers_checkboxes(help_block_id) + button_based_providers.map do |source| + checked = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) + css_class = 'btn' + css_class += ' active' if checked + checkbox_name = 'application_setting[disabled_oauth_sign_in_sources][]' + + label_tag(checkbox_name, class: css_class) do + check_box_tag(checkbox_name, source, checked, + autocomplete: 'off', + 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) + end + end + end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index e0d8d16a95..057b237c06 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -109,6 +109,16 @@ = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled + .form-group + = f.label :disable_oauth_signin_sources, class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - oauth_providers_checkboxes('oauth-providers-help').each do |source| + = source + %span.help-block#oauth-providers-help + Enabled OmniAuth must be configured for GitHub + = link_to "(?)", help_page_path("integration", "github") .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From 8d26836e945f3e66a46370905b9fb34244a0ff91 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:35:03 +0300 Subject: [PATCH 061/199] method to get the enabled_button_based_providers --- app/helpers/auth_helper.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index b4f80fd9b3..24eb813d0b 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -38,6 +38,12 @@ module AuthHelper auth_providers.reject { |provider| form_based_provider?(provider) } end + def enabled_button_based_providers + disabled_providers = current_application_settings.disabled_oauth_sign_in_sources || [] + + button_based_providers.map(&:to_s) - disabled_providers + end + def provider_image_tag(provider, size = 64) label = label_for_provider(provider) From d943e5f691ad9c59e349eab95f1a21bd0f85ab1b Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:36:15 +0300 Subject: [PATCH 062/199] method to check if oauth button based providers are enabled --- app/helpers/auth_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 24eb813d0b..3e1f420925 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -44,6 +44,10 @@ module AuthHelper button_based_providers.map(&:to_s) - disabled_providers end + def button_based_providers_enabled? + !enabled_button_based_providers.empty? + end + def provider_image_tag(provider, size = 64) label = label_for_provider(provider) From 284eccb5e68c863ea8e1001a24af33dd0da5e201 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:36:57 +0300 Subject: [PATCH 063/199] render the omniauth_box if there are any button_based_providers --- app/views/devise/sessions/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index d65fa60025..28194506ac 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -4,7 +4,7 @@ = render 'devise/shared/signin_box' -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box - - if omniauth_enabled? && devise_mapping.omniauthable? + - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled? .clearfix.prepend-top-20 = render 'devise/shared/omniauth_box' From 28da3a88de339a95d49e171da87494f65219cb21 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:37:18 +0300 Subject: [PATCH 064/199] show only the eanbled oauth providers --- app/views/devise/shared/_omniauth_box.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index ecf680e7b2..de18bc2d84 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,7 +1,7 @@ %p %span.light Sign in with   - - providers = button_based_providers + - providers = enabled_button_based_providers - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) From 7c7c5b7e40ff4fed64a9f62beed027064c292eaf Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 15:22:24 +0300 Subject: [PATCH 065/199] text for the disable_oauth_signin_sources label --- app/views/admin/application_settings/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 057b237c06..729c2ff563 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -110,7 +110,7 @@ = f.check_box :signin_enabled Sign-in enabled .form-group - = f.label :disable_oauth_signin_sources, class: 'control-label col-sm-2' + = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 - data_attrs = { toggle: 'buttons' } .btn-group{ data: data_attrs } From 47ee5125e881694b7713f187b48589a2f4bbd747 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 15:57:00 +0300 Subject: [PATCH 066/199] validate disabled_oauth_sign_in_sources in ApplicationSe --- app/models/application_setting.rb | 10 ++++++++++ spec/models/application_setting_spec.rb | 3 +++ 2 files changed, 13 insertions(+) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a48deccb02..d565dbc71d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -119,6 +119,16 @@ class ApplicationSetting < ActiveRecord::Base end end + validates_each :disabled_oauth_sign_in_sources do |record, attr, value| + unless value.nil? + value.each do |source| + unless Devise.omniauth_providers.include?(source.to_sym) + record.errors.add(attr, "'#{source}' is not an ouath sign-in source") + end + end + end + end + before_save :ensure_runners_registration_token after_commit do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 1ce22feed5..fb3ea491df 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -20,6 +20,9 @@ describe ApplicationSetting, models: true do it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } + it { is_expected.to allow_value([:github]).for(:disabled_oauth_sign_in_sources) } + it { is_expected.not_to allow_value([:test]).for(:disabled_oauth_sign_in_sources) } + it { is_expected.to validate_presence_of(:max_attachment_size) } it do From 8c2b72b1c80e8bbd082ac8e1aedb21a1aad07235 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 17:04:54 +0300 Subject: [PATCH 067/199] tests for enabled_button_based_providers helper method of AuthHelper --- spec/helpers/auth_helper_spec.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index e47a54fdac..5d66c92d26 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -2,7 +2,9 @@ require "spec_helper" describe AuthHelper do describe "button_based_providers" do - it 'returns all enabled providers' do + let(:settings) { ApplicationSetting.create_from_defaults } + + it 'returns all enabled providers from devise' do allow(helper).to receive(:auth_providers) { [:twitter, :github] } expect(helper.button_based_providers).to include(*[:twitter, :github]) end @@ -16,5 +18,23 @@ describe AuthHelper do allow(helper).to receive(:auth_providers) { [] } expect(helper.button_based_providers).to eq([]) end + + it 'returns all the enabled providers from settings' do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) + end + + it 'should not return github as provider because it\'s disabled from settings' do + settings.update_attribute( + :disabled_oauth_sign_in_sources, + ['github'] + ) + + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + allow(helper).to receive(:current_application_settings) { settings } + + expect(helper.enabled_button_based_providers).to include('twitter') + expect(helper.enabled_button_based_providers).to_not include('github') + end end end From 6f69f6a1ab70bd2252803a64927d0c8df1d03612 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 17:06:07 +0300 Subject: [PATCH 068/199] tests for button_based_providers_enabled? helper method of AuthHelper --- spec/helpers/auth_helper_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 5d66c92d26..a6a366bc19 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -36,5 +36,23 @@ describe AuthHelper do expect(helper.enabled_button_based_providers).to include('twitter') expect(helper.enabled_button_based_providers).to_not include('github') end + + it 'returns true for button_based_providers_enabled? because there providers' do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + + expect(helper.button_based_providers_enabled?).to be true + end + + it 'returns false for button_based_providers_enabled? because there providers' do + settings.update_attribute( + :disabled_oauth_sign_in_sources, + ['github', 'twitter'] + ) + + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + allow(helper).to receive(:current_application_settings) { settings } + + expect(helper.button_based_providers_enabled?).to be false + end end end From deca3da1a7f4e59c97c27bedf7aa26185a4d883b Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 5 May 2016 10:34:51 +0300 Subject: [PATCH 069/199] stub Devise.omniauth_providers to return GitHub even if the gitlab.yml has no omniauth provider enabled This will fix failing tests in case gitlab.yml file has no omniauth providers enabled --- spec/models/application_setting_spec.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index fb3ea491df..d84f3e998f 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -20,8 +20,14 @@ describe ApplicationSetting, models: true do it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } - it { is_expected.to allow_value([:github]).for(:disabled_oauth_sign_in_sources) } - it { is_expected.not_to allow_value([:test]).for(:disabled_oauth_sign_in_sources) } + describe 'disabled_oauth_sign_in_sources validations' do + before do + allow(Devise).to receive(:omniauth_providers).and_return([:github]) + end + + it { is_expected.to allow_value(['github']).for(:disabled_oauth_sign_in_sources) } + it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) } + end it { is_expected.to validate_presence_of(:max_attachment_size) } From 2e4c914ae88b77c8a3871f8415163a51e23254e5 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 10:31:25 +0300 Subject: [PATCH 070/199] between "" --- spec/helpers/auth_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index a6a366bc19..6dc1135927 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -24,7 +24,7 @@ describe AuthHelper do expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) end - it 'should not return github as provider because it\'s disabled from settings' do + it "should not return github as provider because it's disabled from settings" do settings.update_attribute( :disabled_oauth_sign_in_sources, ['github'] From fc88527c9e21f99a9411423f1c8a6475957c02f3 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 10:40:27 +0300 Subject: [PATCH 071/199] use stub_application_setting instead --- spec/helpers/auth_helper_spec.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 6dc1135927..e6af074a78 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -2,8 +2,6 @@ require "spec_helper" describe AuthHelper do describe "button_based_providers" do - let(:settings) { ApplicationSetting.create_from_defaults } - it 'returns all enabled providers from devise' do allow(helper).to receive(:auth_providers) { [:twitter, :github] } expect(helper.button_based_providers).to include(*[:twitter, :github]) @@ -25,13 +23,11 @@ describe AuthHelper do end it "should not return github as provider because it's disabled from settings" do - settings.update_attribute( - :disabled_oauth_sign_in_sources, - ['github'] + stub_application_setting( + disabled_oauth_sign_in_sources: ['github'] ) allow(helper).to receive(:auth_providers) { [:twitter, :github] } - allow(helper).to receive(:current_application_settings) { settings } expect(helper.enabled_button_based_providers).to include('twitter') expect(helper.enabled_button_based_providers).to_not include('github') @@ -44,13 +40,11 @@ describe AuthHelper do end it 'returns false for button_based_providers_enabled? because there providers' do - settings.update_attribute( - :disabled_oauth_sign_in_sources, - ['github', 'twitter'] + stub_application_setting( + disabled_oauth_sign_in_sources: ['github', 'twitter'] ) allow(helper).to receive(:auth_providers) { [:twitter, :github] } - allow(helper).to receive(:current_application_settings) { settings } expect(helper.button_based_providers_enabled?).to be false end From 3922e7ee2249bb5a683b7474b281d8e83d465740 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 10:42:57 +0300 Subject: [PATCH 072/199] enabled_button_based_providers into their own describe section --- spec/helpers/auth_helper_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index e6af074a78..04b5128279 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -16,7 +16,9 @@ describe AuthHelper do allow(helper).to receive(:auth_providers) { [] } expect(helper.button_based_providers).to eq([]) end + end + describe 'enabled_button_based_providers' do it 'returns all the enabled providers from settings' do allow(helper).to receive(:auth_providers) { [:twitter, :github] } expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) From 038dbb6803948e050484ed3f34c99b2f3bf7a6c5 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 10:51:24 +0300 Subject: [PATCH 073/199] DRYing enabled_button_based_providers tests --- spec/helpers/auth_helper_spec.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 04b5128279..a6df820421 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -19,8 +19,11 @@ describe AuthHelper do end describe 'enabled_button_based_providers' do - it 'returns all the enabled providers from settings' do + before do allow(helper).to receive(:auth_providers) { [:twitter, :github] } + end + + it 'returns all the enabled providers from settings' do expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) end @@ -29,15 +32,11 @@ describe AuthHelper do disabled_oauth_sign_in_sources: ['github'] ) - allow(helper).to receive(:auth_providers) { [:twitter, :github] } - expect(helper.enabled_button_based_providers).to include('twitter') expect(helper.enabled_button_based_providers).to_not include('github') end it 'returns true for button_based_providers_enabled? because there providers' do - allow(helper).to receive(:auth_providers) { [:twitter, :github] } - expect(helper.button_based_providers_enabled?).to be true end @@ -46,8 +45,6 @@ describe AuthHelper do disabled_oauth_sign_in_sources: ['github', 'twitter'] ) - allow(helper).to receive(:auth_providers) { [:twitter, :github] } - expect(helper.button_based_providers_enabled?).to be false end end From 3c3bc93747ebe5864a50d7b8203b357a40012ed4 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 19:32:01 +0300 Subject: [PATCH 074/199] if omniauth is enabled and if there are button_based_providers available show the disable OAuth sources --- .../application_settings/_form.html.haml | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 729c2ff563..81c232b7e9 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -109,16 +109,17 @@ = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled - .form-group - = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' - .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } - - oauth_providers_checkboxes('oauth-providers-help').each do |source| - = source - %span.help-block#oauth-providers-help - Enabled OmniAuth must be configured for GitHub - = link_to "(?)", help_page_path("integration", "github") + - if omniauth_enabled? && !button_based_providers.empty? + .form-group + = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - oauth_providers_checkboxes('oauth-providers-help').each do |source| + = source + %span.help-block#oauth-providers-help + Enabled OmniAuth must be configured for GitHub + = link_to "(?)", help_page_path("integration", "github") .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From dad501a2367a35f6d791efac9002e487176f2c9e Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 19:34:44 +0300 Subject: [PATCH 075/199] no need to show the help about Github or the other authentication sources --- app/views/admin/application_settings/_form.html.haml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 81c232b7e9..99f86e5244 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -117,9 +117,6 @@ .btn-group{ data: data_attrs } - oauth_providers_checkboxes('oauth-providers-help').each do |source| = source - %span.help-block#oauth-providers-help - Enabled OmniAuth must be configured for GitHub - = link_to "(?)", help_page_path("integration", "github") .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From a2d4c349b5772231d7428cf32ac8a66d70beb627 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 19:38:07 +0300 Subject: [PATCH 076/199] unindent 1 level the Disable OAuth form --- .../admin/application_settings/_form.html.haml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 99f86e5244..383f206079 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -109,14 +109,14 @@ = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled - - if omniauth_enabled? && !button_based_providers.empty? - .form-group - = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' - .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } - - oauth_providers_checkboxes('oauth-providers-help').each do |source| - = source + - if omniauth_enabled? && !button_based_providers.empty? + .form-group + = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - oauth_providers_checkboxes('oauth-providers-help').each do |source| + = source .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From 81d0a4a47390f3810b03528182d3eea68ab46112 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 10:21:14 +0300 Subject: [PATCH 077/199] typo --- app/models/application_setting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index d565dbc71d..667b45335b 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -123,7 +123,7 @@ class ApplicationSetting < ActiveRecord::Base unless value.nil? value.each do |source| unless Devise.omniauth_providers.include?(source.to_sym) - record.errors.add(attr, "'#{source}' is not an ouath sign-in source") + record.errors.add(attr, "'#{source}' is not an OAuth sign-in source") end end end From 96122034cfc2eb7039ae75b20b729f35e9aa832e Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 11:17:37 +0300 Subject: [PATCH 078/199] more readable specs for enabled_button_based_providers and button_based_providers_enabled? --- spec/helpers/auth_helper_spec.rb | 44 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index a6df820421..16fbb5dcec 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -23,29 +23,43 @@ describe AuthHelper do allow(helper).to receive(:auth_providers) { [:twitter, :github] } end - it 'returns all the enabled providers from settings' do - expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) + context 'all providers are enabled to sign in' do + it 'returns all the enabled providers from settings' do + expect(helper.enabled_button_based_providers).to include('twitter', 'github') + end end - it "should not return github as provider because it's disabled from settings" do - stub_application_setting( - disabled_oauth_sign_in_sources: ['github'] - ) + context 'GitHub OAuth sign in is disabled from application setting' do + it "doesn't return github as provider" do + stub_application_setting( + disabled_oauth_sign_in_sources: ['github'] + ) - expect(helper.enabled_button_based_providers).to include('twitter') - expect(helper.enabled_button_based_providers).to_not include('github') + expect(helper.enabled_button_based_providers).to include('twitter') + expect(helper.enabled_button_based_providers).to_not include('github') + end + end + end + + describe 'button_based_providers_enabled?' do + before do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } end - it 'returns true for button_based_providers_enabled? because there providers' do - expect(helper.button_based_providers_enabled?).to be true + context 'button based providers enabled' do + it 'returns true' do + expect(helper.button_based_providers_enabled?).to be true + end end - it 'returns false for button_based_providers_enabled? because there providers' do - stub_application_setting( - disabled_oauth_sign_in_sources: ['github', 'twitter'] - ) + context 'all the button based providers are disabled via application_setting' do + it 'returns false' do + stub_application_setting( + disabled_oauth_sign_in_sources: ['github', 'twitter'] + ) - expect(helper.button_based_providers_enabled?).to be false + expect(helper.button_based_providers_enabled?).to be false + end end end end From 7818f7329a436d86b30dff003b60ab8a15a4f0db Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 11:29:19 +0300 Subject: [PATCH 079/199] On Application Settings Page let the user select the enabled OAuth Sign in sources instead of the disabled ones --- .../admin/application_settings_controller.rb | 16 ++++++++-------- app/helpers/application_settings_helper.rb | 8 ++++---- .../admin/application_settings/_form.html.haml | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index cebc01a2af..d7a052700a 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -53,14 +53,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end - disabled_oauth_sign_in_sources = params[:application_setting][:disabled_oauth_sign_in_sources] - if disabled_oauth_sign_in_sources.nil? - params[:application_setting][:disabled_oauth_sign_in_sources] = [] - else - disabled_oauth_sign_in_sources.map! do |source| - source.to_str - end - end + enabled_oauth_sign_in_sources = params[:application_setting][:enabled_oauth_sign_in_sources] + + params[:application_setting][:disabled_oauth_sign_in_sources] = + AuthHelper.button_based_providers.map(&:to_s) - + (enabled_oauth_sign_in_sources.nil? ? [] : enabled_oauth_sign_in_sources) + + params[:application_setting].delete(:enabled_oauth_sign_in_sources) params.require(:application_setting).permit( :default_projects_limit, @@ -105,6 +104,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_packet_size, restricted_visibility_levels: [], import_sources: [], + enabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [] ) end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 17d7389987..ced8e16de7 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -63,13 +63,13 @@ module ApplicationSettingsHelper def oauth_providers_checkboxes(help_block_id) button_based_providers.map do |source| - checked = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) + disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) css_class = 'btn' - css_class += ' active' if checked - checkbox_name = 'application_setting[disabled_oauth_sign_in_sources][]' + css_class += ' active' unless disabled + checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' label_tag(checkbox_name, class: css_class) do - check_box_tag(checkbox_name, source, checked, + check_box_tag(checkbox_name, source, !disabled, autocomplete: 'off', 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 383f206079..ecd032acfd 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -111,7 +111,7 @@ Sign-in enabled - if omniauth_enabled? && !button_based_providers.empty? .form-group - = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' + = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 - data_attrs = { toggle: 'buttons' } .btn-group{ data: data_attrs } From bc099d799ea502c9fd8f1a09f07af70cdcf51156 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 11:50:53 +0300 Subject: [PATCH 080/199] Allow Admins to remove the Login with buttons for OAuth services and still be able to import on the changelog --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 187011c601..8d70ef9944 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -55,6 +55,8 @@ v 8.7.4 - Running rake gitlab:db:drop_tables now drops tables with cascade !4020 - Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100 - Use a case-insensitive comparison in sanitizing URI schemes + - Merge request widget displays TeamCity build state and code coverage correctly again. + - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034 v 8.7.3 - Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented From 3a91f5081eeeef8dca52a418aba2d786cbc8ddf4 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 11:53:22 +0300 Subject: [PATCH 081/199] documentation for enabling or disabling OAuth sign in --- .../img/enabled-oauth-sign-in-sources.png | Bin 0 -> 49081 bytes doc/integration/omniauth.md | 14 ++++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 doc/integration/img/enabled-oauth-sign-in-sources.png diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..95f8bbdcd2489c4eb14c100c92dffadf3a1a65fc GIT binary patch literal 49081 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 T3O4XXNYA9j6~w-Z7zO**Note:** +This setting was introduced with version 8.8 of GitLab + +Administrators are able to enable or disable Sign In via some OmniAuth providers. + +>**Note:** +By default Sign In is enabled via all the OAuth Providers that have been configured in config/gitlab.yml. + +In order to enable/disable an OmniAuth provider go to Admin Area -> Settings-> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. + +![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png) From 2a99c51590da3c430b27538a1f570d34209d51c7 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:39:27 +0300 Subject: [PATCH 082/199] corrections to the Enable or disable Sign In with OmniAuth --- doc/integration/omniauth.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index f0d7394005..f3e891fa45 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -194,14 +194,15 @@ While we can't officially support every possible authentication mechanism out th we'd like to at least help those with specific needs. ## Enable or disable Sign In with an OmniAuth provider without disabling import sources + >**Note:** -This setting was introduced with version 8.8 of GitLab +This setting was introduced with version 8.8 of GitLab. Administrators are able to enable or disable Sign In via some OmniAuth providers. >**Note:** By default Sign In is enabled via all the OAuth Providers that have been configured in config/gitlab.yml. -In order to enable/disable an OmniAuth provider go to Admin Area -> Settings-> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. +In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. ![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png) From 4372043392d735db506f1e696c213c3ece47bc1a Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:40:20 +0300 Subject: [PATCH 083/199] credits for !4034 --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8d70ef9944..7cd8f63321 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,7 @@ v 8.8.0 (unreleased) - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 + - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034. (Andrei Gliga) v 8.7.5 - Fix relative links in wiki pages. !4050 @@ -55,8 +56,6 @@ v 8.7.4 - Running rake gitlab:db:drop_tables now drops tables with cascade !4020 - Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100 - Use a case-insensitive comparison in sanitizing URI schemes - - Merge request widget displays TeamCity build state and code coverage correctly again. - - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034 v 8.7.3 - Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented From 9bfa74fcfe79c244fe4fd7d074b87bb22ba7971f Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:43:54 +0300 Subject: [PATCH 084/199] enabled_oauth_sign_in_sources param doesn't need permit --- app/controllers/admin/application_settings_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index d7a052700a..a2dfed0b7a 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -104,7 +104,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_packet_size, restricted_visibility_levels: [], import_sources: [], - enabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [] ) end From ab7f9c9ae1281857bef655a1af64774c628befa1 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:45:39 +0300 Subject: [PATCH 085/199] Array(enabled_oauth_sign_in_sources) instead of the if --- app/controllers/admin/application_settings_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index a2dfed0b7a..c300560236 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -57,7 +57,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController params[:application_setting][:disabled_oauth_sign_in_sources] = AuthHelper.button_based_providers.map(&:to_s) - - (enabled_oauth_sign_in_sources.nil? ? [] : enabled_oauth_sign_in_sources) + Array(enabled_oauth_sign_in_sources) params[:application_setting].delete(:enabled_oauth_sign_in_sources) From 25ff3fd5bdd8645f45254aa52397f8d6b49386b5 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:46:40 +0300 Subject: [PATCH 086/199] button_based_providers.any? instead of !button_based_providers.empty? --- app/views/admin/application_settings/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index ecd032acfd..909339f7bc 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -109,7 +109,7 @@ = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled - - if omniauth_enabled? && !button_based_providers.empty? + - if omniauth_enabled? && button_based_providers.any? .form-group = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 From e87c96eef6d8d837bf4475e2681f4c5c5e40488d Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:48:08 +0300 Subject: [PATCH 087/199] enabled_button_based_providers.any? instead of ! empty? for button_based_providers_enabled? --- app/helpers/auth_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 3e1f420925..b05fa0a14d 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -45,7 +45,7 @@ module AuthHelper end def button_based_providers_enabled? - !enabled_button_based_providers.empty? + enabled_button_based_providers.any? end def provider_image_tag(provider, size = 64) From 46064cb84c579bd1657baffe24f5a515759bff67 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:50:03 +0300 Subject: [PATCH 088/199] << instead of += --- app/helpers/application_settings_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index ced8e16de7..878b1b254c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -65,7 +65,7 @@ module ApplicationSettingsHelper button_based_providers.map do |source| disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) css_class = 'btn' - css_class += ' active' unless disabled + css_class << ' active' unless disabled checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' label_tag(checkbox_name, class: css_class) do From 6bd8d3ad48b9151c0bcb69bfbce0bc2551cdf184 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:53:02 +0300 Subject: [PATCH 089/199] inline btn-group{ data: { toggle: 'buttons' } } --- app/views/admin/application_settings/_form.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 909339f7bc..576509e3f2 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -113,8 +113,7 @@ .form-group = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } + .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes('oauth-providers-help').each do |source| = source .form-group From 9079b56bf4f149054e347aa09ad151ab617ef123 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 11 May 2016 10:35:18 +0300 Subject: [PATCH 090/199] Revert "no need to show the help about Github or the other authentication sources" This reverts commit 78a832d0fd7ff445c707809134dbb2a42d2a9e03. Show help only for the enabled OAuth Providers on oauth-providers-help block aria: { describedby: help_block_id } instead of 'aria-describedby' => help_block_id provider and not p a more simple approach to the OmniAuth helper links for oauth-providers-help --- app/helpers/application_settings_helper.rb | 9 ++++++++- app/views/admin/application_settings/_form.html.haml | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 878b1b254c..82056a4c97 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -71,8 +71,15 @@ module ApplicationSettingsHelper label_tag(checkbox_name, class: css_class) do check_box_tag(checkbox_name, source, !disabled, autocomplete: 'off', - 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) + aria: { describedby: help_block_id }) + Gitlab::OAuth::Provider.label_for(source) end end end + + def oauth_providers_with_help_links + button_based_providers.map do |provider| + Gitlab::OAuth::Provider.label_for(provider) + ' ' + + link_to("(?)", help_page_path("integration", provider)) + end + end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 576509e3f2..f3681bc96e 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -116,6 +116,9 @@ .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes('oauth-providers-help').each do |source| = source + %span.help-block#oauth-providers-help + Enabled OmniAuth must be configured for + = oauth_providers_with_help_links.to_sentence.html_safe .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From 2576a55e35f06d2d59783867be5e7e2ccbed982e Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 11 May 2016 19:52:06 +0300 Subject: [PATCH 091/199] config/gitlab.yml between `` --- doc/integration/omniauth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index f3e891fa45..820f40f81a 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -201,7 +201,7 @@ This setting was introduced with version 8.8 of GitLab. Administrators are able to enable or disable Sign In via some OmniAuth providers. >**Note:** -By default Sign In is enabled via all the OAuth Providers that have been configured in config/gitlab.yml. +By default Sign In is enabled via all the OAuth Providers that have been configured in `config/gitlab.yml`. In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. From c0c2cd4ed72cda88e182f3eafb472687d9d075f1 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 11 May 2016 19:55:24 +0300 Subject: [PATCH 092/199] new screenshot for the docs on how to enabled/disable OmniAuth Sign In --- .../img/enabled-oauth-sign-in-sources.png | Bin 49081 -> 92674 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png index 95f8bbdcd2489c4eb14c100c92dffadf3a1a65fc..08155a4fade75af85f36fdbe8cacff1af936280c 100644 GIT binary patch literal 92674 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 n{ErgGcytqE7Zr6^h`m+mD8d?#n7A>v#VTY~gf5 literal 49081 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 T3O4XXNYA9j6~w-Z7zO Date: Wed, 11 May 2016 20:20:27 +0300 Subject: [PATCH 093/199] Revert "disabled_oauth_sign_in_sources column" and keep on schema only the disabled_oauth_sign_in_sources column This reverts commit 95358e0095403ad44149d11922d52e4590285a20. --- db/schema.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 74facd1208..b21cc16289 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -70,16 +70,16 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "recaptcha_site_key" t.string "recaptcha_private_key" t.integer "metrics_port", default: 8089 - t.boolean "akismet_enabled", default: false - t.string "akismet_api_key" t.integer "metrics_sample_interval", default: 15 t.boolean "sentry_enabled", default: false t.string "sentry_dsn" + t.boolean "akismet_enabled", default: false + t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" t.boolean "repository_checks_enabled", default: false - t.text "shared_runners_text" t.integer "metrics_packet_size", default: 1 + t.text "shared_runners_text" t.text "disabled_oauth_sign_in_sources" end @@ -427,10 +427,10 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "state" t.integer "iid" t.integer "updated_by_id" + t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" t.date "due_date" - t.integer "moved_to_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -717,8 +717,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.integer "project_id" t.text "data" t.text "encrypted_credentials" - t.string "encrypted_credentials_iv" - t.string "encrypted_credentials_salt" + t.text "encrypted_credentials_iv" + t.text "encrypted_credentials_salt" end create_table "projects", force: :cascade do |t| @@ -816,9 +816,9 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "active", null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true From 8313476f5e1f1b51b3857ba065a65a707f5ca8af Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 11 May 2016 23:39:07 +0300 Subject: [PATCH 094/199] no schema info for ApplicationSetting --- app/models/application_setting.rb | 49 ------------------------------- 1 file changed, 49 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 667b45335b..c143cf215e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,52 +1,3 @@ -# == Schema Information -# -# Table name: application_settings -# -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# default_group_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string(255) -# session_expire_delay :integer default(10080), not null -# import_sources :text -# disabled_oauth_sign_in_sources :text -# help_page_text :text -# admin_notification_email :string(255) -# shared_runners_enabled :boolean default(TRUE), not null -# max_artifacts_size :integer default(100), not null -# runners_registration_token :string -# require_two_factor_authentication :boolean default(FALSE) -# two_factor_grace_period :integer default(48) -# metrics_enabled :boolean default(FALSE) -# metrics_host :string default("localhost") -# metrics_username :string -# metrics_password :string -# metrics_pool_size :integer default(16) -# metrics_timeout :integer default(10) -# metrics_method_call_threshold :integer default(10) -# recaptcha_enabled :boolean default(FALSE) -# recaptcha_site_key :string -# recaptcha_private_key :string -# metrics_port :integer default(8089) -# sentry_enabled :boolean default(FALSE) -# sentry_dsn :string -# email_author_in_body :boolean default(FALSE) -# - class ApplicationSetting < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :runners_registration_token From b17cca1d9460a00510a2e9f04300f07cfb5eb95a Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 09:37:15 +0300 Subject: [PATCH 095/199] delete the enabled_oauth_sign_in_sources after getting it's value into enabled_oauth_sign_in_sources just delete enabled_oauth_sign_in_sources and get it's value --- app/controllers/admin/application_settings_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index c300560236..b9eb7ae792 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -53,14 +53,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end - enabled_oauth_sign_in_sources = params[:application_setting][:enabled_oauth_sign_in_sources] + enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources) params[:application_setting][:disabled_oauth_sign_in_sources] = AuthHelper.button_based_providers.map(&:to_s) - Array(enabled_oauth_sign_in_sources) - params[:application_setting].delete(:enabled_oauth_sign_in_sources) - params.require(:application_setting).permit( :default_projects_limit, :default_branch_protection, From 5553ad24e4fe42670ba0f4e2010819abac499e7d Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 13:37:44 +0300 Subject: [PATCH 096/199] Revert "new screenshot for the docs on how to enabled/disable OmniAuth Sign In" [ci skip] This reverts commit 7b295370efc124360d52e37e8e709914028f6443. --- .../img/enabled-oauth-sign-in-sources.png | Bin 92674 -> 49081 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png index 08155a4fade75af85f36fdbe8cacff1af936280c..95f8bbdcd2489c4eb14c100c92dffadf3a1a65fc 100644 GIT binary patch literal 49081 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 n{ErgGcytqE7Zr6^h`m+mD8d?#n7A>v#VTY~gf5 From 6cdce04937837de8014b1c044c91ed5b32bdc90a Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 13:40:24 +0300 Subject: [PATCH 097/199] Revert "Revert "no need to show the help about Github or the other authentication sources"" This reverts commit 075ace5686c63d40f7e9fa02bf37e1bd3168f7e7. --- app/helpers/application_settings_helper.rb | 9 +-------- app/views/admin/application_settings/_form.html.haml | 3 --- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 82056a4c97..878b1b254c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -71,15 +71,8 @@ module ApplicationSettingsHelper label_tag(checkbox_name, class: css_class) do check_box_tag(checkbox_name, source, !disabled, autocomplete: 'off', - aria: { describedby: help_block_id }) + Gitlab::OAuth::Provider.label_for(source) + 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) end end end - - def oauth_providers_with_help_links - button_based_providers.map do |provider| - Gitlab::OAuth::Provider.label_for(provider) + ' ' + - link_to("(?)", help_page_path("integration", provider)) - end - end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index f3681bc96e..576509e3f2 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -116,9 +116,6 @@ .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes('oauth-providers-help').each do |source| = source - %span.help-block#oauth-providers-help - Enabled OmniAuth must be configured for - = oauth_providers_with_help_links.to_sentence.html_safe .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From 260c88a75865a1b849a4bac8a14c897fbe522047 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 13:43:12 +0300 Subject: [PATCH 098/199] no need to define the help block since there's no help block --- app/helpers/application_settings_helper.rb | 5 ++--- app/views/admin/application_settings/_form.html.haml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 878b1b254c..03080d2593 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -61,7 +61,7 @@ module ApplicationSettingsHelper end end - def oauth_providers_checkboxes(help_block_id) + def oauth_providers_checkboxes button_based_providers.map do |source| disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) css_class = 'btn' @@ -70,8 +70,7 @@ module ApplicationSettingsHelper label_tag(checkbox_name, class: css_class) do check_box_tag(checkbox_name, source, !disabled, - autocomplete: 'off', - 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) + autocomplete: 'off') + Gitlab::OAuth::Provider.label_for(source) end end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 576509e3f2..f7c799c968 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -114,7 +114,7 @@ = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 .btn-group{ data: { toggle: 'buttons' } } - - oauth_providers_checkboxes('oauth-providers-help').each do |source| + - oauth_providers_checkboxes.each do |source| = source .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' From d66833bca5ae0347751ceab99062b638965eab78 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 13:56:04 +0300 Subject: [PATCH 099/199] =?UTF-8?q?schema=20fix=20=F0=9F=98=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/schema.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index b21cc16289..74facd1208 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -70,16 +70,16 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "recaptcha_site_key" t.string "recaptcha_private_key" t.integer "metrics_port", default: 8089 + t.boolean "akismet_enabled", default: false + t.string "akismet_api_key" t.integer "metrics_sample_interval", default: 15 t.boolean "sentry_enabled", default: false t.string "sentry_dsn" - t.boolean "akismet_enabled", default: false - t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" t.boolean "repository_checks_enabled", default: false - t.integer "metrics_packet_size", default: 1 t.text "shared_runners_text" + t.integer "metrics_packet_size", default: 1 t.text "disabled_oauth_sign_in_sources" end @@ -427,10 +427,10 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "state" t.integer "iid" t.integer "updated_by_id" - t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" t.date "due_date" + t.integer "moved_to_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -717,8 +717,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.integer "project_id" t.text "data" t.text "encrypted_credentials" - t.text "encrypted_credentials_iv" - t.text "encrypted_credentials_salt" + t.string "encrypted_credentials_iv" + t.string "encrypted_credentials_salt" end create_table "projects", force: :cascade do |t| @@ -816,9 +816,9 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "active", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "active", default: false, null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true From 945c5b3fe6e0552f77da8b1a1efe75cd04434f53 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 12 May 2016 15:14:14 +0200 Subject: [PATCH 100/199] Removed tracking of total method execution times Because method call timings are inclusive (that is, they include the time of any sub method calls) this would lead to the total method execution time often being far greater than the total transaction time. Because this is incredibly confusing it's best to simply _not_ track the total method execution time, after all it's not that useful to begin with. Fixes gitlab-org/gitlab-ce#17239 --- CHANGELOG | 1 + lib/gitlab/metrics/instrumentation.rb | 2 -- spec/lib/gitlab/metrics/instrumentation_spec.rb | 6 ------ 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 187011c601..985e25aab3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,7 @@ v 8.8.0 (unreleased) - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 + - Total method execution timings are no longer tracked v 8.7.5 - Fix relative links in wiki pages. !4050 diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index 708ef79f30..0f115893a1 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -154,8 +154,6 @@ module Gitlab duration = (Time.now - start) * 1000.0 if duration >= Gitlab::Metrics.method_call_threshold - trans.increment(:method_duration, duration) - trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, { duration: duration }, method: #{label.inspect}) diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 5c885a7a98..7b86450a22 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -56,9 +56,6 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:increment). - with(:method_duration, a_kind_of(Numeric)) - expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy.foo') @@ -139,9 +136,6 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:increment). - with(:method_duration, a_kind_of(Numeric)) - expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy#bar') From 5bd356eb31fb4684199f6f681f4901ebe025ed92 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 12 May 2016 09:27:58 -0500 Subject: [PATCH 101/199] Fix specs --- app/models/ci/build.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 01236b8e3c..4a3f92ad34 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -95,7 +95,7 @@ module Ci end def trace_html - trace_with_state[:html] + trace_with_state[:html] || '' end def trace_with_state(state = nil) From f4ab8ea4968b97fc2c918b85c534978aaa954c94 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 17:16:35 +0200 Subject: [PATCH 102/199] rename method --- app/services/projects/create_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index c61a515a6b..e0081e5162 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -50,7 +50,7 @@ module Projects @project.build_forked_project_link(forked_from_project_id: forked_from_project_id) end - process_import_data(import_data) + save_project_and_import_data(import_data) start_import if @project.import? @@ -100,7 +100,7 @@ module Projects end end - def process_import_data(import_data) + def save_project_and_import_data(import_data) Project.transaction do @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data @project.save From 827fbc10f6147c3c86278ed7f1675b085cc2a3cb Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 3 May 2016 13:58:50 +0100 Subject: [PATCH 103/199] Only show forks for users who can download code The ForksController enforces this, so don't show the link if it won't go anywhere. --- CHANGELOG | 1 + app/helpers/projects_helper.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 187011c601..878453e861 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,7 @@ v 8.8.0 (unreleased) - Backport GitHub Enterprise import support from EE - Create tags using Rugged for performance reasons. !3745 - API: Expose Issue#user_notes_count. !3126 (Anton Popov) + - Don't show forks button when user can't view forks - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) - Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724 diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 85f8854d2e..e1ab78df69 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -138,10 +138,10 @@ module ProjectsHelper private def get_project_nav_tabs(project, current_user) - nav_tabs = [:home, :forks] + nav_tabs = [:home] if !project.empty_repo? && can?(current_user, :download_code, project) - nav_tabs << [:files, :commits, :network, :graphs] + nav_tabs << [:files, :commits, :network, :graphs, :forks] end if project.repo_exists? && can?(current_user, :read_merge_request, project) From 7c32488dc0170547720b36b11f9d8b7f6466c309 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 17:22:43 +0200 Subject: [PATCH 104/199] added changelog --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 4841361482..f8fd6a8ee2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,9 @@ v 8.8.0 (unreleased) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 +v 8.7.5 + - Fix Bitbucket imports not finding the import data + v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) - Fix setting trusted proxies !3970 From f27d7b2c642e66a45b2b3644e955250786ae07fa Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 18:03:16 +0200 Subject: [PATCH 105/199] updated changelog --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f8fd6a8ee2..8dc52cde60 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,7 +44,7 @@ v 8.8.0 (unreleased) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 v 8.7.5 - - Fix Bitbucket imports not finding the import data + - Fix external imports not finding the import data v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) From ad77ab0376fabf3dfadea86c716358964b526956 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 12 May 2016 09:04:04 -0700 Subject: [PATCH 106/199] Add Changelog entry and drop exception handling around HealthCheck::Utils.process_checks, it wasn't needed --- CHANGELOG | 1 + app/controllers/admin/health_check_controller.rb | 6 +----- app/views/admin/health_check/show.html.haml | 12 ++++++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7d5f424eae..7ded902776 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.8.0 (unreleased) - Reduce delay in destroying a project from 1-minute to immediately - Make build status canceled if any of the jobs was canceled and none failed - Upgrade Sidekiq to 4.1.2 + - Added /health_check endpoint for checking service status - Sanitize repo paths in new project error message - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb index 3153a765e3..241c7be0ea 100644 --- a/app/controllers/admin/health_check_controller.rb +++ b/app/controllers/admin/health_check_controller.rb @@ -1,9 +1,5 @@ class Admin::HealthCheckController < Admin::ApplicationController def show - begin - @errors = HealthCheck::Utils.process_checks('standard') - rescue => e - @errors = e.message.blank? ? e.class.to_s : e.message.to_s - end + @errors = HealthCheck::Utils.process_checks('standard') end end diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index ed7025f7a0..ad79fd26d0 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -16,21 +16,21 @@ Health information can be reteived as plain text, json, or xml using: %ul %li - %code= health_check_url(token:current_application_settings.health_check_access_token) + %code= health_check_url(token: current_application_settings.health_check_access_token) %li - %code= health_check_url(token:current_application_settings.health_check_access_token, format: :json) + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json) %li - %code= health_check_url(token:current_application_settings.health_check_access_token, format: :xml) + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml) %p.light You can also ask for the status of specific services: %ul %li - %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :cache) + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache) %li - %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :database) + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database) %li - %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :migrations) + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations) %hr .panel.panel-default From 0641fe6f72a3d42f94062fd00b1cf8a0d643e6a4 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 12 May 2016 18:11:03 +0200 Subject: [PATCH 107/199] Updated CHANGELOG for 8.7.5 [ci skip] --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 1d8712ae92..5dc5e05705 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -48,6 +48,8 @@ v 8.8.0 (unreleased) v 8.7.5 - Fix relative links in wiki pages. !4050 + - Fix always showing build notification message when switching between merge requests !4086 + - Fix an issue when filtering merge requests with more than one label. !3886 v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) From cacbecd05c4edc7d27c458462463f2ee65383ea5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 18:27:20 +0200 Subject: [PATCH 108/199] more changes based on MR feedback --- app/controllers/projects/imports_controller.rb | 2 +- app/services/projects/create_service.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index c70b9bf2f5..d353a39034 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -22,7 +22,7 @@ class Projects::ImportsController < Projects::ApplicationController Project.transaction do @project.import_start end - @project.add_import_job if @project.persisted? + @project.add_import_job end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index e0081e5162..2687544ae6 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -103,9 +103,8 @@ module Projects def save_project_and_import_data(import_data) Project.transaction do @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data - @project.save - if @project.persisted? && !@project.import? + if @project.save && !@project.import? raise 'Failed to create repository' unless @project.create_repository end end From 18fdbf0a035b6feec3b576c01ee1a2f3a95e4305 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Wed, 11 May 2016 23:46:19 +0900 Subject: [PATCH 109/199] Fix a description for default scope on builds --- CHANGELOG | 1 + app/views/admin/builds/index.html.haml | 2 +- app/views/projects/builds/index.html.haml | 2 +- spec/features/admin/admin_builds_spec.rb | 1 + spec/features/builds_spec.rb | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 777b211daa..8d993e3013 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,6 +51,7 @@ v 8.7.5 - Fix relative links in wiki pages. !4050 - Fix always showing build notification message when switching between merge requests !4086 - Fix an issue when filtering merge requests with more than one label. !3886 + - Fix short note for the default scope on build page (Takuya Noguchi) v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 804d7851bd..ed24757087 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -20,7 +20,7 @@ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post .row-content-block.second-block - #{(@scope || 'running').capitalize} builds + #{(@scope || 'all').capitalize} builds %ul.content-list - if @builds.blank? diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 2e8015d119..98f4a9416e 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -36,7 +36,7 @@ %span CI Lint .row-content-block - #{(@scope || 'running').capitalize} builds from this project + #{(@scope || 'all').capitalize} builds from this project %ul.content-list - if @builds.blank? diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index 2e9851fb44..7bbe20fec4 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -19,6 +19,7 @@ describe 'Admin Builds' do visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') + expect(page).to have_selector('.row-content-block', text: 'All builds') expect(page.all('.build-link').size).to eq(4) expect(page).to have_link 'Cancel all' end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 090a941958..f83a78308e 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -43,6 +43,7 @@ describe "Builds" do end it { expect(page).to have_selector('.nav-links li.active', text: 'All') } + it { expect(page).to have_selector('.row-content-block', text: 'All builds from this project') } it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } From bade0ed63e55a8168b9c2c1ddd16edbcfcd9dcd7 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 18:39:10 +0200 Subject: [PATCH 110/199] looks like 8.7.6 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8dc52cde60..1b246919a1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,7 +43,7 @@ v 8.8.0 (unreleased) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 -v 8.7.5 +v 8.7.6 - Fix external imports not finding the import data v 8.7.4 From 491a312724076f5113766b5fb55f38fee316dd38 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 18:40:50 +0200 Subject: [PATCH 111/199] fix merge conflict --- CHANGELOG | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 986f465327..5412e4c19b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,14 +47,14 @@ v 8.8.0 (unreleased) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 - Total method execution timings are no longer tracked +v 8.7.6 + - Fix external imports not finding the import data + v 8.7.5 - Fix relative links in wiki pages. !4050 - Fix always showing build notification message when switching between merge requests !4086 - Fix an issue when filtering merge requests with more than one label. !3886 -v 8.7.6 - - Fix external imports not finding the import data - v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) - Fix setting trusted proxies !3970 From fc2d985bfaa156ad052858cd2025b0300327ff95 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 12 May 2016 12:47:55 -0500 Subject: [PATCH 112/199] Fix CI tests --- app/controllers/jwt_controller.rb | 2 +- ...ntainer_registry_authentication_service.rb | 6 +- config/initializers/1_settings.rb | 2 +- lib/jwt/rsa_token.rb | 2 +- lib/jwt/token.rb | 4 +- spec/lib/jwt/rsa_token_spec.rb | 2 +- spec/lib/jwt/token_spec.rb | 2 +- ...er_registry_authentication_service_spec.rb | 56 +++++++++---------- 8 files changed, 38 insertions(+), 38 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 0048a1a31e..599f62bd12 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -3,7 +3,7 @@ class JwtController < ApplicationController skip_before_action :verify_authenticity_token SERVICES = { - 'container_registry' => Jwt::ContainerRegistryAuthenticationService, + 'container_registry' => JWT::ContainerRegistryAuthenticationService, } def auth diff --git a/app/services/jwt/container_registry_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb index b9fcd38047..0ab3e6d02b 100644 --- a/app/services/jwt/container_registry_authentication_service.rb +++ b/app/services/jwt/container_registry_authentication_service.rb @@ -1,11 +1,11 @@ -module Jwt +module JWT class ContainerRegistryAuthenticationService < BaseService def execute if params[:offline_token] return error('forbidden', 403) unless current_user end - return error('forbidden', 401) if scopes.empty? + return error('forbidden', 401) if scopes.blank? { token: authorized_token(scopes).encoded } end @@ -13,7 +13,7 @@ module Jwt private def authorized_token(access) - token = ::Jwt::RSAToken.new(registry.key) + token = ::JWT::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = params[:service] token.subject = current_user.try(:username) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 1040d840e3..3853845fee 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -271,7 +271,7 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes # Registry # Settings['registry'] ||= Settingslogic.new({}) -Settings.registry['registry'] = false if Settings.registry['enabled'].nil? +Settings.registry['enabled'] = false if Settings.registry['enabled'].nil? Settings.registry['host'] ||= "example.com" Settings.registry['internal_host']||= "localhost" Settings.registry['key'] ||= nil diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb index cc265e3b31..0438135ad5 100644 --- a/lib/jwt/rsa_token.rb +++ b/lib/jwt/rsa_token.rb @@ -1,4 +1,4 @@ -module Jwt +module JWT class RSAToken < Token attr_reader :key_file diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb index 765ab0d60c..f13abf2b71 100644 --- a/lib/jwt/token.rb +++ b/lib/jwt/token.rb @@ -1,4 +1,4 @@ -module Jwt +module JWT class Token attr_accessor :issuer, :subject, :audience, :id attr_accessor :issued_at, :not_before, :expire_time @@ -43,4 +43,4 @@ module Jwt }.compact end end -end \ No newline at end of file +end diff --git a/spec/lib/jwt/rsa_token_spec.rb b/spec/lib/jwt/rsa_token_spec.rb index 710801923e..a5b1d3a67d 100644 --- a/spec/lib/jwt/rsa_token_spec.rb +++ b/spec/lib/jwt/rsa_token_spec.rb @@ -1,4 +1,4 @@ -describe Jwt::RSAToken do +describe JWT::RSAToken do let(:rsa_key) { generate_key } let(:rsa_token) { described_class.new(nil) } let(:rsa_encoded) { rsa_token.encoded } diff --git a/spec/lib/jwt/token_spec.rb b/spec/lib/jwt/token_spec.rb index a56b4cf39b..92fdc3f1b7 100644 --- a/spec/lib/jwt/token_spec.rb +++ b/spec/lib/jwt/token_spec.rb @@ -1,4 +1,4 @@ -describe Jwt::Token do +describe JWT::Token do let(:token) { described_class.new } context 'custom parameters' do diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/jwt/container_registry_authentication_service_spec.rb index ea91f499d0..1873ea2639 100644 --- a/spec/services/jwt/container_registry_authentication_service_spec.rb +++ b/spec/services/jwt/container_registry_authentication_service_spec.rb @@ -1,23 +1,23 @@ require 'spec_helper' -describe Jwt::ContainerRegistryAuthenticationService, services: true do +describe JWT::ContainerRegistryAuthenticationService, services: true do let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } - let(:registry_settings) { + let(:registry_settings) do { issuer: 'rspec', key: nil } - } + end let(:payload) { JWT.decode(subject[:token], rsa_key).first } subject { described_class.new(current_project, current_user, current_params).execute } before do allow(Gitlab.config.registry).to receive_messages(registry_settings) - allow_any_instance_of(Jwt::RSAToken).to receive(:key).and_return(rsa_key) + allow_any_instance_of(JWT::RSAToken).to receive(:key).and_return(rsa_key) end shared_examples 'an authenticated' do @@ -26,13 +26,13 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do end shared_examples 'a accessible' do - let(:access) { + let(:access) do [{ 'type' => 'repository', 'name' => project.path_with_namespace, 'actions' => actions, }] - } + end it_behaves_like 'an authenticated' it { expect(payload).to include('access' => access) } @@ -68,9 +68,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'allow developer to push images' do before { project.team << [current_user, :developer] } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push" } - } + end it_behaves_like 'a pushable' end @@ -78,9 +78,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'allow reporter to pull images' do before { project.team << [current_user, :reporter] } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull" } - } + end it_behaves_like 'a pullable' end @@ -88,9 +88,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'return a least of privileges' do before { project.team << [current_user, :reporter] } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push,pull" } - } + end it_behaves_like 'a pullable' end @@ -98,9 +98,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'disallow guest to pull or push images' do before { project.team << [current_user, :guest] } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull,push" } - } + end it_behaves_like 'a forbidden' end @@ -110,9 +110,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do let(:current_project) { create(:empty_project) } context 'allow to pull and push images' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{current_project.path_with_namespace}:pull,push" } - } + end it_behaves_like 'a pullable and pushable' do let(:project) { current_project } @@ -121,9 +121,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'for other projects' do context 'when pulling' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull" } - } + end context 'allow for public' do let(:project) { create(:empty_project, :public) } @@ -137,9 +137,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do end context 'when pushing' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push" } - } + end context 'disallow for all' do let(:project) { create(:empty_project, :public) } @@ -152,9 +152,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'unauthorized' do context 'for invalid scope' do - let(:current_params) { + let(:current_params) do { scope: 'invalid:aa:bb' } - } + end it_behaves_like 'a forbidden' end @@ -162,9 +162,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'for private project' do let(:project) { create(:empty_project, :private) } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull" } - } + end it_behaves_like 'a forbidden' end @@ -173,17 +173,17 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do let(:project) { create(:empty_project, :public) } context 'when pulling and pushing' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull,push" } - } + end it_behaves_like 'a pullable' end context 'when pushing' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push" } - } + end it_behaves_like 'a forbidden' end From 0c22698bd4dbe7d0d3e4a6c8bc946ac6f5de1c12 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Thu, 12 May 2016 22:48:09 +0200 Subject: [PATCH 113/199] Add API endpoints for un/subscribing from/to a label Closes #15638 --- CHANGELOG | 1 + app/models/concerns/subscribable.rb | 6 ++ doc/api/labels.md | 70 +++++++++++++++++++ lib/api/api.rb | 1 + lib/api/entities.rb | 4 ++ lib/api/helpers.rb | 11 +++ lib/api/issues.rb | 39 +---------- lib/api/labels.rb | 6 +- lib/api/merge_requests.rb | 36 ---------- lib/api/subscriptions.rb | 60 +++++++++++++++++ spec/models/concerns/subscribable_spec.rb | 10 +++ spec/requests/api/issues_spec.rb | 12 ++++ spec/requests/api/labels_spec.rb | 82 +++++++++++++++++++++++ 13 files changed, 261 insertions(+), 77 deletions(-) create mode 100644 lib/api/subscriptions.rb diff --git a/CHANGELOG b/CHANGELOG index efb6dc6f61..aedd9f8ebd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,6 +47,7 @@ v 8.8.0 (unreleased) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 - Total method execution timings are no longer tracked - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034. (Andrei Gliga) + - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) v 8.7.5 - Fix relative links in wiki pages. !4050 diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index d5a881b244..083257f100 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -36,6 +36,12 @@ module Subscribable update(subscribed: !subscribed?(user)) end + def subscribe(user) + subscriptions. + find_or_initialize_by(user_id: user.id). + update(subscribed: true) + end + def unsubscribe(user) subscriptions. find_or_initialize_by(user_id: user.id). diff --git a/doc/api/labels.md b/doc/api/labels.md index 3730c07c5a..b857d81768 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -165,3 +165,73 @@ Example response: "description": "Documentation" } ``` + +## Subscribe to a label + +Subscribes the authenticated user to a label to receive notifications. If the +operation is successful, status code `201` together with the updated label is +returned. If the user is already subscribed to the label, the status code `304` +is returned. If the project or label is not found, status code `404` is +returned. + +``` +POST /projects/:id/labels/:label_id/subscription +``` + +| Attribute | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------ | +| `id` | integer | yes | The ID of a project | +| `label_id` | integer or string | yes | The ID or title of a project's label | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +``` + +Example response: + +```json +{ + "name": "Docs", + "color": "#cc0033", + "description": "", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": true +} +``` + +## Unsubscribe from a label + +Unsubscribes the authenticated user from a label to not receive notifications +from it. If the operation is successful, status code `200` together with the +updated label is returned. If the user is not subscribed to the label, the +status code `304` is returned. If the project or label is not found, status code +`404` is returned. + +``` +DELETE /projects/:id/labels/:label_id/subscription +``` + +| Attribute | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------ | +| `id` | integer | yes | The ID of a project | +| `label_id` | integer or string | yes | The ID or title of a project's label | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +``` + +Example response: + +```json +{ + "name": "Docs", + "color": "#cc0033", + "description": "", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false +} +``` diff --git a/lib/api/api.rb b/lib/api/api.rb index 5fd9c30cb4..360fb41a72 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -57,5 +57,6 @@ module API mount ::API::Variables mount ::API::Runners mount ::API::Licenses + mount ::API::Subscriptions end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 2870a6a40e..406f5ea913 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -307,6 +307,10 @@ module API class Label < Grape::Entity expose :name, :color, :description expose :open_issues_count, :closed_issues_count, :open_merge_requests_count + + expose :subscribed do |label, options| + label.subscribed?(options[:current_user]) + end end class Compare < Grape::Entity diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 40c967453f..5e638dbe16 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -95,6 +95,17 @@ module API end end + def find_project_label(id) + label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id) + label || not_found!('Label') + end + + def find_project_issue(id) + issue = user_project.issues.find(id) + not_found! unless can?(current_user, :read_issue, issue) + issue + end + def paginate(relation) relation.page(params[:page]).per(params[:per_page].to_i).tap do |data| add_pagination_headers(data) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 4092874948..f59a4d6c01 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -103,8 +103,7 @@ module API # Example Request: # GET /projects/:id/issues/:issue_id get ":id/issues/:issue_id" do - @issue = user_project.issues.find(params[:issue_id]) - not_found! unless can?(current_user, :read_issue, @issue) + @issue = find_project_issue(params[:issue_id]) present @issue, with: Entities::Issue, current_user: current_user end @@ -234,42 +233,6 @@ module API authorize!(:destroy_issue, issue) issue.destroy end - - # Subscribes to a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # POST /projects/:id/issues/:issue_id/subscription - post ':id/issues/:issue_id/subscription' do - issue = user_project.issues.find(params[:issue_id]) - - if issue.subscribed?(current_user) - not_modified! - else - issue.toggle_subscription(current_user) - present issue, with: Entities::Issue, current_user: current_user - end - end - - # Unsubscribes from a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # DELETE /projects/:id/issues/:issue_id/subscription - delete ':id/issues/:issue_id/subscription' do - issue = user_project.issues.find(params[:issue_id]) - - if issue.subscribed?(current_user) - issue.unsubscribe(current_user) - present issue, with: Entities::Issue, current_user: current_user - else - not_modified! - end - end end end end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 4af6bef0fa..c806829d69 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -11,7 +11,7 @@ module API # Example Request: # GET /projects/:id/labels get ':id/labels' do - present user_project.labels, with: Entities::Label + present user_project.labels, with: Entities::Label, current_user: current_user end # Creates a new label @@ -36,7 +36,7 @@ module API label = user_project.labels.create(attrs) if label.valid? - present label, with: Entities::Label + present label, with: Entities::Label, current_user: current_user else render_validation_error!(label) end @@ -90,7 +90,7 @@ module API attrs[:name] = attrs.delete(:new_name) if attrs.key?(:new_name) if label.update(attrs) - present label, with: Entities::Label + present label, with: Entities::Label, current_user: current_user else render_validation_error!(label) end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 7e78609ecb..4e7de8867b 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -327,42 +327,6 @@ module API issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: Entities::Issue, current_user: current_user end - - # Subscribes to a merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of a merge request - # Example Request: - # POST /projects/:id/issues/:merge_request_id/subscription - post "#{path}/subscription" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - if merge_request.subscribed?(current_user) - not_modified! - else - merge_request.toggle_subscription(current_user) - present merge_request, with: Entities::MergeRequest, current_user: current_user - end - end - - # Unsubscribes from a merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of a merge request - # Example Request: - # DELETE /projects/:id/merge_requests/:merge_request_id/subscription - delete "#{path}/subscription" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - if merge_request.subscribed?(current_user) - merge_request.unsubscribe(current_user) - present merge_request, with: Entities::MergeRequest, current_user: current_user - else - not_modified! - end - end end end end diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb new file mode 100644 index 0000000000..c49e2a21b8 --- /dev/null +++ b/lib/api/subscriptions.rb @@ -0,0 +1,60 @@ +module API + class Subscriptions < Grape::API + before { authenticate! } + + subscribable_types = { + 'merge_request' => proc { |id| user_project.merge_requests.find(id) }, + 'merge_requests' => proc { |id| user_project.merge_requests.find(id) }, + 'issues' => proc { |id| find_project_issue(id) }, + 'labels' => proc { |id| find_project_label(id) }, + } + + resource :projects do + subscribable_types.each do |type, finder| + type_singularized = type.singularize + type_id_str = :"#{type_singularized}_id" + entity_class = Entities.const_get(type_singularized.camelcase) + + # Subscribe to a resource + # + # Parameters: + # id (required) - The ID of a project + # subscribable_id (required) - The ID of a resource + # Example Request: + # POST /projects/:id/labels/:subscribable_id/subscription + # POST /projects/:id/issues/:subscribable_id/subscription + # POST /projects/:id/merge_requests/:subscribable_id/subscription + post ":id/#{type}/:#{type_id_str}/subscription" do + resource = instance_exec(params[type_id_str], &finder) + + if resource.subscribed?(current_user) + not_modified! + else + resource.subscribe(current_user) + present resource, with: entity_class, current_user: current_user + end + end + + # Unsubscribe from a resource + # + # Parameters: + # id (required) - The ID of a project + # subscribable_id (required) - The ID of a resource + # Example Request: + # DELETE /projects/:id/labels/:subscribable_id/subscription + # DELETE /projects/:id/issues/:subscribable_id/subscription + # DELETE /projects/:id/merge_requests/:subscribable_id/subscription + delete ":id/#{type}/:#{type_id_str}/subscription" do + resource = instance_exec(params[type_id_str], &finder) + + if !resource.subscribed?(current_user) + not_modified! + else + resource.unsubscribe(current_user) + present resource, with: entity_class, current_user: current_user + end + end + end + end + end +end diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb index e31fdb0bff..b7fc5a9249 100644 --- a/spec/models/concerns/subscribable_spec.rb +++ b/spec/models/concerns/subscribable_spec.rb @@ -44,6 +44,16 @@ describe Subscribable, 'Subscribable' do end end + describe '#subscribe' do + it 'subscribes the given user' do + expect(resource.subscribed?(user)).to be_falsey + + resource.subscribe(user) + + expect(resource.subscribed?(user)).to be_truthy + end + end + describe '#unsubscribe' do it 'unsubscribes the given current user' do resource.subscriptions.create(user: user, subscribed: true) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9dd43f4fab..37ab9cc8cf 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -623,6 +623,12 @@ describe API::API, api: true do expect(response.status).to eq(404) end + + it 'returns 404 if the issue is confidential' do + post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response.status).to eq(404) + end end describe 'DELETE :id/issues/:issue_id/subscription' do @@ -644,5 +650,11 @@ describe API::API, api: true do expect(response.status).to eq(404) end + + it 'returns 404 if the issue is confidential' do + delete api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response.status).to eq(404) + end end end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 6943ff9d26..b2c7f8d9ac 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -190,4 +190,86 @@ describe API::API, api: true do expect(json_response['message']['color']).to eq(['must be a valid color code']) end end + + describe "POST /projects/:id/labels/:label_id/subscription" do + context "when label_id is a label title" do + it "should subscribe to the label" do + post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + + expect(response.status).to eq(201) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_truthy + end + end + + context "when label_id is a label ID" do + it "should subscribe to the label" do + post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(201) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_truthy + end + end + + context "when user is already subscribed to label" do + before { label1.subscribe(user) } + + it "should return 304" do + post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(304) + end + end + + context "when label ID is not found" do + it "should a return 404 error" do + post api("/projects/#{project.id}/labels/1234/subscription", user) + + expect(response.status).to eq(404) + end + end + end + + describe "DELETE /projects/:id/labels/:label_id/subscription" do + before { label1.subscribe(user) } + + context "when label_id is a label title" do + it "should unsubscribe from the label" do + delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_falsey + end + end + + context "when label_id is a label ID" do + it "should unsubscribe from the label" do + delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_falsey + end + end + + context "when user is already unsubscribed from label" do + before { label1.unsubscribe(user) } + + it "should return 304" do + delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(304) + end + end + + context "when label ID is not found" do + it "should a return 404 error" do + delete api("/projects/#{project.id}/labels/1234/subscription", user) + + expect(response.status).to eq(404) + end + end + end end From e4d3e949555c51cf54ead163bf001b4ec86a2733 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 12 May 2016 16:22:21 -0500 Subject: [PATCH 114/199] Move broadcast msg into content-wrapper --- app/views/layouts/_page.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 3c3bc41bf0..3c0660d34f 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,5 +1,4 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } - = render "layouts/broadcast" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo %a#logo @@ -27,6 +26,7 @@ .container-fluid = render "layouts/nav/#{nav}" .content-wrapper{ class: ('page-with-layout-nav' if defined?(nav) && nav) } + = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message %div{ class: (container_class unless @no_container) } From 05bbad5817c567b547177441769643042d699d8a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 11 May 2016 15:17:16 -0500 Subject: [PATCH 115/199] Clean up EventsHelper spec --- spec/helpers/events_helper_spec.rb | 95 +++++++++++++++--------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index e68a5ec29a..c0d2be98e8 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -1,64 +1,65 @@ require 'spec_helper' describe EventsHelper do - include ApplicationHelper - include GitlabMarkdownHelper + describe '#event_note' do + before do + allow(helper).to receive(:current_user).and_return(double) + end - let(:current_user) { create(:user, email: "current@email.com") } + it 'should display one line of plain text without alteration' do + input = 'A short, plain note' + expect(helper.event_note(input)).to match(input) + expect(helper.event_note(input)).not_to match(/\.\.\.\z/) + end - it 'should display one line of plain text without alteration' do - input = 'A short, plain note' - expect(event_note(input)).to match(input) - expect(event_note(input)).not_to match(/\.\.\.\z/) - end + it 'should display inline code' do + input = 'A note with `inline code`' + expected = 'A note with inline code' - it 'should display inline code' do - input = 'A note with `inline code`' - expected = 'A note with inline code' + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should truncate a note with multiple paragraphs' do + input = "Paragraph 1\n\nParagraph 2" + expected = 'Paragraph 1...' - it 'should truncate a note with multiple paragraphs' do - input = "Paragraph 1\n\nParagraph 2" - expected = 'Paragraph 1...' + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should display the first line of a code block' do + input = "```\nCode block\nwith two lines\n```" + expected = %r{Code block\.\.\.} - it 'should display the first line of a code block' do - input = "```\nCode block\nwith two lines\n```" - expected = %r{Code block\.\.\.} + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should truncate a single long line of text' do + text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars + input = text * 4 + expected = (text * 2).sub(/.{3}/, '...') - it 'should truncate a single long line of text' do - text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars - input = "#{text}#{text}#{text}#{text}" # 200 chars - expected = "#{text}#{text}".sub(/.{3}/, '...') + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should preserve a link href when link text is truncated' do + text = 'The quick brown fox jumped over the lazy dog' # 44 chars + input = "#{text}#{text}#{text} " # 133 chars + link_url = 'http://example.com/foo/bar/baz' # 30 chars + input << link_url + expected_link_text = 'http://example...' - it 'should preserve a link href when link text is truncated' do - text = 'The quick brown fox jumped over the lazy dog' # 44 chars - input = "#{text}#{text}#{text} " # 133 chars - link_url = 'http://example.com/foo/bar/baz' # 30 chars - input << link_url - expected_link_text = 'http://example...' + expect(helper.event_note(input)).to match(link_url) + expect(helper.event_note(input)).to match(expected_link_text) + end - expect(event_note(input)).to match(link_url) - expect(event_note(input)).to match(expected_link_text) - end - - it 'should preserve code color scheme' do - input = "```ruby\ndef test\n 'hello world'\nend\n```" - expected = '

    ' \
    -      "def test\n" \
    -      "  \'hello world\'\n" \
    -      "end" \
    -      '
    ' - expect(event_note(input)).to eq(expected) + it 'should preserve code color scheme' do + input = "```ruby\ndef test\n 'hello world'\nend\n```" + expected = '
    ' \
    +        "def test\n" \
    +        "  \'hello world\'\n" \
    +        "end" \
    +        '
    ' + expect(helper.event_note(input)).to eq(expected) + end end end From 7bc594735b7923d1e57eb504f09716bbb72c8c06 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 12 May 2016 17:06:47 -0500 Subject: [PATCH 116/199] Undo changes; reduce sidebar internal width --- app/assets/stylesheets/framework/sidebar.scss | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index c243dd8a42..f90d7a806d 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -63,7 +63,7 @@ float: left; height: $header-height; width: 100%; - padding-left: 16px; + padding-left: 22px; overflow: hidden; outline: none; transition-duration: .3s; @@ -78,7 +78,7 @@ } .gitlab-text-container { - width: 220px; + width: 230px; h3 { width: 158px; @@ -146,12 +146,12 @@ a { padding: 7px 15px; - transition-duration: .3s; font-size: $gl-font-size; line-height: 24px; color: $gray; display: block; text-decoration: none; + padding-left: 23px; font-weight: normal; outline: none; @@ -253,11 +253,6 @@ width: auto; a { - @media (min-width: $screen-xs-min) { - padding-left: 23px; - transition-duration: .3s; - } - span { display: none; } @@ -317,7 +312,7 @@ } .nav-sidebar li a { - width: 220px; + width: $sidebar_width; &.back-link { i { From 4fea5cda0bf7ff56dafae90306441b0d65f3ca05 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 12 May 2016 17:11:37 -0500 Subject: [PATCH 117/199] Fix minor typos in admin health check page --- app/views/admin/health_check/show.html.haml | 2 +- spec/features/admin/admin_health_check_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index ad79fd26d0..c2313986a7 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -13,7 +13,7 @@ = icon('refresh') Reset health check access token %p.light - Health information can be reteived as plain text, json, or xml using: + Health information can be retrieved as plain text, JSON, or XML using: %ul %li %code= health_check_url(token: current_application_settings.health_check_access_token) diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 4fde04b609..dec2dedf2b 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -13,7 +13,7 @@ feature "Admin Health Check", feature: true do end it { page.has_text? 'Health Check' } - it { page.has_text? 'Health information can be reteived' } + it { page.has_text? 'Health information can be retrieved' } it 'has a health check access token' do token = current_application_settings.health_check_access_token From 311fb58c44fe6dd14709c8eb8f9346ec73e03aff Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 10 May 2016 23:24:23 -0300 Subject: [PATCH 118/199] Require space around operators --- .rubocop.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 3fb2f2df59..562197300b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -276,7 +276,7 @@ Style/IdenticalConditionalBranches: Enabled: false # Checks the indentation of the first line of the right-hand-side of a -# multi-line assignment. +# multi-line assignment. Style/IndentAssignment: Enabled: false @@ -531,7 +531,7 @@ Style/SpaceAroundKeyword: # Use a single space around operators. Style/SpaceAroundOperators: - Enabled: false + Enabled: true # Checks that the left block brace has or doesn't have space before it. Style/SpaceBeforeBlockBraces: From f5a0ac0fc197bae2eb5fe1045ed237cdbbaf6ea4 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 10 May 2016 23:58:06 -0300 Subject: [PATCH 119/199] Codestyle: make sure we have space around operators --- app/controllers/application_controller.rb | 2 +- app/controllers/concerns/creates_commit.rb | 2 +- app/helpers/search_helper.rb | 2 +- app/helpers/selects_helper.rb | 2 +- .../project_services/external_wiki_service.rb | 2 +- app/models/project_wiki.rb | 2 +- app/models/repository.rb | 4 ++-- app/services/merge_requests/build_service.rb | 2 +- app/services/system_hooks_service.rb | 2 +- config/environments/test.rb | 2 +- config/initializers/1_settings.rb | 18 ++++++++--------- config/initializers/carrierwave.rb | 2 +- config/initializers/devise.rb | 2 +- config/routes.rb | 2 +- features/steps/dashboard/issues.rb | 2 +- features/steps/dashboard/merge_requests.rb | 2 +- features/steps/profile/profile.rb | 2 +- features/steps/project/team_management.rb | 2 +- lib/api/helpers.rb | 2 +- lib/gitlab/email/reply_parser.rb | 2 +- lib/gitlab/redis.rb | 6 +++--- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 4 ++-- .../ci/build/artifacts/metadata/entry_spec.rb | 4 ++-- spec/lib/gitlab/lfs/lfs_router_spec.rb | 4 ++-- spec/models/commit_spec.rb | 2 +- spec/models/hooks/service_hook_spec.rb | 4 ++-- spec/models/hooks/system_hook_spec.rb | 20 +++++++++---------- .../slack_service/note_message_spec.rb | 4 ++-- spec/requests/api/builds_spec.rb | 4 ++-- spec/requests/api/group_members_spec.rb | 10 +++++----- spec/requests/ci/api/builds_spec.rb | 4 ++-- spec/support/jira_service_helper.rb | 10 +++++----- spec/support/stub_gitlab_calls.rb | 20 +++++++++---------- 33 files changed, 77 insertions(+), 77 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 17b3f49aed..1429ee40bb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -176,7 +176,7 @@ class ApplicationController < ActionController::Base end def check_password_expiration - if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? + if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? redirect_to new_profile_password_path and return end end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 787416c17a..dacb5679dd 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -122,7 +122,7 @@ module CreatesCommit # Merge request from fork to this project @mr_source_project = @tree_edit_project @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch ||= @ref end end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 24c4c098c6..d2f94d4ae6 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -59,7 +59,7 @@ module SearchHelper # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - ref = @ref || @project.repository.root_ref + ref = @ref || @project.repository.root_ref [ { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index e951a87a21..bb395e3788 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -18,7 +18,7 @@ module SelectsHelper first_user: first_user, current_user: opts[:current_user] || false, "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], - author_id: opts[:author_id] || '' + author_id: opts[:author_id] || '' } } diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index f042bfc521..d7b6e50519 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -25,7 +25,7 @@ class ExternalWikiService < Service def execute(_data) @response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil - if @response !=200 + if @response != 200 nil end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 7c1a61bb0b..060ed9b44e 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -113,7 +113,7 @@ class ProjectWiki end def page_title_and_dir(title) - title_array = title.split("/") + title_array = title.split("/") title = title_array.pop [title, title_array.join("/")] end diff --git a/app/models/repository.rb b/app/models/repository.rb index de7e163078..0eff74320f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -795,7 +795,7 @@ class Repository def check_revert_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] - args << { mainline: 1 } if commit.merge_commit? + args << { mainline: 1 } if commit.merge_commit? revert_index = rugged.revert_commit(*args) return false if revert_index.conflicts? @@ -809,7 +809,7 @@ class Repository def check_cherry_pick_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] - args << 1 if commit.merge_commit? + args << 1 if commit.merge_commit? cherry_pick_index = rugged.cherrypick_commit(*args) return false if cherry_pick_index.conflicts? diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index cd4230aa5e..1b48899bb0 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -66,7 +66,7 @@ module MergeRequests commits = merge_request.compare_commits if commits && commits.count == 1 commit = commits.first - merge_request.title = commit.title + merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) case issue diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index e43b5b51e5..1fb72cf89e 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -85,7 +85,7 @@ class SystemHooksService path_with_namespace: model.path_with_namespace, project_id: model.id, owner_name: owner.name, - owner_email: owner.respond_to?(:email) ? owner.email : "", + owner_email: owner.respond_to?(:email) ? owner.email : "", project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase } end diff --git a/config/environments/test.rb b/config/environments/test.rb index a703c0934f..fb25d3a8b1 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -20,7 +20,7 @@ Rails.application.configure do config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false + config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 23c8cea038..a69b933d81 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -126,7 +126,7 @@ end Settings['omniauth'] ||= Settingslogic.new({}) -Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? +Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_providers'].nil? @@ -134,7 +134,7 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? -Settings.omniauth['providers'] ||= [] +Settings.omniauth['providers'] ||= [] Settings.omniauth['cas3'] ||= Settingslogic.new({}) Settings.omniauth.cas3['session_duration'] ||= 8.hours Settings.omniauth['session_tickets'] ||= Settingslogic.new({}) @@ -168,7 +168,7 @@ end Settings['shared'] ||= Settingslogic.new({}) Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) -Settings['issues_tracker'] ||= {} +Settings['issues_tracker'] ||= {} # # GitLab @@ -183,7 +183,7 @@ Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80 Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' -Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" +Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}" Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab' @@ -196,7 +196,7 @@ Settings.gitlab['user_home'] ||= begin rescue ArgumentError # no user configured '/home/' + Settings.gitlab['user'] end -Settings.gitlab['time_zone'] ||= nil +Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) @@ -211,7 +211,7 @@ Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.g Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? -Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) +Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] @@ -225,8 +225,8 @@ Settings['gitlab_ci'] ||= Settingslogic.new({}) Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['shared_runners_enabled'].nil? Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil? Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? -Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) +Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) # # Reply by email @@ -240,7 +240,7 @@ Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'] Settings['artifacts'] ||= Settingslogic.new({}) Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) -Settings.artifacts['max_size'] ||= 100 # in megabytes +Settings.artifacts['max_size'] ||= 100 # in megabytes # # Git LFS @@ -298,7 +298,7 @@ Settings['backup'] ||= Settingslogic.new({}) Settings.backup['keep_time'] ||= 0 Settings.backup['pg_schema'] = nil Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root) -Settings.backup['archive_permissions'] ||= 0600 +Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) # Convert upload connection settings to use symbol keys, to make Fog happy if Settings.backup['upload']['connection'] diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 0d278717d6..1933afcbfb 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -20,7 +20,7 @@ if File.exist?(aws_file) config.fog_public = false # optional, defaults to {} - config.fog_attributes = { 'Cache-Control'=>'max-age=315576000' } + config.fog_attributes = { 'Cache-Control' => 'max-age=315576000' } # optional time (in seconds) that authenticated urls will be valid. # when fog_public is false and provider is AWS or Google, defaults to 600 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 31dceaebca..021bdb1125 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -243,7 +243,7 @@ Devise.setup do |config| when Hash # Add procs for handling SLO if provider['name'] == 'cas3' - provider['args'][:on_single_sign_out] = lambda do |request| + provider['args'][:on_single_sign_out] = lambda do |request| ticket = request.params[:session_index] raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) Gitlab::OAuth::Session.destroy(:cas3, ticket) diff --git a/config/routes.rb b/config/routes.rb index ec876eba34..0c01c52921 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -83,7 +83,7 @@ Rails.application.routes.draw do get 'help' => 'help#index' get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ } get 'help/shortcuts' - get 'help/ui' => 'help#ui' + get 'help/ui' => 'help#ui' # # Global snippets diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index e21af72a77..8706f0e8e7 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -74,7 +74,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps def project @project ||= begin - project =create :project + project = create :project project.team << [current_user, :master] project end diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index a2adc87f8e..06db36c701 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -100,7 +100,7 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps def project @project ||= begin - project =create :project + project = create :project project.team << [current_user, :master] project end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 909de31a47..b1a87b96ef 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -166,7 +166,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I have group with projects' do - @group = create(:group) + @group = create(:group) @group.add_owner(current_user) @project = create(:project, namespace: @group) @event = create(:closed_issue_event, project: @project) diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 3fbcf770b6..c6ced74737 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -126,7 +126,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I share project with group "OpenSource"' do project = Project.find_by(name: 'Shop') - os_group = create(:group, name: 'OpenSource') + os_group = create(:group, name: 'OpenSource') create(:project, group: os_group) @os_user1 = create(:user) @os_user2 = create(:user) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 5e638dbe16..cadf9f98fe 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -2,7 +2,7 @@ module API module Helpers PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" PRIVATE_TOKEN_PARAM = :private_token - SUDO_HEADER ="HTTP_SUDO" + SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo def parse_boolean(value) diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb index 6ed36b51f1..3411eb1d9c 100644 --- a/lib/gitlab/email/reply_parser.rb +++ b/lib/gitlab/email/reply_parser.rb @@ -65,7 +65,7 @@ module Gitlab (l =~ /On \w+ \d+,? \d+,?.*wrote:/) # Headers on subsequent lines - break if (0..2).all? { |off| lines[idx+off] =~ REPLYING_HEADER_REGEX } + break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX } # Headers on the same line break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3 diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index f7db7a6391..40766f35f7 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -25,7 +25,7 @@ module Gitlab end @pool.with { |redis| yield redis } end - + def self.redis_store_options url = new.url redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url) @@ -40,10 +40,10 @@ module Gitlab def initialize(rails_env=nil) rails_env ||= Rails.env config_file = File.expand_path('../../../config/resque.yml', __FILE__) - + @url = "redis://localhost:6379" if File.exist?(config_file) - @url =YAML.load_file(config_file)[rails_env] + @url = YAML.load_file(config_file)[rails_env] end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index c7ab318537..9eef8ea097 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -443,12 +443,12 @@ module Ci context 'when job variables are defined' do context 'when syntax is correct' do it 'returns job variables' do - variables = { + variables = { KEY1: 'value1', SOME_KEY_2: 'value2' } - config = YAML.dump( + config = YAML.dump( { before_script: ['pwd'], rspec: { variables: variables, diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb index acca0b08ba..46a5b7fce6 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -10,8 +10,8 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do 'path/dir_1/subdir/subfile' => { size: 10 }, 'path/second_dir' => {}, 'path/second_dir/dir_3/file_2' => { size: 10 }, - 'path/second_dir/dir_3/file_3'=> { size: 10 }, - 'another_directory/'=> {}, + 'path/second_dir/dir_3/file_3' => { size: 10 }, + 'another_directory/' => {}, 'another_file' => {}, '/file/with/absolute_path' => {} } end diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb index 5852b31ab3..3325190789 100644 --- a/spec/lib/gitlab/lfs/lfs_router_spec.rb +++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb @@ -26,8 +26,8 @@ describe Gitlab::Lfs::Router, lib: true do let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_size) { 499013 } - let(:respond_with_deprecated) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} - let(:respond_with_disabled) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} + let(:respond_with_deprecated) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} + let(:respond_with_disabled) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} describe 'when lfs is disabled' do before do diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ad47e338a3..ccb100cd96 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -56,7 +56,7 @@ describe Commit, models: true do end it "does not truncates a message with a newline after 80 but less 100 characters" do - message =<'application/json', 'X-Gitlab-Event'=>'Service Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' } ).once end it "POSTs the data as JSON" do @service_hook.execute(@data) expect(WebMock).to have_requested(:post, @service_hook.url).with( - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' } ).once end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 56a9fbe972..4078b9e4ff 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -33,7 +33,7 @@ describe SystemHook, models: true do Projects::CreateService.new(user, name: 'empty').execute expect(WebMock).to have_requested(:post, system_hook.url).with( body: /project_create/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -42,7 +42,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /project_destroy/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -51,7 +51,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_create/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -60,7 +60,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_destroy/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -69,7 +69,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_add_to_team/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -79,7 +79,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_team/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -88,7 +88,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /group_create/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -97,7 +97,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /group_destroy/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -106,7 +106,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_add_to_group/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -116,7 +116,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_group/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index d37590cab7..379c3e1219 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -65,7 +65,7 @@ describe SlackService::NoteMessage, models: true do expect(message.pretext).to eq("Test User commented on " \ " in : " \ "*merge request title*") - expected_attachments = [ + expected_attachments = [ { text: "comment on a merge request", color: color, @@ -117,7 +117,7 @@ describe SlackService::NoteMessage, models: true do expect(message.pretext).to eq("Test User commented on " \ " in : " \ "*snippet title*") - expected_attachments = [ + expected_attachments = [ { text: "comment on a snippet", color: color, diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 5ead735be4..0fbc984c06 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -106,8 +106,8 @@ describe API::API, api: true do context 'authorized user' do let(:download_headers) do - { 'Content-Transfer-Encoding'=>'binary', - 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } end it 'should return specific build artifacts' do diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb index 96d89e6920..02553d0f8e 100644 --- a/spec/requests/api/group_members_spec.rb +++ b/spec/requests/api/group_members_spec.rb @@ -34,11 +34,11 @@ describe API::API, api: true do expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.size).to eq(5) - expect(json_response.find { |e| e['id']==owner.id }['access_level']).to eq(GroupMember::OWNER) - expect(json_response.find { |e| e['id']==reporter.id }['access_level']).to eq(GroupMember::REPORTER) - expect(json_response.find { |e| e['id']==developer.id }['access_level']).to eq(GroupMember::DEVELOPER) - expect(json_response.find { |e| e['id']==master.id }['access_level']).to eq(GroupMember::MASTER) - expect(json_response.find { |e| e['id']==guest.id }['access_level']).to eq(GroupMember::GUEST) + expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER) + expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER) + expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER) + expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER) + expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST) end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index dfd361a2cd..cae4656010 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -402,8 +402,8 @@ describe Ci::API::API do context 'build has artifacts' do let(:build) { create(:ci_build, :artifacts) } let(:download_headers) do - { 'Content-Transfer-Encoding'=>'binary', - 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } end it 'should download artifact' do diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index a3f496359b..5ebe095743 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -2,11 +2,11 @@ module JiraServiceHelper def jira_service_settings properties = { - "title"=>"JIRA tracker", - "project_url"=>"http://jira.example/issues/?jql=project=A", - "issues_url"=>"http://jira.example/browse/JIRA-1", - "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa", - "api_url"=>"http://jira.example/rest/api/2" + "title" => "JIRA tracker", + "project_url" => "http://jira.example/issues/?jql=project=A", + "issues_url" => "http://jira.example/browse/JIRA-1", + "new_issue_url" => "http://jira.example/secure/CreateIssue.jspa", + "api_url" => "http://jira.example/rest/api/2" } jira_tracker.update_attributes(properties: properties, active: true) diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index eec2e68111..b5ca34bc02 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -36,20 +36,20 @@ module StubGitlabCalls stub_request(:post, "#{gitlab_url}api/v3/session.json"). with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}", - headers: { 'Content-Type'=>'application/json' }). - to_return(status: 201, body: f, headers: { 'Content-Type'=>'application/json' }) + headers: { 'Content-Type' => 'application/json' }). + to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_user f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json')) stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + with(headers: { 'Content-Type' => 'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token"). - with(headers: { 'Content-Type'=>'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + with(headers: { 'Content-Type' => 'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_project_8 @@ -66,19 +66,19 @@ module StubGitlabCalls f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json')) stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + with(headers: { 'Content-Type' => 'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_projects_owned stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). + with(headers: { 'Content-Type' => 'application/json' }). to_return(status: 200, body: "", headers: {}) end def stub_ci_enable stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). + with(headers: { 'Content-Type' => 'application/json' }). to_return(status: 200, body: "", headers: {}) end From 8d58e691c69f623878522c4f0dccf9f24229e9ce Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Fri, 13 May 2016 04:51:13 -0300 Subject: [PATCH 120/199] remove extra space to fix codestyle --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 0c01c52921..9e776a1f54 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,7 +74,7 @@ Rails.application.routes.draw do end # Health check - get 'health_check(/:checks)' => 'health_check#index', as: :health_check + get 'health_check(/:checks)' => 'health_check#index', as: :health_check # Enable Grack support mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] From 8eb67187d04f49f88afc13cdc3d0d591bb1e2a20 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 13:12:21 +0200 Subject: [PATCH 121/199] removed transaction stuff --- app/controllers/projects/imports_controller.rb | 4 +--- app/services/projects/create_service.rb | 8 +------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index d353a39034..a1b84afcd9 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -19,9 +19,7 @@ class Projects::ImportsController < Projects::ApplicationController if @project.import_failed? @project.import_retry else - Project.transaction do - @project.import_start - end + @project.import_start @project.add_import_job end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 2687544ae6..6728fabea1 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -52,7 +52,7 @@ module Projects save_project_and_import_data(import_data) - start_import if @project.import? + @project.import_start if @project.import? after_create_actions if @project.persisted? @@ -94,12 +94,6 @@ module Projects end end - def start_import - Project.transaction do - @project.import_start - end - end - def save_project_and_import_data(import_data) Project.transaction do @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data From ae89161db5d6294948990b1d87f4fa306f2a1be9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 May 2016 09:25:38 -0500 Subject: [PATCH 122/199] Remove background color differentiation from gitlab logo/text in sidebar Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/gitlab-theme.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index dc4668877f..51a17d1469 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -9,9 +9,6 @@ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { .page-with-sidebar { .header-logo { - background-color: $color; - border-color: $color; - a { color: $color-light; @@ -21,7 +18,7 @@ } &:hover { - background-color: $color-darker; + background-color: $color-dark; a { color: #fff; From 6da24056030ccf45ec39f2f3b6addc6f0ffe1d1f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 May 2016 10:05:52 -0500 Subject: [PATCH 123/199] Add UI changes in 8.8 to changelog Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 588a4b4eef..30d4955c9f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -49,6 +49,8 @@ v 8.8.0 (unreleased) - Total method execution timings are no longer tracked - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034. (Andrei Gliga) - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) + - Hide left sidebar on phone screens to give more space for content + - Redesign navigation for profile and group pages v 8.7.5 - Fix relative links in wiki pages. !4050 From 16b1d3a34f665776bbdfca041326b9c2d569e59b Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 13 May 2016 11:12:49 -0500 Subject: [PATCH 124/199] Fix mobile dropdown bug --- app/assets/stylesheets/framework/nav.scss | 19 +++++++++++++++++++ app/helpers/nav_helper.rb | 8 ++++++++ app/views/layouts/_page.html.haml | 2 +- .../layouts/nav/_group_settings.html.haml | 2 +- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index f2d7dc625e..a81fcb1c6b 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -209,6 +209,15 @@ float: right; padding: 7px 0 0; + @media (max-width: $screen-xs-min) { + float: none; + padding: 0 9px; + + .dropdown-new { + width: 100%; + } + } + i { color: $layout-link-gray; } @@ -225,6 +234,10 @@ .dropdown { margin-left: 7px; + + @media (max-width: $screen-xs-min) { + margin-left: 0; + } } } @@ -260,4 +273,10 @@ .page-with-layout-nav { margin-top: 50px; + + &.controls-dropdown-visible { + @media (max-width: $screen-xs-min) { + margin-top: 96px; + } + } } diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 3aa4103045..fbb799eecd 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -43,4 +43,12 @@ module NavHelper class_name += " with-horizontal-nav" if defined?(nav) && nav class_name end + + def layout_nav_class + "page-with-layout-nav" if defined?(nav) && nav + end + + def layout_dropdown_class + "controls-dropdown-visible" if current_user + end end diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 3c0660d34f..5be0b546a6 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -25,7 +25,7 @@ .layout-nav .container-fluid = render "layouts/nav/#{nav}" - .content-wrapper{ class: ('page-with-layout-nav' if defined?(nav) && nav) } + .content-wrapper{ class: "#{layout_nav_class} #{layout_dropdown_class}" } = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index e391ec7f2b..0b2673f1a8 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,7 +1,7 @@ - if current_user - if access = @group.users.find_by(id: current_user.id) .controls - %span.dropdown.group-settings-dropdown + .dropdown.group-settings-dropdown %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} = icon('cog') = icon('caret-down') From 2148566b3dce96f36bbf86555ef0d68758044429 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 May 2016 11:21:31 -0500 Subject: [PATCH 125/199] Fix issue sidebar top position after header height change Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/header.scss | 4 ++-- app/assets/stylesheets/pages/issuable.scss | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 8190a97ed5..0da96c4017 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -6,12 +6,12 @@ header { transition-duration: .3s; &.navbar-empty { - height: 58px; + height: $header-height; background: #fff; border-bottom: 1px solid $btn-gray-hover; .center-logo { - margin: 11px 0; + margin: 8px 0; text-align: center; #tanuki-logo, img { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 1cf3023ecc..d06086a581 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -125,7 +125,7 @@ .right-sidebar { position: fixed; - top: 58px; + top: $header-height; bottom: 0; right: 0; z-index: 10; From 9a7a0415535da7541095de363dee37b9f09fe702 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 May 2016 11:24:00 -0500 Subject: [PATCH 126/199] Fix login btn vertical align Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/common.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 3386523dbf..f8aecd0558 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -289,7 +289,7 @@ table { text-shadow: none; @media (min-width: $screen-sm-min) { - margin-top: 11px; + margin-top: 8px; } } From 935c684ad3427069431a1130d364a4481af6cb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 13 May 2016 18:39:32 +0200 Subject: [PATCH 127/199] Revert "Merge branch 'doc-fix-db-gem-install-instructions' into 'master' " This reverts commit 66d8ef1ad9d5952b7e7f026658f71aafc861950e, reversing changes made to 6a8359f3d3be01af6f5b124b61af7ee1c77c17d0. The `--with` option was added in Bundler 1.10 but in this case it shouldn't even be needed: I've tested the original command locally and the pg gem was installed, so I believe the user that opened the original MR ran the wrong command (i.e. `sudo -u git -H bundle install --without development test postgres --deployment` instead of `sudo -u git -H bundle install --without development test mysql --deployment`. See discussion at https://gitlab.com/gitlab-org/gitlab-ce/commit/66d8ef1ad9d5952b7e7f026658f71aafc861950e for more context. --- doc/update/patch_versions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index b4283a526f..60729316cd 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -57,10 +57,10 @@ sudo -u git -H make cd /home/git/gitlab # PostgreSQL -sudo -u git -H bundle install --without development test mysql --with postgres --deployment +sudo -u git -H bundle install --without development test mysql --deployment # MySQL -sudo -u git -H bundle install --without development test postgres --with mysql --deployment +sudo -u git -H bundle install --without development test postgres --deployment # Optional: clean up old gems sudo -u git -H bundle clean From e5c97101478829fe8200557afd02bde92c3eb4e4 Mon Sep 17 00:00:00 2001 From: Artem Sidorenko Date: Tue, 3 May 2016 17:33:43 +0200 Subject: [PATCH 128/199] Use the relative url prefix for links in Wiki --- CHANGELOG | 3 +++ app/models/project_wiki.rb | 2 +- lib/gitlab/url_builder.rb | 2 +- spec/models/project_wiki_spec.rb | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..5ffe91ce4e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -52,6 +52,9 @@ v 8.8.0 (unreleased) - Hide left sidebar on phone screens to give more space for content - Redesign navigation for profile and group pages +v 8.7.6 + - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) + v 8.7.5 - Fix relative links in wiki pages. !4050 - Fix always showing build notification message when switching between merge requests !4086 diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 060ed9b44e..339fb0b9f9 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -40,7 +40,7 @@ class ProjectWiki end def wiki_base_path - ["/", @project.path_with_namespace, "/wikis"].join('') + [Gitlab.config.gitlab.relative_url_root, "/", @project.path_with_namespace, "/wikis"].join('') end # Returns the Gollum::Wiki object. diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 2bbbd3074e..fe65c24610 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -62,7 +62,7 @@ module Gitlab end def wiki_page_url - "#{Gitlab.config.gitlab.url}#{object.wiki.wiki_base_path}/#{object.slug}" + namespace_project_wiki_url(object.wiki.project.namespace, object.wiki.project, object.slug) end end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 532e3f013f..91ebb612ba 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -38,7 +38,8 @@ describe ProjectWiki, models: true do describe "#wiki_base_path" do it "returns the wiki base path" do - wiki_base_path = "/#{project.path_with_namespace}/wikis" + wiki_base_path = "#{Gitlab.config.gitlab.relative_url_root}/#{project.path_with_namespace}/wikis" + expect(subject.wiki_base_path).to eq(wiki_base_path) end end From 9ef9e008feb99aaf0c4edc85bb76039eb46f0794 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 16:22:50 -0500 Subject: [PATCH 129/199] Move JWT to Gitlab::JWT --- app/controllers/jwt_controller.rb | 2 +- ...ntainer_registry_authentication_service.rb | 118 +++++++++--------- 2 files changed, 61 insertions(+), 59 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 599f62bd12..c203c50d1f 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -3,7 +3,7 @@ class JwtController < ApplicationController skip_before_action :verify_authenticity_token SERVICES = { - 'container_registry' => JWT::ContainerRegistryAuthenticationService, + 'container_registry' => ::Gitlab::JWT::ContainerRegistryAuthenticationService, } def auth diff --git a/app/services/jwt/container_registry_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb index 0ab3e6d02b..dd0f295478 100644 --- a/app/services/jwt/container_registry_authentication_service.rb +++ b/app/services/jwt/container_registry_authentication_service.rb @@ -1,69 +1,71 @@ -module JWT - class ContainerRegistryAuthenticationService < BaseService - def execute - if params[:offline_token] - return error('forbidden', 403) unless current_user +module Gitlab + module JWT + class ContainerRegistryAuthenticationService < BaseService + def execute + if params[:offline_token] + return error('forbidden', 403) unless current_user + end + + return error('forbidden', 401) if scopes.blank? + + { token: authorized_token(scopes).encoded } end - return error('forbidden', 401) if scopes.blank? + private - { token: authorized_token(scopes).encoded } - end - - private - - def authorized_token(access) - token = ::JWT::RSAToken.new(registry.key) - token.issuer = registry.issuer - token.audience = params[:service] - token.subject = current_user.try(:username) - token[:access] = access - token - end - - def scopes - return unless params[:scope] - - @scopes ||= begin - scope = process_scope(params[:scope]) - [scope].compact - end - end - - def process_scope(scope) - type, name, actions = scope.split(':', 3) - actions = actions.split(',') - - case type - when 'repository' - process_repository_access(type, name, actions) - end - end - - def process_repository_access(type, name, actions) - requested_project = Project.find_with_namespace(name) - return unless requested_project - - actions = actions.select do |action| - can_access?(requested_project, action) + def authorized_token(access) + token = ::JWT::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token[:access] = access + token end - { type: type, name: name, actions: actions } if actions.present? - end + def scopes + return unless params[:scope] - def can_access?(requested_project, requested_action) - case requested_action - when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) - when 'push' - requested_project == project || can?(current_user, :create_container_registry, requested_project) - else - false + @scopes ||= begin + scope = process_scope(params[:scope]) + [scope].compact + end end - end - def registry - Gitlab.config.registry + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + requested_project = Project.find_with_namespace(name) + return unless requested_project + + actions = actions.select do |action| + can_access?(requested_project, action) + end + + { type: type, name: name, actions: actions } if actions.present? + end + + def can_access?(requested_project, requested_action) + case requested_action + when 'pull' + requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) + when 'push' + requested_project == project || can?(current_user, :create_container_registry, requested_project) + else + false + end + end + + def registry + Gitlab.config.registry + end end end end From e900ff972a4a6133a499adcc1263d3634863f410 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 16:23:02 -0500 Subject: [PATCH 130/199] Improve JwtController code --- app/controllers/jwt_controller.rb | 37 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index c203c50d1f..e067f59808 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -1,22 +1,13 @@ class JwtController < ApplicationController skip_before_action :authenticate_user! skip_before_action :verify_authenticity_token + before_action :authenticate_project_or_user SERVICES = { 'container_registry' => ::Gitlab::JWT::ContainerRegistryAuthenticationService, } def auth - @authenticated = authenticate_with_http_basic do |login, password| - # if it's possible we first try to authenticate project with login and password - @project = authenticate_project(login, password) - @user = authenticate_user(login, password) unless @project - end - - unless @authenticated - head :forbidden if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) - end - service = SERVICES[params[:service]] head :not_found unless service @@ -28,19 +19,28 @@ class JwtController < ApplicationController private + def authenticate_project_or_user + authenticate_with_http_basic do |login, password| + # if it's possible we first try to authenticate project with login and password + @project = authenticate_project(login, password) + return if @project + + @user = authenticate_user(login, password) + return if @user + end + + if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) + head :forbidden + end + end + def auth_params params.permit(:service, :scope, :offline_token, :account, :client_id) end def authenticate_project(login, password) - matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) - - if matched_login.present? - underscored_service = matched_login['s'].underscore - - if underscored_service == 'gitlab_ci' - Project.find_by(builds_enabled: true, runners_token: password) - end + if login == 'gitlab_ci_token' + Project.find_by(builds_enabled: true, runners_token: password) end end @@ -77,6 +77,7 @@ class JwtController < ApplicationController if banned Rails.logger.info "IP #{request.ip} failed to login " \ "as #{login} but has been temporarily banned from Git auth" + return end end end From 9eb4569ed9330d65fb24e0272402fe5ac739b8be Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 13 May 2016 16:17:01 -0500 Subject: [PATCH 131/199] Change all occurrences of doc.gitlab.com to docs.gitlab.com [ci skip] --- doc/administration/environment_variables.md | 2 +- doc/administration/high_availability/load_balancer.md | 2 +- doc/administration/high_availability/nfs.md | 2 +- doc/api/services.md | 2 +- doc/development/doc_styleguide.md | 4 ++-- doc/gitlab-basics/create-issue.md | 2 +- doc/gitlab-basics/create-project.md | 2 +- doc/hooks/custom_hooks.md | 2 +- doc/install/installation.md | 2 +- doc/install/relative_url.md | 2 +- doc/integration/README.md | 2 +- doc/intro/README.md | 2 +- doc/logs/logs.md | 2 +- doc/raketasks/README.md | 2 +- doc/update/README.md | 6 +++--- doc/workflow/gitlab_flow.md | 4 ++-- doc/workflow/groups.md | 2 +- doc/workflow/importing/import_projects_from_github.md | 2 +- doc/workflow/importing/import_projects_from_gitlab_com.md | 2 +- doc/workflow/lfs/manage_large_binaries_with_git_lfs.md | 2 +- 20 files changed, 24 insertions(+), 24 deletions(-) diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index 43ab153d76..7f53915a4d 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -58,4 +58,4 @@ to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`. It's possible to preconfigure the GitLab docker image by adding the environment variable `GITLAB_OMNIBUS_CONFIG` to the `docker run` command. -For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://doc.gitlab.com/omnibus/docker/#preconfigure-docker-container). +For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://docs.gitlab.com/omnibus/docker/#preconfigure-docker-container). diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index b1fe34ed9a..136f570ac2 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -60,4 +60,4 @@ Read more on high-availability configuration: configure custom domains with custom SSL, which would not be possible if SSL was terminated at the load balancer. -[gitlab-pages]: http://doc.gitlab.com/ee/pages/administration.html +[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index e4e124e200..49ff5d536a 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -113,4 +113,4 @@ Read more on high-availability configuration: 1. [Configure the GitLab application servers](gitlab.md) 1. [Configure the load balancers](load_balancer.md) -[udp-log-shipping]: http://doc.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" +[udp-log-shipping]: http://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" diff --git a/doc/api/services.md b/doc/api/services.md index 7d45b2cf46..83ac784515 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -491,7 +491,7 @@ Jira issue tracker Set JIRA service for a project. -> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html) +> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html) ``` PUT /projects/:id/services/jira diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 187ec9e7b7..8292b39375 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -127,7 +127,7 @@ Inside the document: ``` If the document you are editing resides in a place other than the GitLab CE/EE `doc/` directory, instead of the relative link, use the full path: - `http://doc.gitlab.com/ce/administration/restart_gitlab.html`. + `http://docs.gitlab.com/ce/administration/restart_gitlab.html`. Replace `reconfigure` with `restart` where appropriate. ## Installation guide @@ -266,5 +266,5 @@ curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "restricted_signup_domai [cURL]: http://curl.haxx.se/ "cURL website" [single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html -[gfm]: http://doc.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" +[gfm]: http://docs.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" [doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation" diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index 87f078def0..5221d85b66 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all ![Submit new issue](basicsimages/submit_new_issue.png) -Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://doc.gitlab.com/ce/customization/issue_closing.html). +Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html). diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index b545d62549..f737dffc02 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -14,7 +14,7 @@ Fill out the required information: 1. Select a [visibility level](https://gitlab.com/help/public_access/public_access) -1. You can also [import your existing projects](http://doc.gitlab.com/ce/workflow/importing/README.html) +1. You can also [import your existing projects](http://docs.gitlab.com/ce/workflow/importing/README.html) 1. Click on "create project" diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index dcdf49d337..820934f97f 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** +Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://docs.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. diff --git a/doc/install/installation.md b/doc/install/installation.md index 44ae0be406..fa11eb9ba6 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -6,7 +6,7 @@ Since an installation from source is a lot of work and error prone we strongly r One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes. On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time. -Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://doc.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. +Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://docs.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. After this termination Runit will detect Sidekiq is not running and will start it. Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time. diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md index 0245febfcd..44d2a14f36 100644 --- a/doc/install/relative_url.md +++ b/doc/install/relative_url.md @@ -132,5 +132,5 @@ To disable the relative URL: 1. Follow the same as above starting from 2. and set up the GitLab URL to one that doesn't contain a relative path. -[omnibus-rel]: http://doc.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" +[omnibus-rel]: http://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" [restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab" diff --git a/doc/integration/README.md b/doc/integration/README.md index 6fe04aa2a0..fd330dd7a7 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -19,7 +19,7 @@ See the documentation below for details on how to configure these services. GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. -[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html +[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html ## Project services diff --git a/doc/intro/README.md b/doc/intro/README.md index ab298d3808..382d10aaf4 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -39,4 +39,4 @@ Install and update your GitLab installation. - [Install GitLab](https://about.gitlab.com/installation/) - [Update GitLab](https://about.gitlab.com/update/) -- [Explore Omnibus GitLab configuration options](http://doc.gitlab.com/omnibus/settings/configuration.html) +- [Explore Omnibus GitLab configuration options](http://docs.gitlab.com/omnibus/settings/configuration.html) diff --git a/doc/logs/logs.md b/doc/logs/logs.md index 27937e5176..ef5affa2eb 100644 --- a/doc/logs/logs.md +++ b/doc/logs/logs.md @@ -1,6 +1,6 @@ ## Log system GitLab has advanced log system so everything is logging and you can analize your instance using various system log files. -In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://doc.gitlab.com/ee/administration/audit_events.html) +In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://docs.gitlab.com/ee/administration/audit_events.html) System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 6be954ad68..a49c43b8ef 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -8,4 +8,4 @@ - [User management](user_management.md) - [Webhooks](web_hooks.md) - [Import](import.md) of git repositories in bulk -- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators +- [Rebuild authorized_keys file](http://docs.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators diff --git a/doc/update/README.md b/doc/update/README.md index a770633c9b..975d72164b 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -29,7 +29,7 @@ Based on your installation, choose a section below that fits your needs. ## Omnibus Packages -- The [Omnibus update guide](http://doc.gitlab.com/omnibus/update/README.html) +- The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html) contains the steps needed to update an Omnibus GitLab package. ## Installation from source @@ -86,10 +86,10 @@ possible. information about configuring GitLab to work with a MySQL database. - [Restoring from backup after a failed upgrade](restore_after_failure.md) -[omnidocker]: http://doc.gitlab.com/omnibus/docker/README.html +[omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html [source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update [source-ce]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update [ee-ce]: ../downgrade_ee_to_ce/README.md [ce]: https://about.gitlab.com/features/#community [ee]: https://about.gitlab.com/features/#enterprise -[omni-ce-ee]: http://doc.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition +[omni-ce-ee]: http://docs.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 1b354bcc0f..2b2f140f8b 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged. If the assigned person does not feel comfortable they can close the merge request without merging. -In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://doc.gitlab.com/ce/permissions/permissions.html). +In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html). So if you want to merge it into a protected branch you assign it to someone with master authorizations. ## Issues with GitLab flow @@ -187,7 +187,7 @@ If you have an issue that spans across multiple repositories, the best thing is ![Vim screen showing the rebase view](rebase.png) With git you can use an interactive rebase (`rebase -i`) to squash multiple commits into one and reorder them. -In GitLab EE and .com you can also [rebase before merge](http://doc.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. +In GitLab EE and .com you can also [rebase before merge](http://docs.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical. However you should never rebase commits you have pushed to a remote server. Somebody can have referred to the commits or cherry-picked them. diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md index 52bf611dc5..34ada1774d 100644 --- a/doc/workflow/groups.md +++ b/doc/workflow/groups.md @@ -54,7 +54,7 @@ If necessary, you can increase the access level of an individual user for a spec ## Managing group memberships via LDAP In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. -See [the GitLab Enterprise Edition documentation](http://doc.gitlab.com/ee/integration/ldap.html) for more information. +See [the GitLab Enterprise Edition documentation](http://docs.gitlab.com/ee/integration/ldap.html) for more information. ## Allowing only admins to create groups diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index e670e415c7..a7dfac2c12 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -44,5 +44,5 @@ case the namespace is taken, the project will be imported on the user's namespace. [gh-import]: ../../integration/github.md "GitHub integration" -[ee-gh]: http://doc.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE" +[ee-gh]: http://docs.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE" [new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab" diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index 1117db98e7..dcc00074b7 100644 --- a/doc/workflow/importing/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -2,7 +2,7 @@ You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if GitLab support is enabled on your GitLab instance. -You can read more about GitLab support [here](http://doc.gitlab.com/ce/integration/gitlab.html) +You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html) To get to the importer page you need to go to "New project" page. ![New project page](gitlab_importer/new_project_page.png) 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 83db44c10b..1295dfbd77 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -4,7 +4,7 @@ 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://doc.gitlab.com/ee/workflow/git_annex.html) +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. From 509654b3784da2a084a0c8303e9c6cc1498b8d2b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 16:41:30 -0500 Subject: [PATCH 132/199] Improve JWT::RSAToken::kid --- lib/jwt/rsa_token.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb index 0438135ad5..4de89bf0d3 100644 --- a/lib/jwt/rsa_token.rb +++ b/lib/jwt/rsa_token.rb @@ -24,11 +24,13 @@ module JWT @key ||= OpenSSL::PKey::RSA.new(key_data) end + def public_key + key.public_key + end + def kid - sha256 = Digest::SHA256.new - sha256.update(key.public_key.to_der) - payload = StringIO.new(sha256.digest).read(30) - Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| + fingerprint = Digest::SHA256.digest(public_key.to_der) + Base32.encode(fingerprint).split('').each_slice(4).each_with_object([]) do |slice, mem| mem << slice.join end.join(':') end From 8532dc0d774d43e2e23ab169d5af0c2ab12c139c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 21 Apr 2016 17:16:22 -0300 Subject: [PATCH 133/199] Import pull requests from GitHub where the source branch was removed --- lib/gitlab/github_import/importer.rb | 44 ++++++++++++------- .../github_import/pull_request_formatter.rb | 26 +++++++---- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 0f9e3ee14e..a3f2789178 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,12 +3,15 @@ module Gitlab class Importer include Gitlab::ShellAdapter - attr_reader :project, :client + attr_reader :client, :project, :repo, :repo_url def initialize(project) @project = project - if import_data_credentials - @client = Client.new(import_data_credentials[:user]) + @repo = project.import_source + @repo_url = project.import_url + + if credentials + @client = Client.new(credentials[:user]) @formatter = Gitlab::ImportFormatter.new else raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" @@ -22,8 +25,8 @@ module Gitlab private - def import_data_credentials - @import_data_credentials ||= project.import_data.credentials if project.import_data + def credentials + @credentials ||= project.import_data.credentials if project.import_data end def import_labels @@ -68,22 +71,31 @@ module Gitlab end def import_pull_requests - client.pull_requests(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| - pull_request = PullRequestFormatter.new(project, raw_data) + pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc) + .map { |raw| PullRequestFormatter.new(project, raw) } + .reject(&:cross_project?) - if pull_request.valid? - merge_request = MergeRequest.new(pull_request.attributes) + source_branches_removed = pull_requests.reject(&:source_branch_exists?) + source_branches_removed.each do |pull_request| + client.create_ref(repo, "refs/heads/#{pull_request.source_branch}", pull_request.source_sha) + end - if merge_request.save - apply_labels(pull_request.number, merge_request) - import_comments(pull_request.number, merge_request) - import_comments_on_diff(pull_request.number, merge_request) - end + project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') + + pull_requests.each do |pull_request| + merge_request = MergeRequest.new(pull_request.attributes) + + if merge_request.save + apply_labels(pull_request.number, merge_request) + import_comments(pull_request.number, merge_request) + import_comments_on_diff(pull_request.number, merge_request) end end + source_branches_removed.each do |pull_request| + client.delete_ref(repo, "heads/#{pull_request.source_branch}") + end + true rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index d21b942ad4..be970f724c 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -7,7 +7,7 @@ module Gitlab title: raw_data.title, description: description, source_project: source_project, - source_branch: source_branch.name, + source_branch: source_branch, target_project: target_project, target_branch: target_branch.name, state: state, @@ -27,6 +27,22 @@ module Gitlab !cross_project? && source_branch.present? && target_branch.present? end + def cross_project? + source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + end + + def source_branch_exists? + source_project.repository.branch_names.include?(source_branch) + end + + def source_branch + raw_data.head.ref + end + + def source_sha + raw_data.head.sha + end + private def assigned? @@ -51,10 +67,6 @@ module Gitlab raw_data.body || "" end - def cross_project? - source_repo.present? && target_repo.present? && source_repo.id != target_repo.id - end - def description formatter.author_line(author) + body end @@ -73,10 +85,6 @@ module Gitlab raw_data.head.repo end - def source_branch - source_project.repository.find_branch(raw_data.head.ref) - end - def target_project project end From 9b4dc552cb51faee38baffa6f29954d795282658 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 22 Apr 2016 15:25:04 -0300 Subject: [PATCH 134/199] Import pull requests from GitHub where the target branch was removed --- lib/gitlab/github_import/importer.rb | 25 +++++++++++++------ .../github_import/pull_request_formatter.rb | 18 +++++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index a3f2789178..bc25fc8c0b 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -75,10 +75,11 @@ module Gitlab .map { |raw| PullRequestFormatter.new(project, raw) } .reject(&:cross_project?) - source_branches_removed = pull_requests.reject(&:source_branch_exists?) - source_branches_removed.each do |pull_request| - client.create_ref(repo, "refs/heads/#{pull_request.source_branch}", pull_request.source_sha) - end + source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch, pr.source_sha] } + target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch, pr.target_sha] } + branches_removed = source_branches_removed | target_branches_removed + + create_refs(branches_removed) project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') @@ -92,15 +93,25 @@ module Gitlab end end - source_branches_removed.each do |pull_request| - client.delete_ref(repo, "heads/#{pull_request.source_branch}") - end + delete_refs(branches_removed) true rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message end + def create_refs(branches) + branches.each do |branch| + client.create_ref(repo, "refs/heads/#{branch.first}", branch.last) + end + end + + def delete_refs(branches) + branches.each do |branch| + client.delete_ref(repo, "heads/#{branch.first}") + end + end + def apply_labels(number, issuable) issue = client.issue(project.import_source, number) diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index be970f724c..ef32a23b04 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -9,7 +9,7 @@ module Gitlab source_project: source_project, source_branch: source_branch, target_project: target_project, - target_branch: target_branch.name, + target_branch: target_branch, state: state, milestone: milestone, author_id: author_id, @@ -43,6 +43,18 @@ module Gitlab raw_data.head.sha end + def target_branch_exists? + target_project.repository.branch_names.include?(target_branch) + end + + def target_branch + raw_data.base.ref + end + + def target_sha + raw_data.base.sha + end + private def assigned? @@ -93,10 +105,6 @@ module Gitlab raw_data.base.repo end - def target_branch - target_project.repository.find_branch(raw_data.base.ref) - end - def state @state ||= case true when raw_data.state == 'closed' && raw_data.merged_at.present? From 3b50867550818413eed01fcbe85e5442707150be Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 26 Apr 2016 12:03:48 -0300 Subject: [PATCH 135/199] Fix spec for Gitlab::GithubImport::PullRequestFormatter --- lib/gitlab/github_import/importer.rb | 2 +- .../github_import/pull_request_formatter.rb | 10 +- .../pull_request_formatter_spec.rb | 104 +++++++++++++----- 3 files changed, 82 insertions(+), 34 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index bc25fc8c0b..9aea947a81 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -73,7 +73,7 @@ module Gitlab def import_pull_requests pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc) .map { |raw| PullRequestFormatter.new(project, raw) } - .reject(&:cross_project?) + .select(&:valid?) source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch, pr.source_sha] } target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch, pr.target_sha] } diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index ef32a23b04..f242596bdf 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -24,11 +24,7 @@ module Gitlab end def valid? - !cross_project? && source_branch.present? && target_branch.present? - end - - def cross_project? - source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + !cross_project? end def source_branch_exists? @@ -79,6 +75,10 @@ module Gitlab raw_data.body || "" end + def cross_project? + source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + end + def description formatter.author_line(author) + body end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index e59c0ca110..b1f3d17373 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -4,9 +4,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:project) { create(:project) } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } - let(:source_branch) { double(ref: 'feature', repo: source_repo) } + let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } let(:target_repo) { repository } - let(:target_branch) { double(ref: 'master', repo: target_repo) } + let(:target_branch) { double(ref: 'master', repo: target_repo, sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7') } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -137,11 +137,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:milestone) { double(number: 45) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } - it 'returns nil when milestone does not exists' do + it 'returns nil when milestone does not exist' do expect(pull_request.attributes.fetch(:milestone)).to be_nil end - it 'returns milestone when is exists' do + it 'returns milestone when it exists' do milestone = create(:milestone, project: project, iid: 45) expect(pull_request.attributes.fetch(:milestone)).to eq milestone @@ -157,32 +157,80 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + describe '#source_branch' do + let(:raw_data) { double(base_data) } + + it 'returns head ref' do + expect(pull_request.source_branch).to eq 'feature' + end + end + + describe '#source_sha' do + let(:raw_data) { double(base_data) } + + it 'returns head sha' do + expect(pull_request.source_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end + end + + describe '#source_branch_exists?' do + context 'when source branch exists' do + let(:raw_data) { double(base_data) } + + it 'returns true' do + expect(pull_request.source_branch_exists?).to eq true + end + end + + context 'when source branch does not exist' do + let(:raw_data) { double(base_data.merge(head: double(ref: 'removed-branch').as_null_object)) } + + it 'returns false' do + expect(pull_request.source_branch_exists?).to eq false + end + end + end + + describe '#target_branch' do + let(:raw_data) { double(base_data) } + + it 'returns base ref' do + expect(pull_request.target_branch).to eq 'master' + end + end + + describe '#target_sha' do + let(:raw_data) { double(base_data) } + + it 'returns base sha' do + expect(pull_request.target_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7' + end + end + + describe '#target_branch_exists?' do + context 'when target branch exists' do + let(:raw_data) { double(base_data) } + + it 'returns true' do + expect(pull_request.target_branch_exists?).to eq true + end + end + + context 'when target branch does not exist' do + let(:raw_data) { double(base_data.merge(base: double(ref: 'removed-branch').as_null_object)) } + + it 'returns false' do + expect(pull_request.target_branch_exists?).to eq false + end + end + end + describe '#valid?' do - let(:invalid_branch) { double(ref: 'invalid-branch').as_null_object } + context 'when source, and target repos are not a fork' do + let(:raw_data) { double(base_data) } - context 'when source, and target repositories are the same' do - context 'and source and target branches exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: target_branch)) } - - it 'returns true' do - expect(pull_request.valid?).to eq true - end - end - - context 'and source branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: invalid_branch, base: target_branch)) } - - it 'returns false' do - expect(pull_request.valid?).to eq false - end - end - - context 'and target branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: invalid_branch)) } - - it 'returns false' do - expect(pull_request.valid?).to eq false - end + it 'returns true' do + expect(pull_request.valid?).to eq true end end From 99d3e21f19ffb5cccb58fdfeac4fb6174e7e65e2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 10 May 2016 17:41:46 -0500 Subject: [PATCH 136/199] Extract LegacyDiffNote out of Note --- app/assets/javascripts/notes.js.coffee | 2 + app/assets/stylesheets/pages/notes.scss | 3 +- app/controllers/projects/commit_controller.rb | 11 +- .../projects/compare_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 7 +- app/controllers/projects/notes_controller.rb | 6 +- app/finders/notes_finder.rb | 2 +- app/helpers/diff_helper.rb | 22 +- app/helpers/notes_helper.rb | 29 +-- app/models/legacy_diff_note.rb | 182 ++++++++++++++ app/models/note.rb | 222 +++--------------- .../notify/note_merge_request_email.html.haml | 2 +- .../projects/diffs/_parallel_view.html.haml | 10 +- app/views/projects/diffs/_text_file.html.haml | 9 +- .../notes/_commit_discussion.html.haml | 0 .../notes/_diff_notes_with_reply.html.haml | 18 +- .../_diff_notes_with_reply_parallel.html.haml | 20 +- .../projects/notes/_discussion.html.haml | 47 +++- app/views/projects/notes/_form.html.haml | 1 + app/views/projects/notes/_note.html.haml | 9 +- app/views/projects/notes/_notes.html.haml | 9 +- .../notes/discussions/_active.html.haml | 16 -- .../notes/discussions/_commit.html.haml | 25 -- .../notes/discussions/_diff.html.haml | 28 --- .../_legacy_diff_with_notes.html.haml | 30 +++ .../notes/discussions/_notes.html.haml | 7 + .../notes/discussions/_outdated.html.haml | 14 -- .../20160508215820_add_type_to_notes.rb | 5 + ...508221410_set_type_on_legacy_diff_notes.rb | 5 + db/schema.rb | 5 +- lib/api/commits.rb | 2 + lib/api/entities.rb | 6 +- spec/models/note_spec.rb | 8 +- 33 files changed, 390 insertions(+), 374 deletions(-) create mode 100644 app/models/legacy_diff_note.rb delete mode 100644 app/views/projects/notes/_commit_discussion.html.haml delete mode 100644 app/views/projects/notes/discussions/_active.html.haml delete mode 100644 app/views/projects/notes/discussions/_commit.html.haml delete mode 100644 app/views/projects/notes/discussions/_diff.html.haml create mode 100644 app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml create mode 100644 app/views/projects/notes/discussions/_notes.html.haml delete mode 100644 app/views/projects/notes/discussions/_outdated.html.haml create mode 100644 db/migrate/20160508215820_add_type_to_notes.rb create mode 100644 db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index efb3e8e219..6d9d6528f4 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -285,6 +285,7 @@ class @Notes form.addClass "js-main-target-form" form.find("#note_line_code").remove() + form.find("#note_type").remove() ### General note form setup. @@ -472,6 +473,7 @@ class @Notes setupDiscussionNoteForm: (dataHolder, form) => # setup note target form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}" + form.find("#note_type").val dataHolder.data("noteType") form.find("#line_type").val dataHolder.data("lineType") form.find("#note_commit_id").val dataHolder.data("commitId") form.find("#note_line_code").val dataHolder.data("lineCode") diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 624c8249f7..a3e1ac13a4 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -226,8 +226,7 @@ ul.notes { } } -.note-action-button, -.discussion-action-button { +.note-action-button { display: inline-block; margin-left: 10px; line-height: 24px; diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index a202cb3869..9bcb82ef3f 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -17,11 +17,12 @@ class Projects::CommitController < Projects::ApplicationController def show apply_diff_view_cookie! - @line_notes = commit.notes.inline + @grouped_diff_notes = commit.notes.grouped_diff_notes + @note = @project.build_commit_note(commit) - @notes = commit.notes.not_inline.fresh + @notes = commit.notes.non_diff_notes.fresh @noteable = @commit - @comments_allowed = @reply_allowed = true + @comments_allowed = true @comments_target = { noteable_type: 'Commit', commit_id: @commit.id @@ -67,10 +68,10 @@ class Projects::CommitController < Projects::ApplicationController create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", success_path: successful_change_path, failure_path: failed_change_path) end - + def cherry_pick assign_change_commit_vars(@commit.cherry_pick_branch_name) - + return render_404 if @target_branch.blank? create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 671d5c2302..587b84fd92 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -22,7 +22,7 @@ class Projects::CompareController < Projects::ApplicationController @base_commit = @project.merge_base_commit(@base_ref, @head_ref) @diffs = compare.diffs(diff_options) @diff_refs = [@base_commit, @commit] - @line_notes = [] + @grouped_diff_notes = {} end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 9c147b3689..e571da64e4 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -73,12 +73,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController # but we need it for the "View file @ ..." link by deleted files @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit - @comments_allowed = @reply_allowed = true + @comments_allowed = true @comments_target = { noteable_type: 'MergeRequest', noteable_id: @merge_request.id } - @line_notes = @merge_request.notes.where("line_code is not null") + + @grouped_diff_notes = @merge_request.notes.grouped_diff_notes respond_to do |format| format.html @@ -300,7 +301,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh - @discussions = Note.discussions_from_notes(@notes) + @discussions = @notes.discussions @noteable = @merge_request # Get commits from repository diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 707a0d0e5c..4a57cd29a2 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -96,7 +96,7 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_html(note) - return unless note.for_diff_line? + return unless note.diff_note? if params[:view] == 'parallel' template = "projects/notes/_diff_notes_with_reply_parallel" @@ -120,7 +120,7 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_with_diff_html(note) - return unless note.for_diff_line? + return unless note.diff_note? render_to_string( "projects/notes/_discussion", @@ -158,7 +158,7 @@ class Projects::NotesController < Projects::ApplicationController def note_params params.require(:note).permit( :note, :noteable, :noteable_id, :noteable_type, :project_id, - :attachment, :line_code, :commit_id + :attachment, :line_code, :commit_id, :type ) end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index fa4c635f55..c41be33353 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -10,7 +10,7 @@ class NotesFinder notes = case target_type when "commit" - project.notes.for_commit_id(target_id).not_inline + project.notes.for_commit_id(target_id).non_diff_notes when "issue" project.issues.find(target_id).notes.nonawards.inc_author when "merge_request" diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 9f73edb455..5f311f3780 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -55,22 +55,18 @@ module DiffHelper end end - def line_comments - @line_comments ||= @line_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) - end + def organize_comments(left, right) + notes_left = notes_right = nil - def organize_comments(type_left, type_right, line_code_left, line_code_right) - comments_left = comments_right = nil - - unless type_left.nil? && type_right == 'new' - comments_left = line_comments[line_code_left] + unless left[:type].nil? && right[:type] == 'new' + notes_left = @grouped_diff_notes[left[:line_code]] end - unless type_left.nil? && type_right.nil? - comments_right = line_comments[line_code_right] + unless left[:type].nil? && right[:type].nil? + notes_right = @grouped_diff_notes[right[:line_code]] end - [comments_left, comments_right] + [notes_left, notes_right] end def inline_diff_btn @@ -96,8 +92,8 @@ module DiffHelper ].join(' ').html_safe end - def commit_for_diff(diff) - if diff.deleted_file + def commit_for_diff(diff_file) + if diff_file.deleted_file @base_commit || @commit.parent || @commit else @commit diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 95072b5373..b401c8385b 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -1,7 +1,7 @@ module NotesHelper # Helps to distinguish e.g. commit notes in mr notes list def note_for_main_target?(note) - (@noteable.class.name == note.noteable_type && !note.for_diff_line?) + @noteable.class.name == note.noteable_type && !note.diff_note? end def note_target_fields(note) @@ -15,16 +15,6 @@ module NotesHelper note.editable? && can?(current_user, :admin_note, note) end - def link_to_commit_diff_line_note(note) - if note.for_commit_diff_line? - link_to( - "#{note.diff_file_name}:L#{note.diff_new_line}", - namespace_project_commit_path(@project.namespace, @project, - note.noteable, anchor: note.line_code) - ) - end - end - def noteable_json(noteable) { id: noteable.id, @@ -35,7 +25,7 @@ module NotesHelper end def link_to_new_diff_note(line_code, line_type = nil) - discussion_id = Note.build_discussion_id( + discussion_id = LegacyDiffNote.build_discussion_id( @comments_target[:noteable_type], @comments_target[:noteable_id] || @comments_target[:commit_id], line_code @@ -45,9 +35,10 @@ module NotesHelper noteable_type: @comments_target[:noteable_type], noteable_id: @comments_target[:noteable_id], commit_id: @comments_target[:commit_id], + line_type: line_type, line_code: line_code, - discussion_id: discussion_id, - line_type: line_type + note_type: LegacyDiffNote.name, + discussion_id: discussion_id } button_tag(class: 'btn add-diff-note js-add-diff-note-button', @@ -57,18 +48,24 @@ module NotesHelper end end - def link_to_reply_diff(note, line_type = nil) + def link_to_reply_discussion(note, line_type = nil) return unless current_user data = { noteable_type: note.noteable_type, noteable_id: note.noteable_id, commit_id: note.commit_id, - line_code: note.line_code, discussion_id: note.discussion_id, line_type: line_type } + if note.diff_note? + data.merge!( + line_code: note.line_code, + note_type: LegacyDiffNote.name + ) + end + button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', data: data, title: 'Add a reply' end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb new file mode 100644 index 0000000000..b5de85df99 --- /dev/null +++ b/app/models/legacy_diff_note.rb @@ -0,0 +1,182 @@ +class LegacyDiffNote < Note + serialize :st_diff + + validates :line_code, presence: true, line_code: true + + before_create :set_diff + + class << self + def build_discussion_id(noteable_type, noteable_id, line_code, active = true) + [super(noteable_type, noteable_id), line_code, active].join("-") + end + end + + def diff_note? + true + end + + def legacy_diff_note? + true + end + + def discussion_id + @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code, active?) + end + + def find_diff + return nil unless noteable + return @diff if defined?(@diff) + + # Don't use ||= because nil is a valid value for @diff + @diff = noteable.diffs(Commit.max_diff_options).find do |d| + Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path + end + end + + def set_diff + # First lets find notes with same diff + # before iterating over all mr diffs + diff = diff_for_line_code unless for_merge_request? + diff ||= find_diff + + self.st_diff = diff.to_hash if diff + end + + def diff + @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) + end + + def diff_for_line_code + attributes = { + noteable_type: noteable_type, + line_code: line_code + } + + if for_commit? + attributes[:commit_id] = commit_id + else + attributes[:noteable_id] = noteable_id + end + + self.class.where(attributes).last.try(:diff) + end + + # Check if this note is part of an "active" discussion + # + # This will always return true for anything except MergeRequest noteables, + # which have special logic. + # + # If the note's current diff cannot be matched in the MergeRequest's current + # diff, it's considered inactive. + def active? + return true if for_commit? + return true unless self.diff + return false unless noteable + return @active if defined?(@active) + + noteable_diff = find_noteable_diff + + if noteable_diff + parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) + + @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } + else + @active = false + end + + @active + end + + def diff_file_index + line_code.split('_')[0] if line_code + end + + def diff_file_name + diff.new_path if diff + end + + def file_path + if diff.new_path.present? + diff.new_path + elsif diff.old_path.present? + diff.old_path + end + end + + def diff_old_line + line_code.split('_')[1].to_i if line_code + end + + def diff_new_line + line_code.split('_')[2].to_i if line_code + end + + def generate_line_code(line) + Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + end + + def diff_line + return @diff_line if @diff_line + + if diff + diff_lines.each do |line| + if generate_line_code(line) == self.line_code + @diff_line = line.text + end + end + end + + @diff_line + end + + def diff_line_type + return @diff_line_type if @diff_line_type + + if diff + diff_lines.each do |line| + if generate_line_code(line) == self.line_code + @diff_line_type = line.type + end + end + end + + @diff_line_type + end + + def truncated_diff_lines + max_number_of_lines = 16 + prev_match_line = nil + prev_lines = [] + + highlighted_diff_lines.each do |line| + if line.type == "match" + prev_lines.clear + prev_match_line = line + else + prev_lines << line + + break if generate_line_code(line) == self.line_code + + prev_lines.shift if prev_lines.length >= max_number_of_lines + end + end + + prev_lines + end + + def diff_lines + @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) + end + + def highlighted_diff_lines + Gitlab::Diff::Highlight.new(diff_lines).highlight + end + + private + + # Find the diff on noteable that matches our own + def find_noteable_diff + diffs = noteable.diffs(Commit.max_diff_options) + diffs.find { |d| d.new_path == self.diff.new_path } + end +end diff --git a/app/models/note.rb b/app/models/note.rb index f26aa1bf63..3bc5587070 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -1,6 +1,5 @@ -require 'carrierwave/orm/activerecord' - class Note < ActiveRecord::Base + extend ActiveModel::Naming include Gitlab::CurrentSettings include Participable include Mentionable @@ -22,12 +21,10 @@ class Note < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true before_validation :set_award! - before_validation :clear_blank_line_code! validates :note, :project, presence: true validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award } - validates :line_code, line_code: true, allow_blank: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } @@ -41,8 +38,6 @@ class Note < ActiveRecord::Base scope :awards, ->{ where(is_award: true) } scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } - scope :inline, ->{ where("line_code IS NOT NULL") } - scope :not_inline, ->{ where(line_code: nil) } scope :system, ->{ where(system: true) } scope :user, ->{ where(system: false) } scope :common, ->{ where(noteable_type: ["", nil]) } @@ -50,38 +45,31 @@ class Note < ActiveRecord::Base scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } + scope :legacy_diff_notes, ->{ where(type: 'LegacyDiffNote') } + scope :non_diff_notes, ->{ where(type: ['Note', nil]) } + scope :with_associations, -> do includes(:author, :noteable, :updated_by, project: [:project_members, { group: [:group_members] }]) end - serialize :st_diff - before_create :set_diff, if: ->(n) { n.line_code.present? } + before_validation :clear_blank_line_code! class << self - def discussions_from_notes(notes) - discussion_ids = [] - discussions = [] - - notes.each do |note| - next if discussion_ids.include?(note.discussion_id) - - # don't group notes for the main target - if !note.for_diff_line? && note.for_merge_request? - discussions << [note] - else - discussions << notes.select do |other_note| - note.discussion_id == other_note.discussion_id - end - discussion_ids << note.discussion_id - end - end - - discussions + def model_name + ActiveModel::Name.new(self, nil, 'note') end - def build_discussion_id(type, id, line_code) - [:discussion, type.try(:underscore), id, line_code].join("-").to_sym + def build_discussion_id(noteable_type, noteable_id) + [:discussion, noteable_type.try(:underscore), noteable_id].join("-") + end + + def discussions + all.group_by(&:discussion_id).values + end + + def grouped_diff_notes + legacy_diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) end # Searches for notes matching the given query. @@ -116,167 +104,35 @@ class Note < ActiveRecord::Base system && SystemNoteService.cross_reference?(note) end - def max_attachment_size - current_application_settings.max_attachment_size.megabytes.to_i + def diff_note? + false end - def find_diff - return nil unless noteable - return @diff if defined?(@diff) + def legacy_diff_note? + false + end - # Don't use ||= because nil is a valid value for @diff - @diff = noteable.diffs(Commit.max_diff_options).find do |d| - Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path - end + def discussion_id + @discussion_id ||= + if for_merge_request? + [:discussion, :note, id].join("-") + else + self.class.build_discussion_id(noteable_type, noteable_id || commit_id) + end + end + + def max_attachment_size + current_application_settings.max_attachment_size.megabytes.to_i end def hook_attrs attributes end - def set_diff - # First lets find notes with same diff - # before iterating over all mr diffs - diff = diff_for_line_code unless for_merge_request? - diff ||= find_diff - - self.st_diff = diff.to_hash if diff - end - - def diff - @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) - end - - def diff_for_line_code - Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff) - end - - # Check if this note is part of an "active" discussion - # - # This will always return true for anything except MergeRequest noteables, - # which have special logic. - # - # If the note's current diff cannot be matched in the MergeRequest's current - # diff, it's considered inactive. - def active? - return true unless self.diff - return false unless noteable - return @active if defined?(@active) - - noteable_diff = find_noteable_diff - - if noteable_diff - parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) - - @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } - else - @active = false - end - - @active - end - - def diff_file_index - line_code.split('_')[0] if line_code - end - - def diff_file_name - diff.new_path if diff - end - - def file_path - if diff.new_path.present? - diff.new_path - elsif diff.old_path.present? - diff.old_path - end - end - - def diff_old_line - line_code.split('_')[1].to_i if line_code - end - - def diff_new_line - line_code.split('_')[2].to_i if line_code - end - - def generate_line_code(line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) - end - - def diff_line - return @diff_line if @diff_line - - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line = line.text - end - end - end - - @diff_line - end - - def diff_line_type - return @diff_line_type if @diff_line_type - - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line_type = line.type - end - end - end - - @diff_line_type - end - - def truncated_diff_lines - max_number_of_lines = 16 - prev_match_line = nil - prev_lines = [] - - highlighted_diff_lines.each do |line| - if line.type == "match" - prev_lines.clear - prev_match_line = line - else - prev_lines << line - - break if generate_line_code(line) == self.line_code - - prev_lines.shift if prev_lines.length >= max_number_of_lines - end - end - - prev_lines - end - - def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) - end - - def highlighted_diff_lines - Gitlab::Diff::Highlight.new(diff_lines).highlight - end - - def discussion_id - @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) - end - def for_commit? noteable_type == "Commit" end - def for_commit_diff_line? - for_commit? && for_diff_line? - end - - def for_diff_line? - line_code.present? - end - def for_issue? noteable_type == "Issue" end @@ -285,10 +141,6 @@ class Note < ActiveRecord::Base noteable_type == "MergeRequest" end - def for_merge_request_diff_line? - for_merge_request? && for_diff_line? - end - def for_snippet? noteable_type == "Snippet" end @@ -361,14 +213,8 @@ class Note < ActiveRecord::Base self.line_code = nil if self.line_code.blank? end - # Find the diff on noteable that matches our own - def find_noteable_diff - diffs = noteable.diffs(Commit.max_diff_options) - diffs.find { |d| d.new_path == self.diff.new_path } - end - def awards_supported? - (for_issue? || for_merge_request?) && !for_diff_line? + (for_issue? || for_merge_request?) && !diff_note? end def contains_emoji_only? diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index 65f0e4c406..27e8ea5f5a 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,4 +1,4 @@ -- if @note.diff_file_name +- if @note.legacy_diff_note? %p.details New comment on diff for = link_to @note.diff_file_name, @target_url diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 81948513e4..3d91dc4312 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -30,13 +30,13 @@ %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) - = link_to_new_diff_note(right[:line_code], 'new') + = link_to_new_diff_note(new_line_code, 'new') %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - - if @reply_allowed - - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) - - if comments_left.present? || comments_right.present? - = render "projects/notes/diff_notes_with_reply_parallel", notes_left: comments_left, notes_right: comments_right + - if @comments_allowed + - notes_left, notes_right = organize_comments(left, right) + - if notes_left.present? || notes_right.present? + = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right - if diff_file.diff.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index e7169d7b59..d58444a9d5 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -6,16 +6,15 @@ %table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' } - last_line = 0 - - raw_diff_lines = diff_file.diff_lines.to_a - diff_file.highlighted_diff_lines.each_with_index do |line, index| - line_code = generate_line_code(diff_file.file_path, line) - last_line = line.new_pos = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code} - - if @reply_allowed - - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) - - unless comments.empty? - = render "projects/notes/diff_notes_with_reply", notes: comments, line: raw_diff_lines[index].text + - if @comments_allowed + - diff_notes = @grouped_diff_notes[line_code] + - if diff_notes + = render "projects/notes/diff_notes_with_reply", notes: diff_notes - if last_line > 0 = render "projects/diffs/match_line", { line: "", diff --git a/app/views/projects/notes/_commit_discussion.html.haml b/app/views/projects/notes/_commit_discussion.html.haml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index 39be072855..8144c1ba49 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -1,10 +1,8 @@ -- note = notes.first # example note --# Check if line want not changed since comment was left -- if !defined?(line) || line == note.diff_line - %tr.notes_holder - %td.notes_line{ colspan: 2 } - %td.notes_content - %ul.notes{ data: { discussion_id: note.discussion_id } } - = render notes - .discussion-reply-holder - = link_to_reply_diff(note) +- note = notes.first +%tr.notes_holder + %td.notes_line{ colspan: 2 } + %td.notes_content + %ul.notes{ data: { discussion_id: note.discussion_id } } + = render partial: "projects/notes/note", collection: notes, as: :note + .discussion-reply-holder + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index f8aa5e2fa7..45986b0d1e 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -1,27 +1,27 @@ -- note1 = notes_left.present? ? notes_left.first : nil -- note2 = notes_right.present? ? notes_right.first : nil +- note_left = notes_left.present? ? notes_left.first : nil +- note_right = notes_right.present? ? notes_right.first : nil %tr.notes_holder - - if note1 + - if note_left %td.notes_line.old %td.notes_content.parallel.old - %ul.notes{ data: { discussion_id: note1.discussion_id } } - = render notes_left + %ul.notes{ data: { discussion_id: note_left.discussion_id } } + = render partial: "projects/notes/note", collection: notes_left, as: :note .discussion-reply-holder - = link_to_reply_diff(note1, 'old') + = link_to_reply_discussion(note_left, 'old') - else %td.notes_line.old= "" %td.notes_content.parallel.old= "" - - if note2 + - if note_right %td.notes_line.new %td.notes_content.parallel.new - %ul.notes{ data: { discussion_id: note2.discussion_id } } - = render notes_right + %ul.notes{ data: { discussion_id: note_right.discussion_id } } + = render partial: "projects/notes/note", collection: notes_right, as: :note .discussion-reply-holder - = link_to_reply_diff(note2, 'new') + = link_to_reply_discussion(note_right, 'new') - else %td.notes_line.new= "" %td.notes_content.parallel.new= "" diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index 572b00a38c..40e03050a7 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -1,13 +1,46 @@ - note = discussion_notes.first +- expanded = !note.diff_note? || note.active? %li.note.note-discussion.timeline-entry .timeline-entry-inner .timeline-icon = link_to user_path(note.author) do - = image_tag avatar_icon(note.author_email), class: "avatar s40" + = image_tag avatar_icon(note.author), class: "avatar s40" .timeline-content - - if note.for_merge_request? - - (active_notes, outdated_notes) = discussion_notes.partition(&:active?) - = render "projects/notes/discussions/active", discussion_notes: active_notes if active_notes.length > 0 - = render "projects/notes/discussions/outdated", discussion_notes: outdated_notes if outdated_notes.length > 0 - - else - = render "projects/notes/discussions/commit", discussion_notes: discussion_notes + .discussion.js-toggle-container{ class: note.discussion_id } + .discussion-header + = link_to_member(@project, note.author, avatar: false) + + .inline.discussion-headline-light + = note.author.to_reference + started a discussion on + + - if note.for_commit? + - commit = note.noteable + - if commit + commit + = link_to commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code), class: 'monospace' + - else + a deleted commit + - else + - if note.active? + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do + the diff + - else + the outdated diff + + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago") + + .discussion-actions + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do + - if expanded + = icon("chevron-up") + - else + = icon("chevron-down") + + Toggle discussion + + .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } + - if note.diff_note? + = render "projects/notes/discussions/legacy_diff_with_notes", discussion_notes: discussion_notes + - else + = render "projects/notes/discussions/notes", discussion_notes: discussion_notes diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index d0ac380f21..67ed38a7b2 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -6,6 +6,7 @@ = f.hidden_field :line_code = f.hidden_field :noteable_id = f.hidden_field :noteable_type + = f.hidden_field :type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index aeb7c1d5ee..9fbc9a4554 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,5 +1,8 @@ +- return unless note.author +- return if note.cross_reference_not_visible_for?(current_user) + - note_editable = note_editable?(note) -%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } +%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} @@ -8,8 +11,8 @@ .note-header = link_to_member(note.project, note.author, avatar: false) .inline.note-headline-light - = "#{note.author.to_reference}" - - if !note.system + = note.author.to_reference + - unless note.system commented %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml index 62db86fb18..ebf7e8a9cb 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/projects/notes/_notes.html.haml @@ -2,14 +2,9 @@ - @discussions.each do |discussion_notes| - note = discussion_notes.first - if note_for_main_target?(note) - - next if note.cross_reference_not_visible_for?(current_user) - - = render discussion_notes + = render partial: "projects/notes/note", object: note, as: :note - else = render 'projects/notes/discussion', discussion_notes: discussion_notes - else - @notes.each do |note| - - next unless note.author - - next if note.cross_reference_not_visible_for?(current_user) - - = render note + = render partial: "projects/notes/note", object: note, as: :note diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml deleted file mode 100644 index 0ea8862a68..0000000000 --- a/app/views/projects/notes/discussions/_active.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- note = discussion_notes.first -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion" - = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do - on the diff - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-up - Show/hide discussion - - .discussion-body.js-toggle-content - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml deleted file mode 100644 index 2a2ead58ee..0000000000 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -- note = discussion_notes.first -- commit = note.noteable -- commit_description = commit ? 'commit' : 'a deleted commit' -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion on #{commit_description}" - - if commit - = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-up - Show/hide discussion - .discussion-body.js-toggle-content - - if note.for_diff_line? - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note - - else - .panel.panel-default - .notes{ data: { discussion_id: discussion_notes.first.discussion_id } } - %ul.notes.timeline - = render discussion_notes - .discussion-reply-holder - = link_to_reply_diff(discussion_notes.first) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml deleted file mode 100644 index d46aab000c..0000000000 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- diff = note.diff -- if diff - .diff-file - .diff-header - %span - - if diff.deleted_file - = diff.old_path - - else - = diff.new_path - - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode - %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" - .diff-content.code.js-syntax-highlight - %table - - note.truncated_diff_lines.each do |line| - - type = line.type - - line_code = generate_line_code(note.file_path, line) - %tr.line_holder{ id: line_code, class: "#{type}" } - - if type == "match" - %td.old_line.diff-line-num= "..." - %td.new_line.diff-line-num= "..." - %td.line_content.match= line.text - - else - %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } - %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } - %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) - - - if line_code == note.line_code - = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml new file mode 100644 index 0000000000..3ab11f6461 --- /dev/null +++ b/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml @@ -0,0 +1,30 @@ +- note = discussion_notes.first +- diff = note.diff +- return unless diff + +.diff-file + .diff-header + %span + - if diff.deleted_file + = diff.old_path + - else + = diff.new_path + - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode + %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" + .diff-content.code.js-syntax-highlight + %table + - note.truncated_diff_lines.each do |line| + - type = line.type + - line_code = generate_line_code(note.file_path, line) + %tr.line_holder{ id: line_code, class: "#{type}" } + - if type == "match" + %td.old_line.diff-line-num= "..." + %td.new_line.diff-line-num= "..." + %td.line_content.match= line.text + - else + %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } + %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } + %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) + + - if line_code == note.line_code + = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_notes.html.haml b/app/views/projects/notes/discussions/_notes.html.haml new file mode 100644 index 0000000000..e598e3c7c6 --- /dev/null +++ b/app/views/projects/notes/discussions/_notes.html.haml @@ -0,0 +1,7 @@ +- note = discussion_notes.first +.panel.panel-default + .notes{ data: { discussion_id: note.discussion_id } } + %ul.notes.timeline + = render partial: "projects/notes/note", collection: discussion_notes, as: :note + .discussion-reply-holder + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml deleted file mode 100644 index 45141bcd1d..0000000000 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- note = discussion_notes.first -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion" - on the outdated diff - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-down - Show/hide discussion - .discussion-body.js-toggle-content.hide - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/db/migrate/20160508215820_add_type_to_notes.rb b/db/migrate/20160508215820_add_type_to_notes.rb new file mode 100644 index 0000000000..58944d4e65 --- /dev/null +++ b/db/migrate/20160508215820_add_type_to_notes.rb @@ -0,0 +1,5 @@ +class AddTypeToNotes < ActiveRecord::Migration + def change + add_column :notes, :type, :string + end +end diff --git a/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb new file mode 100644 index 0000000000..c3f23d89d5 --- /dev/null +++ b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb @@ -0,0 +1,5 @@ +class SetTypeOnLegacyDiffNotes < ActiveRecord::Migration + def change + execute "UPDATE notes SET type = 'LegacyDiffNote' WHERE line_code IS NOT NULL" + end +end diff --git a/db/schema.rb b/db/schema.rb index 9b5aa640cb..e1117a0d85 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -632,10 +632,11 @@ ActiveRecord::Schema.define(version: 20160509201028) do t.string "line_code" t.string "commit_id" t.integer "noteable_id" - t.boolean "system", default: false, null: false + t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" - t.boolean "is_award", default: false, null: false + t.boolean "is_award", default: false, null: false + t.string "type" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 93a3a5ce08..4a11c8e362 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -107,6 +107,8 @@ module API break if opts[:line_code] end + + opts[:type] = LegacyDiffNote.name if opts[:line_code] end note = ::Notes::CreateService.new(user_project, current_user, opts).execute diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 406f5ea913..1619199b0a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -227,9 +227,9 @@ module API class CommitNote < Grape::Entity expose :note - expose(:path) { |note| note.diff_file_name } - expose(:line) { |note| note.diff_new_line } - expose(:line_type) { |note| note.diff_line_type } + expose(:path) { |note| note.diff_file_name if note.legacy_diff_note? } + expose(:line) { |note| note.diff_new_line if note.legacy_diff_note? } + expose(:line_type) { |note| note.diff_line_type if note.legacy_diff_note? } expose :author, using: Entities::UserBasic expose :created_at end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 4b788b5788..264888cb37 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -43,12 +43,8 @@ describe Note, models: true do expect(note.noteable.id).to eq(commit.id) end - it "should be recognized by #for_diff_line?" do - expect(note).to be_for_diff_line - end - - it "should be recognized by #for_commit_diff_line?" do - expect(note).to be_for_commit_diff_line + it "should be recognized by #legacy_diff_note?" do + expect(note).to be_legacy_diff_note end end From 9a3ed265e9c82ee55bc6a17737ef6c5c2a1ad7e3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 14:53:04 -0500 Subject: [PATCH 137/199] Use `@diff_notes_disabled` instead of `@comments_allowed` --- app/controllers/projects/commit_controller.rb | 1 - app/controllers/projects/compare_controller.rb | 1 + app/controllers/projects/merge_requests_controller.rb | 2 +- app/views/projects/diffs/_line.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 6 +++--- app/views/projects/diffs/_text_file.html.haml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 9bcb82ef3f..6e02f1240f 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -22,7 +22,6 @@ class Projects::CommitController < Projects::ApplicationController @note = @project.build_commit_note(commit) @notes = commit.notes.non_diff_notes.fresh @noteable = @commit - @comments_allowed = true @comments_target = { noteable_type: 'Commit', commit_id: @commit.id diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 587b84fd92..af0b69a244 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -22,6 +22,7 @@ class Projects::CompareController < Projects::ApplicationController @base_commit = @project.merge_base_commit(@base_ref, @head_ref) @diffs = compare.diffs(diff_options) @diff_refs = [@base_commit, @commit] + @diff_notes_disabled = true @grouped_diff_notes = {} end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e571da64e4..c5757a2462 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -73,7 +73,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController # but we need it for the "View file @ ..." link by deleted files @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit - @comments_allowed = true @comments_target = { noteable_type: 'MergeRequest', noteable_id: @merge_request.id @@ -118,6 +117,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.last_commit @base_commit = @merge_request.diff_base_commit @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare + @diff_notes_disabled = true @ci_commit = @merge_request.ci_commit @statuses = @ci_commit.statuses if @ci_commit diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 107097ad96..f1577e8a47 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -15,7 +15,7 @@ = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " ".html_safe : line.new_pos diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 3d91dc4312..4ecc9528bd 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -16,7 +16,7 @@ - else %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) @@ -29,11 +29,11 @@ %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(new_line_code, 'new') %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - - if @comments_allowed + - unless @diff_notes_disabled - notes_left, notes_right = organize_comments(left, right) - if notes_left.present? || notes_right.present? = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index d58444a9d5..068593a7dd 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -11,7 +11,7 @@ - last_line = line.new_pos = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code} - - if @comments_allowed + - unless @diff_notes_disabled - diff_notes = @grouped_diff_notes[line_code] - if diff_notes = render "projects/notes/diff_notes_with_reply", notes: diff_notes From 7848d54f5b0024f58d8ce2b0259347816be5bdbc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 14:53:31 -0500 Subject: [PATCH 138/199] Clean up LegacyDiffNote somewhat --- app/models/legacy_diff_note.rb | 183 ++++++++---------- app/models/note.rb | 4 + .../notify/note_merge_request_email.html.haml | 2 +- .../_legacy_diff_with_notes.html.haml | 2 +- lib/api/entities.rb | 2 +- spec/models/legacy_diff_note_spec.rb | 74 +++++++ 6 files changed, 160 insertions(+), 107 deletions(-) create mode 100644 spec/models/legacy_diff_note_spec.rb diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index b5de85df99..bbefc911b2 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -23,86 +23,10 @@ class LegacyDiffNote < Note @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code, active?) end - def find_diff - return nil unless noteable - return @diff if defined?(@diff) - - # Don't use ||= because nil is a valid value for @diff - @diff = noteable.diffs(Commit.max_diff_options).find do |d| - Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path - end - end - - def set_diff - # First lets find notes with same diff - # before iterating over all mr diffs - diff = diff_for_line_code unless for_merge_request? - diff ||= find_diff - - self.st_diff = diff.to_hash if diff - end - - def diff - @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) - end - - def diff_for_line_code - attributes = { - noteable_type: noteable_type, - line_code: line_code - } - - if for_commit? - attributes[:commit_id] = commit_id - else - attributes[:noteable_id] = noteable_id - end - - self.class.where(attributes).last.try(:diff) - end - - # Check if this note is part of an "active" discussion - # - # This will always return true for anything except MergeRequest noteables, - # which have special logic. - # - # If the note's current diff cannot be matched in the MergeRequest's current - # diff, it's considered inactive. - def active? - return true if for_commit? - return true unless self.diff - return false unless noteable - return @active if defined?(@active) - - noteable_diff = find_noteable_diff - - if noteable_diff - parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) - - @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } - else - @active = false - end - - @active - end - - def diff_file_index + def diff_file_hash line_code.split('_')[0] if line_code end - def diff_file_name - diff.new_path if diff - end - - def file_path - if diff.new_path.present? - diff.new_path - elsif diff.old_path.present? - diff.old_path - end - end - def diff_old_line line_code.split('_')[1].to_i if line_code end @@ -111,36 +35,32 @@ class LegacyDiffNote < Note line_code.split('_')[2].to_i if line_code end - def generate_line_code(line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + def diff + @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) + end + + def diff_file_path + diff.new_path.presence || diff.old_path + end + + def diff_lines + @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) end def diff_line - return @diff_line if @diff_line + @diff_line ||= diff_lines.find { |line| generate_line_code(line) == self.line_code } + end - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line = line.text - end - end - end - - @diff_line + def diff_line_text + diff_line.try(:text) end def diff_line_type - return @diff_line_type if @diff_line_type + diff_line.try(:type) + end - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line_type = line.type - end - end - end - - @diff_line_type + def highlighted_diff_lines + Gitlab::Diff::Highlight.new(diff_lines).highlight end def truncated_diff_lines @@ -164,16 +84,71 @@ class LegacyDiffNote < Note prev_lines end - def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) - end + # Check if this note is part of an "active" discussion + # + # This will always return true for anything except MergeRequest noteables, + # which have special logic. + # + # If the note's current diff cannot be matched in the MergeRequest's current + # diff, it's considered inactive. + def active? + return @active if defined?(@active) + return true if for_commit? + return true unless self.diff + return false unless noteable - def highlighted_diff_lines - Gitlab::Diff::Highlight.new(diff_lines).highlight + noteable_diff = find_noteable_diff + + if noteable_diff + parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) + + @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line_text } + else + @active = false + end + + @active end private + def find_diff + return nil unless noteable + return @diff if defined?(@diff) + + @diff = noteable.diffs(Commit.max_diff_options).find do |d| + d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash + end + end + + def set_diff + # First lets find notes with same diff + # before iterating over all mr diffs + diff = diff_for_line_code unless for_merge_request? + diff ||= find_diff + + self.st_diff = diff.to_hash if diff + end + + def diff_for_line_code + attributes = { + noteable_type: noteable_type, + line_code: line_code + } + + if for_commit? + attributes[:commit_id] = commit_id + else + attributes[:noteable_id] = noteable_id + end + + self.class.where(attributes).last.try(:diff) + end + + def generate_line_code(line) + Gitlab::Diff::LineCode.generate(diff_file_path, line.new_pos, line.old_pos) + end + # Find the diff on noteable that matches our own def find_noteable_diff diffs = noteable.diffs(Commit.max_diff_options) diff --git a/app/models/note.rb b/app/models/note.rb index 3bc5587070..7e5bdc09a8 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -112,6 +112,10 @@ class Note < ActiveRecord::Base false end + def active? + true + end + def discussion_id @discussion_id ||= if for_merge_request? diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index 27e8ea5f5a..a3643a00cf 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,7 @@ - if @note.legacy_diff_note? %p.details New comment on diff for - = link_to @note.diff_file_name, @target_url + = link_to @note.diff_file_path, @target_url \: = render 'note_message' diff --git a/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml index 3ab11f6461..6401245bf7 100644 --- a/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml +++ b/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml @@ -15,7 +15,7 @@ %table - note.truncated_diff_lines.each do |line| - type = line.type - - line_code = generate_line_code(note.file_path, line) + - line_code = generate_line_code(note.diff_file_path, line) %tr.line_holder{ id: line_code, class: "#{type}" } - if type == "match" %td.old_line.diff-line-num= "..." diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 1619199b0a..93a5798e21 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -227,7 +227,7 @@ module API class CommitNote < Grape::Entity expose :note - expose(:path) { |note| note.diff_file_name if note.legacy_diff_note? } + expose(:path) { |note| note.diff_file_path if note.legacy_diff_note? } expose(:line) { |note| note.diff_new_line if note.legacy_diff_note? } expose(:line_type) { |note| note.diff_line_type if note.legacy_diff_note? } expose :author, using: Entities::UserBasic diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb new file mode 100644 index 0000000000..7c29bef54e --- /dev/null +++ b/spec/models/legacy_diff_note_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe LegacyDiffNote, models: true do + describe "Commit diff line notes" do + let!(:note) { create(:note_on_commit_diff, note: "+1 from me") } + let!(:commit) { note.noteable } + + it "should save a valid note" do + expect(note.commit_id).to eq(commit.id) + expect(note.noteable.id).to eq(commit.id) + end + + it "should be recognized by #legacy_diff_note?" do + expect(note).to be_legacy_diff_note + end + end + + describe '#active?' do + it 'is always true when the note has no associated diff' do + note = build(:note_on_merge_request_diff) + + expect(note).to receive(:diff).and_return(nil) + + expect(note).to be_active + end + + it 'is never true when the note has no noteable associated' do + note = build(:note_on_merge_request_diff) + + expect(note).to receive(:diff).and_return(double) + expect(note).to receive(:noteable).and_return(nil) + + expect(note).not_to be_active + end + + it 'returns the memoized value if defined' do + note = build(:note_on_merge_request_diff) + + note.instance_variable_set(:@active, 'foo') + expect(note).not_to receive(:find_noteable_diff) + + expect(note.active?).to eq 'foo' + end + + context 'for a merge request noteable' do + it 'is false when noteable has no matching diff' do + merge = build_stubbed(:merge_request, :simple) + note = build(:note_on_merge_request_diff, noteable: merge) + + allow(note).to receive(:diff).and_return(double) + expect(note).to receive(:find_noteable_diff).and_return(nil) + + expect(note).not_to be_active + end + + it 'is true when noteable has a matching diff' do + merge = create(:merge_request, :simple) + + # Generate a real line_code value so we know it will match. We use a + # random line from a random diff just for funsies. + diff = merge.diffs.to_a.sample + line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample + code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) + + # We're persisting in order to trigger the set_diff callback + note = create(:note_on_merge_request_diff, noteable: merge, line_code: code) + + # Make sure we don't get a false positive from a guard clause + expect(note).to receive(:find_noteable_diff).and_call_original + expect(note).to be_active + end + end + end +end From c452fa8124ffe18e2e74e14491dcb3e419e60057 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 14:53:42 -0500 Subject: [PATCH 139/199] Update specs --- features/steps/shared/diff_note.rb | 12 +-- spec/factories/notes.rb | 4 +- spec/features/notes_on_merge_requests_spec.rb | 2 +- spec/models/note_spec.rb | 76 +------------------ 4 files changed, 10 insertions(+), 84 deletions(-) diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index e846c52d47..e8b1e4b487 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -23,7 +23,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Typo, please fix" find(".js-comment-button").trigger("click") sleep 0.05 @@ -33,7 +33,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do click_parallel_diff_line(sample_commit.line_code, 'old') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Old comment" find(".js-comment-button").trigger("click") end @@ -41,7 +41,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the right side like "New comment"' do click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") end @@ -51,7 +51,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Should fix it :smile:" find('.js-md-preview-button').click end @@ -62,7 +62,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.del_line_code) - page.within("form[id$='#{sample_commit.del_line_code}']") do + page.within("form[id$='#{sample_commit.del_line_code}-true']") do fill_in "note[note]", with: "DRY this up" find('.js-md-preview-button').click end @@ -91,7 +91,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in 'note[note]', with: ':smile:' click_button('Comment') end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 840b13196a..26719f2652 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -9,10 +9,10 @@ FactoryGirl.define do author factory :note_on_commit, traits: [:on_commit] - factory :note_on_commit_diff, traits: [:on_commit, :on_diff] + factory :note_on_commit_diff, traits: [:on_commit, :on_diff], class: LegacyDiffNote factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] factory :note_on_merge_request, traits: [:on_merge_request] - factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] + factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] factory :downvote_note, traits: [:award, :downvote] diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 389812ff7e..9e9fec0194 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -192,7 +192,7 @@ describe 'Comments', feature: true do end it 'should be removed when canceled' do - page.within(".diff-file form[id$='#{line_code}']") do + page.within(".diff-file form[id$='#{line_code}-true']") do find('.js-close-discussion-note-form').trigger('click') end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 264888cb37..5d916f0e6a 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -34,20 +34,6 @@ describe Note, models: true do end end - describe "Commit diff line notes" do - let!(:note) { create(:note_on_commit_diff, note: "+1 from me") } - let!(:commit) { note.noteable } - - it "should save a valid note" do - expect(note.commit_id).to eq(commit.id) - expect(note.noteable.id).to eq(commit.id) - end - - it "should be recognized by #legacy_diff_note?" do - expect(note).to be_legacy_diff_note - end - end - describe 'authorization' do before do @p1 = create(:project) @@ -144,66 +130,6 @@ describe Note, models: true do end end - describe '#active?' do - it 'is always true when the note has no associated diff' do - note = build(:note) - - expect(note).to receive(:diff).and_return(nil) - - expect(note).to be_active - end - - it 'is never true when the note has no noteable associated' do - note = build(:note) - - expect(note).to receive(:diff).and_return(double) - expect(note).to receive(:noteable).and_return(nil) - - expect(note).not_to be_active - end - - it 'returns the memoized value if defined' do - note = build(:note) - - expect(note).to receive(:diff).and_return(double) - expect(note).to receive(:noteable).and_return(double) - - note.instance_variable_set(:@active, 'foo') - expect(note).not_to receive(:find_noteable_diff) - - expect(note.active?).to eq 'foo' - end - - context 'for a merge request noteable' do - it 'is false when noteable has no matching diff' do - merge = build_stubbed(:merge_request, :simple) - note = build(:note, noteable: merge) - - allow(note).to receive(:diff).and_return(double) - expect(note).to receive(:find_noteable_diff).and_return(nil) - - expect(note).not_to be_active - end - - it 'is true when noteable has a matching diff' do - merge = create(:merge_request, :simple) - - # Generate a real line_code value so we know it will match. We use a - # random line from a random diff just for funsies. - diff = merge.diffs.to_a.sample - line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample - code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) - - # We're persisting in order to trigger the set_diff callback - note = create(:note, noteable: merge, line_code: code) - - # Make sure we don't get a false positive from a guard clause - expect(note).to receive(:find_noteable_diff).and_call_original - expect(note).to be_active - end - end - end - describe "editable?" do it "returns true" do note = build(:note) @@ -254,7 +180,7 @@ describe Note, models: true do end it "is not an award emoji when comment is on a diff" do - note = create(:note, note: ":blowfish:", noteable: merge_request, line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2") + note = create(:note_on_merge_request_diff, note: ":blowfish:", noteable: merge_request, line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2") note = note.reload expect(note.note).to eq(":blowfish:") From e0bb169ae390a00b3993bd0551859db05ad2bde1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 14:53:55 -0500 Subject: [PATCH 140/199] "the outdated diff" -> "an outdated diff" --- app/views/projects/notes/_discussion.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index 40e03050a7..aa26d8e87f 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -26,7 +26,7 @@ = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do the diff - else - the outdated diff + an outdated diff = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago") From e5ce1668061904a559ac6b9e5c830436fc743795 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 16:02:22 -0500 Subject: [PATCH 141/199] Satisfy Rubocop --- app/controllers/projects/commit_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 6e02f1240f..10b5932aff 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -22,7 +22,7 @@ class Projects::CommitController < Projects::ApplicationController @note = @project.build_commit_note(commit) @notes = commit.notes.non_diff_notes.fresh @noteable = @commit - @comments_target = { + @comments_target = { noteable_type: 'Commit', commit_id: @commit.id } From 876dc7bacb5c2835537d13dfedd2f7ed55463c6b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 17:25:00 -0500 Subject: [PATCH 142/199] Rename diff_with_notes partial. --- app/views/projects/notes/_discussion.html.haml | 2 +- ...acy_diff_with_notes.html.haml => _diff_with_notes.html.haml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/views/projects/notes/discussions/{_legacy_diff_with_notes.html.haml => _diff_with_notes.html.haml} (100%) diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index aa26d8e87f..7869d6413d 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -41,6 +41,6 @@ .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } - if note.diff_note? - = render "projects/notes/discussions/legacy_diff_with_notes", discussion_notes: discussion_notes + = render "projects/notes/discussions/diff_with_notes", discussion_notes: discussion_notes - else = render "projects/notes/discussions/notes", discussion_notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml similarity index 100% rename from app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml rename to app/views/projects/notes/discussions/_diff_with_notes.html.haml From 7e1f14e21517d3907a0e096d44b30797612f69cd Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 26 Apr 2016 19:57:37 -0300 Subject: [PATCH 143/199] Preserve commits/diff/comments for PRs that were merged on GitHub --- app/models/merge_request.rb | 10 ++++++- app/models/merge_request_diff.rb | 26 ++++++++++++++----- .../github_import/pull_request_formatter.rb | 2 ++ .../pull_request_formatter_spec.rb | 6 +++++ spec/models/merge_request_spec.rb | 15 +++++++++-- 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5c5e6007aa..45ddcf6812 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -26,6 +26,10 @@ class MergeRequest < ActiveRecord::Base # when creating new merge request attr_accessor :can_be_created, :compare_commits, :compare + # Temporary fields to store target_sha, and base_sha to + # compare when importing pull requests from GitHub + attr_accessor :base_target_sha, :head_source_sha + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -490,10 +494,14 @@ class MergeRequest < ActiveRecord::Base end def target_sha - @target_sha ||= target_project.repository.commit(target_branch).try(:sha) + return @base_target_sha if defined?(@base_target_sha) + + target_project.repository.commit(target_branch).try(:sha) end def source_sha + return @head_source_sha if defined?(@head_source_sha) + last_commit.try(:sha) || source_tip.try(:sha) end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index eb42c07b9b..6ad8fc3f03 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -6,7 +6,7 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request - delegate :target_branch, :source_branch, to: :merge_request, prefix: nil + delegate :head_source_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil state_machine :state, initial: :empty do state :collected @@ -38,8 +38,8 @@ class MergeRequestDiff < ActiveRecord::Base @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha, + self.base, + self.head, ) compare.diffs(options) end @@ -144,7 +144,7 @@ class MergeRequestDiff < ActiveRecord::Base self.st_diffs = new_diffs - self.base_commit_sha = self.repository.merge_base(self.source_sha, self.target_branch) + self.base_commit_sha = self.repository.merge_base(self.head, self.base) self.save end @@ -160,10 +160,24 @@ class MergeRequestDiff < ActiveRecord::Base end def source_sha + return head_source_sha if head_source_sha.present? + source_commit = merge_request.source_project.commit(source_branch) source_commit.try(:sha) end + def target_sha + merge_request.target_sha + end + + def base + self.target_sha || self.target_branch + end + + def head + self.source_sha + end + def compare @compare ||= begin @@ -172,8 +186,8 @@ class MergeRequestDiff < ActiveRecord::Base Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha + self.base, + self.head ) end end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index f242596bdf..3b541a04d5 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -8,8 +8,10 @@ module Gitlab description: description, source_project: source_project, source_branch: source_branch, + head_source_sha: source_sha, target_project: target_project, target_branch: target_branch, + base_target_sha: target_sha, state: state, milestone: milestone, author_id: author_id, diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index b1f3d17373..1d15d3d937 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -41,8 +41,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'opened', milestone: nil, author_id: project.creator_id, @@ -66,8 +68,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'closed', milestone: nil, author_id: project.creator_id, @@ -91,8 +95,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'merged', milestone: nil, author_id: project.creator_id, diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c8578749b2..9eef08c6d0 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -64,7 +64,13 @@ describe MergeRequest, models: true do describe '#target_sha' do context 'when the target branch does not exist anymore' do - subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + let(:project) { create(:project) } + + subject { create(:merge_request, source_project: project, target_project: project) } + + before do + project.repository.raw_repository.delete_branch(subject.target_branch) + end it 'returns nil' do expect(subject.target_sha).to be_nil @@ -289,7 +295,12 @@ describe MergeRequest, models: true do let(:fork_project) { create(:project, forked_from_project: project) } context 'when the target branch does not exist anymore' do - subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + subject { create(:merge_request, source_project: project, target_project: project) } + + before do + project.repository.raw_repository.delete_branch(subject.target_branch) + subject.reload + end it 'does not crash' do expect{ subject.diverged_commits_count }.not_to raise_error From e001bd5e3d813fa72f724fc0e661b36099f678ea Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 9 May 2016 16:17:05 -0500 Subject: [PATCH 144/199] Import PRs where branch names were reused across PRs --- .../github_import/pull_request_formatter.rb | 28 +++++++++++++--- .../pull_request_formatter_spec.rb | 32 +++++++++++++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 3b541a04d5..9f8182f643 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -30,11 +30,19 @@ module Gitlab end def source_branch_exists? - source_project.repository.branch_names.include?(source_branch) + source_project.repository.branch_exists?(source_ref) end def source_branch - raw_data.head.ref + @source_branch ||= if source_branch_exists? + source_ref + else + "#{source_ref}-#{short_id(source_sha)}" + end + end + + def short_id(sha, length = 7) + sha.to_s[0..length] end def source_sha @@ -42,11 +50,15 @@ module Gitlab end def target_branch_exists? - target_project.repository.branch_names.include?(target_branch) + target_project.repository.branch_exists?(target_ref) end def target_branch - raw_data.base.ref + @target_branch ||= if target_branch_exists? + target_ref + else + "#{target_ref}-#{short_id(target_sha)}" + end end def target_sha @@ -99,6 +111,10 @@ module Gitlab raw_data.head.repo end + def source_ref + raw_data.head.ref + end + def target_project project end @@ -107,6 +123,10 @@ module Gitlab raw_data.base.repo end + def target_ref + raw_data.base.ref + end + def state @state ||= case true when raw_data.state == 'closed' && raw_data.merged_at.present? diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 1d15d3d937..adedc57719 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -164,10 +164,20 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end describe '#source_branch' do - let(:raw_data) { double(base_data) } + context 'when source branch exists' do + let(:raw_data) { double(base_data) } - it 'returns head ref' do - expect(pull_request.source_branch).to eq 'feature' + it 'returns head ref' do + expect(pull_request.source_branch).to eq 'feature' + end + end + + context 'when source branch does not exist' do + let(:raw_data) { double(base_data.merge(head: double(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) } + + it 'returns head ref' do + expect(pull_request.source_branch).to eq 'removed-branch-2e5d3239' + end end end @@ -198,10 +208,20 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end describe '#target_branch' do - let(:raw_data) { double(base_data) } + context 'when target branch exists' do + let(:raw_data) { double(base_data) } - it 'returns base ref' do - expect(pull_request.target_branch).to eq 'master' + it 'returns base ref' do + expect(pull_request.target_branch).to eq 'master' + end + end + + context 'when target branch does not exist' do + let(:raw_data) { double(base_data.merge(base: double(ref: 'removed-branch', sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7'))) } + + it 'returns head ref' do + expect(pull_request.target_branch).to eq 'removed-branch-8ffb3c15' + end end end From 795a7ca8f171a1634134e8b8ce5cf80a65381903 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 9 May 2016 17:45:37 -0500 Subject: [PATCH 145/199] Extract GitHub branch formatter --- app/models/repository.rb | 4 + lib/gitlab/github_import/branch_formatter.rb | 25 ++++++ lib/gitlab/github_import/importer.rb | 30 +++---- .../github_import/pull_request_formatter.rb | 74 +++------------- .../github_import/branch_formatter_spec.rb | 57 ++++++++++++ .../pull_request_formatter_spec.rb | 88 ------------------- 6 files changed, 113 insertions(+), 165 deletions(-) create mode 100644 lib/gitlab/github_import/branch_formatter.rb create mode 100644 spec/lib/gitlab/github_import/branch_formatter_spec.rb diff --git a/app/models/repository.rb b/app/models/repository.rb index 0eff74320f..3716ea6ad6 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -195,6 +195,10 @@ class Repository cache.fetch(:branch_names) { branches.map(&:name) } end + def branch_exists?(branch_name) + branch_names.include?(branch_name) + end + def tag_names cache.fetch(:tag_names) { raw_repository.tag_names } end diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb new file mode 100644 index 0000000000..f968ec4b10 --- /dev/null +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -0,0 +1,25 @@ +module Gitlab + module GithubImport + class BranchFormatter < BaseFormatter + delegate :repo, :sha, :ref, to: :raw_data + + def exists? + project.repository.branch_exists?(ref) + end + + def name + @name ||= exists? ? ref : "#{ref}-#{short_id}" + end + + def valid? + repo.present? + end + + private + + def short_id + sha.to_s[0..7] + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 9aea947a81..1f160837c5 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -6,8 +6,8 @@ module Gitlab attr_reader :client, :project, :repo, :repo_url def initialize(project) - @project = project - @repo = project.import_source + @project = project + @repo = project.import_source @repo_url = project.import_url if credentials @@ -30,7 +30,7 @@ module Gitlab end def import_labels - client.labels(project.import_source).each do |raw_data| + client.labels(repo).each do |raw_data| Label.create!(LabelFormatter.new(project, raw_data).attributes) end @@ -40,7 +40,7 @@ module Gitlab end def import_milestones - client.list_milestones(project.import_source, state: :all).each do |raw_data| + client.list_milestones(repo, state: :all).each do |raw_data| Milestone.create!(MilestoneFormatter.new(project, raw_data).attributes) end @@ -50,9 +50,7 @@ module Gitlab end def import_issues - client.list_issues(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| + client.list_issues(repo, state: :all, sort: :created, direction: :asc).each do |raw_data| gh_issue = IssueFormatter.new(project, raw_data) if gh_issue.valid? @@ -75,8 +73,8 @@ module Gitlab .map { |raw| PullRequestFormatter.new(project, raw) } .select(&:valid?) - source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch, pr.source_sha] } - target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch, pr.target_sha] } + source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] } + target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] } branches_removed = source_branches_removed | target_branches_removed create_refs(branches_removed) @@ -101,19 +99,19 @@ module Gitlab end def create_refs(branches) - branches.each do |branch| - client.create_ref(repo, "refs/heads/#{branch.first}", branch.last) + branches.each do |name, sha| + client.create_ref(repo, "refs/heads/#{name}", sha) end end def delete_refs(branches) - branches.each do |branch| - client.delete_ref(repo, "heads/#{branch.first}") + branches.each do |name, _| + client.delete_ref(repo, "heads/#{name}") end end def apply_labels(number, issuable) - issue = client.issue(project.import_source, number) + issue = client.issue(repo, number) if issue.labels.count > 0 label_ids = issue.labels.map do |raw| @@ -125,12 +123,12 @@ module Gitlab end def import_comments(issue_number, noteable) - comments = client.issue_comments(project.import_source, issue_number) + comments = client.issue_comments(repo, issue_number) create_comments(comments, noteable) end def import_comments_on_diff(pull_request_number, merge_request) - comments = client.pull_request_comments(project.import_source, pull_request_number) + comments = client.pull_request_comments(repo, pull_request_number) create_comments(comments, merge_request) end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 9f8182f643..361d15d991 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,17 +1,20 @@ module Gitlab module GithubImport class PullRequestFormatter < BaseFormatter + delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true + delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true + def attributes { iid: number, title: raw_data.title, description: description, - source_project: source_project, - source_branch: source_branch, - head_source_sha: source_sha, - target_project: target_project, - target_branch: target_branch, - base_target_sha: target_sha, + source_project: source_branch_project, + source_branch: source_branch_name, + head_source_sha: source_branch_sha, + target_project: target_branch_project, + target_branch: target_branch_name, + base_target_sha: target_branch_sha, state: state, milestone: milestone, author_id: author_id, @@ -29,40 +32,12 @@ module Gitlab !cross_project? end - def source_branch_exists? - source_project.repository.branch_exists?(source_ref) - end - def source_branch - @source_branch ||= if source_branch_exists? - source_ref - else - "#{source_ref}-#{short_id(source_sha)}" - end - end - - def short_id(sha, length = 7) - sha.to_s[0..length] - end - - def source_sha - raw_data.head.sha - end - - def target_branch_exists? - target_project.repository.branch_exists?(target_ref) + @source_branch ||= BranchFormatter.new(project, raw_data.head) end def target_branch - @target_branch ||= if target_branch_exists? - target_ref - else - "#{target_ref}-#{short_id(target_sha)}" - end - end - - def target_sha - raw_data.base.sha + @target_branch ||= BranchFormatter.new(project, raw_data.base) end private @@ -90,7 +65,8 @@ module Gitlab end def cross_project? - source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + source_branch_repo.present? && target_branch_repo.present? && + source_branch_repo.id != target_branch_repo.id end def description @@ -103,30 +79,6 @@ module Gitlab end end - def source_project - project - end - - def source_repo - raw_data.head.repo - end - - def source_ref - raw_data.head.ref - end - - def target_project - project - end - - def target_repo - raw_data.base.repo - end - - def target_ref - raw_data.base.ref - end - def state @state ||= case true when raw_data.state == 'closed' && raw_data.merged_at.present? diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb new file mode 100644 index 0000000000..58bfdd6c72 --- /dev/null +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::BranchFormatter, lib: true do + let(:project) { create(:project) } + let(:repo) { double } + let(:raw) do + { + ref: 'feature', + repo: repo, + sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + } + end + + describe '#exists?' do + it 'returns true when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.exists?).to eq true + end + + it 'returns false when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.exists?).to eq false + end + end + + describe '#name' do + it 'returns raw ref when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.name).to eq 'feature' + end + + it 'returns formatted ref when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.name).to eq 'removed-branch-2e5d3239' + end + end + + describe '#repo' do + it 'returns raw repo' do + branch = described_class.new(project, double(raw)) + + expect(branch.repo).to eq repo + end + end + + describe '#sha' do + it 'returns raw sha' do + branch = described_class.new(project, double(raw)) + + expect(branch.sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index adedc57719..5fed98e392 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -163,94 +163,6 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end - describe '#source_branch' do - context 'when source branch exists' do - let(:raw_data) { double(base_data) } - - it 'returns head ref' do - expect(pull_request.source_branch).to eq 'feature' - end - end - - context 'when source branch does not exist' do - let(:raw_data) { double(base_data.merge(head: double(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) } - - it 'returns head ref' do - expect(pull_request.source_branch).to eq 'removed-branch-2e5d3239' - end - end - end - - describe '#source_sha' do - let(:raw_data) { double(base_data) } - - it 'returns head sha' do - expect(pull_request.source_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' - end - end - - describe '#source_branch_exists?' do - context 'when source branch exists' do - let(:raw_data) { double(base_data) } - - it 'returns true' do - expect(pull_request.source_branch_exists?).to eq true - end - end - - context 'when source branch does not exist' do - let(:raw_data) { double(base_data.merge(head: double(ref: 'removed-branch').as_null_object)) } - - it 'returns false' do - expect(pull_request.source_branch_exists?).to eq false - end - end - end - - describe '#target_branch' do - context 'when target branch exists' do - let(:raw_data) { double(base_data) } - - it 'returns base ref' do - expect(pull_request.target_branch).to eq 'master' - end - end - - context 'when target branch does not exist' do - let(:raw_data) { double(base_data.merge(base: double(ref: 'removed-branch', sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7'))) } - - it 'returns head ref' do - expect(pull_request.target_branch).to eq 'removed-branch-8ffb3c15' - end - end - end - - describe '#target_sha' do - let(:raw_data) { double(base_data) } - - it 'returns base sha' do - expect(pull_request.target_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7' - end - end - - describe '#target_branch_exists?' do - context 'when target branch exists' do - let(:raw_data) { double(base_data) } - - it 'returns true' do - expect(pull_request.target_branch_exists?).to eq true - end - end - - context 'when target branch does not exist' do - let(:raw_data) { double(base_data.merge(base: double(ref: 'removed-branch').as_null_object)) } - - it 'returns false' do - expect(pull_request.target_branch_exists?).to eq false - end - end - end - describe '#valid?' do context 'when source, and target repos are not a fork' do let(:raw_data) { double(base_data) } From ebaa19c162bec7dce64db25124e448d832c17384 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 10 May 2016 16:23:59 -0500 Subject: [PATCH 146/199] Fix validation method for Gitlab::GithubImport::PullRequestFormatter --- lib/gitlab/github_import/branch_formatter.rb | 4 ++++ lib/gitlab/github_import/pull_request_formatter.rb | 5 ++--- .../gitlab/github_import/branch_formatter_spec.rb | 14 ++++++++++++++ .../github_import/pull_request_formatter_spec.rb | 4 ++-- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index f968ec4b10..a15fc84b41 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -15,6 +15,10 @@ module Gitlab repo.present? end + def valid? + repo.present? + end + private def short_id diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 361d15d991..574737b31c 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -29,7 +29,7 @@ module Gitlab end def valid? - !cross_project? + source_branch.valid? && target_branch.valid? && !cross_project? end def source_branch @@ -65,8 +65,7 @@ module Gitlab end def cross_project? - source_branch_repo.present? && target_branch_repo.present? && - source_branch_repo.id != target_branch_repo.id + source_branch_repo.id != target_branch_repo.id end def description diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index 58bfdd6c72..3cb634ba01 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -54,4 +54,18 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do expect(branch.sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' end end + + describe '#valid?' do + it 'returns true when repository exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.valid?).to eq true + end + + it 'returns false when repository does not exist' do + branch = described_class.new(project, double(raw.merge(repo: nil))) + + expect(branch.valid?).to eq false + end + end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 5fed98e392..120f59e6e7 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -173,7 +173,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when source repo is a fork' do - let(:source_repo) { double(id: 2, fork: true) } + let(:source_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do @@ -182,7 +182,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when target repo is a fork' do - let(:target_repo) { double(id: 2, fork: true) } + let(:target_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do From 290aa75e6f8dfbd92ed11f6c271fb4ad83831e86 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 10 May 2016 17:23:03 -0500 Subject: [PATCH 147/199] Remove branches that does exist on GitHub after PRs were imported --- lib/gitlab/github_import/importer.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 1f160837c5..408d9b7963 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -79,8 +79,6 @@ module Gitlab create_refs(branches_removed) - project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') - pull_requests.each do |pull_request| merge_request = MergeRequest.new(pull_request.attributes) @@ -102,11 +100,14 @@ module Gitlab branches.each do |name, sha| client.create_ref(repo, "refs/heads/#{name}", sha) end + + project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') end def delete_refs(branches) branches.each do |name, _| client.delete_ref(repo, "heads/#{name}") + project.repository.rm_branch(project.creator, name) end end From bd1a6208d9b76922ae07bbe59f9f03a3573fe59d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 10 May 2016 17:34:57 -0500 Subject: [PATCH 148/199] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..accaaaba4e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,6 +51,7 @@ v 8.8.0 (unreleased) - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) - Hide left sidebar on phone screens to give more space for content - Redesign navigation for profile and group pages + - Import pull requests from GitHub where the source or target branches were removed v 8.7.5 - Fix relative links in wiki pages. !4050 From cf6d4e9db507d31feede527cb2dcc196d763c359 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 14 May 2016 00:48:49 -0500 Subject: [PATCH 149/199] Change colors for charcoal and graphite sidebar themes Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/gitlab-theme.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 51a17d1469..f47eb1f233 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -88,8 +88,8 @@ } $theme-blue: #2980b9; -$theme-charcoal: #333c47; -$theme-graphite: #888; +$theme-charcoal: #3d454d; +$theme-graphite: #666; $theme-gray: #373737; $theme-green: #019875; $theme-violet: #548; @@ -100,11 +100,11 @@ body { } &.ui_charcoal { - @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d); + @include gitlab-theme(#d6d7d9, #485157, $theme-charcoal, #353b41); } &.ui_graphite { - @include gitlab-theme(#ccc, $theme-graphite, #777, #666); + @include gitlab-theme(#ccc, #777, $theme-graphite, #555); } &.ui_gray { From 774a5107822d3d451b88ed3a7257aeac8d91c35a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 09:39:33 -0500 Subject: [PATCH 150/199] Fix specs --- .../jwt/container_registry_authentication_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/jwt/container_registry_authentication_service_spec.rb index 1873ea2639..7c87985252 100644 --- a/spec/services/jwt/container_registry_authentication_service_spec.rb +++ b/spec/services/jwt/container_registry_authentication_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe JWT::ContainerRegistryAuthenticationService, services: true do +describe Gitlab::JWT::ContainerRegistryAuthenticationService, services: true do let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } From 63cdf1aeb04b9694c0b6d44b1141868fcc5a0904 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 11:11:48 -0500 Subject: [PATCH 151/199] Use Auth::ContainerRegistryAuthenticationService --- app/controllers/jwt_controller.rb | 2 +- ...ntainer_registry_authentication_service.rb | 69 ++++++++++++++++++ ...ntainer_registry_authentication_service.rb | 71 ------------------- ...er_registry_authentication_service_spec.rb | 2 +- 4 files changed, 71 insertions(+), 73 deletions(-) create mode 100644 app/services/auth/container_registry_authentication_service.rb delete mode 100644 app/services/jwt/container_registry_authentication_service.rb diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index e067f59808..e5affb1adc 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -4,7 +4,7 @@ class JwtController < ApplicationController before_action :authenticate_project_or_user SERVICES = { - 'container_registry' => ::Gitlab::JWT::ContainerRegistryAuthenticationService, + 'container_registry' => Auth::ContainerRegistryAuthenticationService, } def auth diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb new file mode 100644 index 0000000000..0323a42b69 --- /dev/null +++ b/app/services/auth/container_registry_authentication_service.rb @@ -0,0 +1,69 @@ +module Auth + class ContainerRegistryAuthenticationService < BaseService + def execute + if params[:offline_token] + return error('forbidden', 403) unless current_user + end + + return error('forbidden', 401) if scopes.blank? + + { token: authorized_token(scopes).encoded } + end + + private + + def authorized_token(access) + token = ::JWT::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token[:access] = access + token + end + + def scopes + return unless params[:scope] + + @scopes ||= begin + scope = process_scope(params[:scope]) + [scope].compact + end + end + + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + requested_project = Project.find_with_namespace(name) + return unless requested_project + + actions = actions.select do |action| + can_access?(requested_project, action) + end + + { type: type, name: name, actions: actions } if actions.present? + end + + def can_access?(requested_project, requested_action) + case requested_action + when 'pull' + requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) + when 'push' + requested_project == project || can?(current_user, :create_container_registry, requested_project) + else + false + end + end + + def registry + Gitlab.config.registry + end + end +end diff --git a/app/services/jwt/container_registry_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb deleted file mode 100644 index dd0f295478..0000000000 --- a/app/services/jwt/container_registry_authentication_service.rb +++ /dev/null @@ -1,71 +0,0 @@ -module Gitlab - module JWT - class ContainerRegistryAuthenticationService < BaseService - def execute - if params[:offline_token] - return error('forbidden', 403) unless current_user - end - - return error('forbidden', 401) if scopes.blank? - - { token: authorized_token(scopes).encoded } - end - - private - - def authorized_token(access) - token = ::JWT::RSAToken.new(registry.key) - token.issuer = registry.issuer - token.audience = params[:service] - token.subject = current_user.try(:username) - token[:access] = access - token - end - - def scopes - return unless params[:scope] - - @scopes ||= begin - scope = process_scope(params[:scope]) - [scope].compact - end - end - - def process_scope(scope) - type, name, actions = scope.split(':', 3) - actions = actions.split(',') - - case type - when 'repository' - process_repository_access(type, name, actions) - end - end - - def process_repository_access(type, name, actions) - requested_project = Project.find_with_namespace(name) - return unless requested_project - - actions = actions.select do |action| - can_access?(requested_project, action) - end - - { type: type, name: name, actions: actions } if actions.present? - end - - def can_access?(requested_project, requested_action) - case requested_action - when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) - when 'push' - requested_project == project || can?(current_user, :create_container_registry, requested_project) - else - false - end - end - - def registry - Gitlab.config.registry - end - end - end -end diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/jwt/container_registry_authentication_service_spec.rb index 7c87985252..1873ea2639 100644 --- a/spec/services/jwt/container_registry_authentication_service_spec.rb +++ b/spec/services/jwt/container_registry_authentication_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::JWT::ContainerRegistryAuthenticationService, services: true do +describe JWT::ContainerRegistryAuthenticationService, services: true do let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } From 0a9979d9ef3b5205e36ecfb66ff6322fa824492f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 11:16:11 -0500 Subject: [PATCH 152/199] Rename specs --- .../container_registry_authentication_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spec/services/{jwt => auth}/container_registry_authentication_service_spec.rb (98%) diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb similarity index 98% rename from spec/services/jwt/container_registry_authentication_service_spec.rb rename to spec/services/auth/container_registry_authentication_service_spec.rb index 1873ea2639..8dc47a24ee 100644 --- a/spec/services/jwt/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe JWT::ContainerRegistryAuthenticationService, services: true do +describe Auth::ContainerRegistryAuthenticationService, services: true do let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } From 4f1e0014a73e1e310a82a3d8ce52367a1f9d6a9e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:03:41 -0500 Subject: [PATCH 153/199] Simplify registry configuration --- config/gitlab.yml.example | 4 +--- config/initializers/1_settings.rb | 38 ++++--------------------------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 37639e52e6..2e383bc90f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -179,9 +179,7 @@ production: &base registry: # enabled: true # host: localhost - # port: 5000 - # https: false - # internal_host: localhost + # api_url: http://localhost:5000/ # key: config/registry.key # issuer: omnibus-certificate diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index e46be3f420..d1fcb053be 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -27,30 +27,6 @@ class Settings < Settingslogic ].join('') end - def build_registry_api_url - if registry.port.to_i == (registry.https ? 443 : 80) - custom_port = nil - else - custom_port = ":#{registry.port}" - end - [ registry.protocol, - "://", - registry.internal_host, - custom_port - ].join('') - end - - def build_registry_host_with_port - if registry.port.to_i == (registry.https ? 443 : 80) - custom_port = nil - else - custom_port = ":#{registry.port}" - end - [ registry.host, - custom_port - ].join('') - end - def build_gitlab_shell_ssh_path_prefix user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" @@ -271,15 +247,11 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes # Registry # Settings['registry'] ||= Settingslogic.new({}) -Settings.registry['enabled'] = false if Settings.registry['enabled'].nil? -Settings.registry['host'] ||= "example.com" -Settings.registry['internal_host']||= "localhost" -Settings.registry['key'] ||= nil -Settings.registry['https'] = false if Settings.registry['https'].nil? -Settings.registry['port'] ||= Settings.registry.https ? 443 : 80 -Settings.registry['protocol'] ||= Settings.registry.https ? "https" : "http" -Settings.registry['api_url'] ||= Settings.send(:build_registry_api_url) -Settings.registry['host_port'] ||= Settings.send(:build_registry_host_with_port) +Settings.registry['enabled'] ||= false +Settings.registry['host'] ||= "example.com" +Settings.registry['api_url'] ||= "http://localhost:5000/" +Settings.registry['key'] ||= nil +Settings.registry['issuer'] ||= nil # # Git LFS From 393459b2b24fe788764ee787552da055846b9a63 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:04:04 -0500 Subject: [PATCH 154/199] Improve code design after review --- app/controllers/jwt_controller.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index e5affb1adc..0a993bf280 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -12,9 +12,8 @@ class JwtController < ApplicationController head :not_found unless service result = service.new(@project, @user, auth_params).execute - return head result[:http_status] if result[:http_status] - render json: result + render json: result, status: result[:http_status] end private @@ -27,10 +26,8 @@ class JwtController < ApplicationController @user = authenticate_user(login, password) return if @user - end - if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) - head :forbidden + render_403 end end From 46cc04ce7a374127dd617c8fd2671efed2819cda Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:16:44 -0500 Subject: [PATCH 155/199] Add TODO --- app/controllers/jwt_controller.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 0a993bf280..bd9d7e4425 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -42,6 +42,9 @@ class JwtController < ApplicationController end def authenticate_user(login, password) + # TODO: this is a copy and paste from grack_auth, + # it should be refactored in the future + user = Gitlab::Auth.new.find(login, password) # If the user authenticated successfully, we reset the auth failure count From 715a8cfa2f4639bf36b604f6e3eb2814187367c0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:22:45 -0500 Subject: [PATCH 156/199] Fix authentication service --- app/models/ability.rb | 1 + .../auth/container_registry_authentication_service.rb | 8 +++++++- .../container_registry_authentication_service_spec.rb | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 59d5195f5b..7432124046 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -61,6 +61,7 @@ class Ability :read_merge_request, :read_note, :read_commit_status, + :read_container_registry, :download_code ] diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 0323a42b69..a63e7046fc 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -1,6 +1,10 @@ module Auth class ContainerRegistryAuthenticationService < BaseService + AUDIENCE = 'container_registry' + def execute + return error('not found', 404) unless registry.enabled + if params[:offline_token] return error('forbidden', 403) unless current_user end @@ -52,9 +56,11 @@ module Auth end def can_access?(requested_project, requested_action) + return false unless requested_project.container_registry_enabled? + case requested_action when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) + requested_project == project || can?(current_user, :read_container_registry, requested_project) when 'push' requested_project == project || can?(current_user, :create_container_registry, requested_project) else diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 8dc47a24ee..6e86a3dcf5 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -7,6 +7,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } let(:registry_settings) do { + enabled: true, issuer: 'rspec', key: nil } From df973df8c536fb9ffe0a9ed4e92f7d9144eb687a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 16:04:26 -0500 Subject: [PATCH 157/199] Simplify JWT::RSAToken::kid --- lib/jwt/rsa_token.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb index 4de89bf0d3..d7df9269e1 100644 --- a/lib/jwt/rsa_token.rb +++ b/lib/jwt/rsa_token.rb @@ -29,10 +29,14 @@ module JWT end def kid - fingerprint = Digest::SHA256.digest(public_key.to_der) - Base32.encode(fingerprint).split('').each_slice(4).each_with_object([]) do |slice, mem| - mem << slice.join - end.join(':') + # calculate sha256 from DER encoded ASN1 + kid = Digest::SHA256.digest(public_key.to_der) + + # we encode only 30 bytes with base32 + kid = Base32.encode(kid[0..29]) + + # insert colon every 4 characters + kid.scan(/.{4}/).join(':') end end end From f4f9184a01bc7442411bbcffd9b6a86784fa5f53 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 18:23:31 -0500 Subject: [PATCH 158/199] Rename JWT to JSONWebToken --- app/controllers/jwt_controller.rb | 2 +- app/models/ability.rb | 12 ++++----- ...ntainer_registry_authentication_service.rb | 27 ++++++++----------- lib/{jwt => json_web_token}/rsa_token.rb | 2 +- lib/{jwt => json_web_token}/token.rb | 2 +- .../{jwt => json_web_token}/rsa_token_spec.rb | 2 +- .../lib/{jwt => json_web_token}/token_spec.rb | 2 +- ...er_registry_authentication_service_spec.rb | 2 +- 8 files changed, 23 insertions(+), 28 deletions(-) rename lib/{jwt => json_web_token}/rsa_token.rb (97%) rename lib/{jwt => json_web_token}/token.rb (97%) rename spec/lib/{jwt => json_web_token}/rsa_token_spec.rb (95%) rename spec/lib/{jwt => json_web_token}/token_spec.rb (92%) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index bd9d7e4425..0edf084e9e 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -4,7 +4,7 @@ class JwtController < ApplicationController before_action :authenticate_project_or_user SERVICES = { - 'container_registry' => Auth::ContainerRegistryAuthenticationService, + Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService, } def auth diff --git a/app/models/ability.rb b/app/models/ability.rb index 7432124046..f70268d313 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -61,7 +61,7 @@ class Ability :read_merge_request, :read_note, :read_commit_status, - :read_container_registry, + :read_container_image, :download_code ] @@ -204,7 +204,7 @@ class Ability :admin_label, :read_commit_status, :read_build, - :read_container_registry, + :read_container_image, ] end @@ -219,8 +219,8 @@ class Ability :create_merge_request, :create_wiki, :push_code, - :create_container_registry, - :update_container_registry, + :create_container_image, + :update_container_image, ] end @@ -247,7 +247,7 @@ class Ability :admin_project, :admin_commit_status, :admin_build, - :admin_container_registry, + :admin_container_image, ] end @@ -293,7 +293,7 @@ class Ability end unless project.container_registry_enabled - rules += named_abilities('container_registry') + rules += named_abilities('container_image') end rules diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index a63e7046fc..bbbc84475c 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -9,39 +9,34 @@ module Auth return error('forbidden', 403) unless current_user end - return error('forbidden', 401) if scopes.blank? + return error('forbidden', 401) unless scope - { token: authorized_token(scopes).encoded } + { token: authorized_token(scope).encoded } end private - def authorized_token(access) - token = ::JWT::RSAToken.new(registry.key) + def authorized_token(*accesses) + token = JSONWebToken::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = params[:service] token.subject = current_user.try(:username) - token[:access] = access + token[:access] = accesses token end - def scopes + def scope return unless params[:scope] - @scopes ||= begin - scope = process_scope(params[:scope]) - [scope].compact - end + @scope ||= process_scope(params[:scope]) end def process_scope(scope) type, name, actions = scope.split(':', 3) actions = actions.split(',') + return unless type == 'repository' - case type - when 'repository' - process_repository_access(type, name, actions) - end + process_repository_access(type, name, actions) end def process_repository_access(type, name, actions) @@ -60,9 +55,9 @@ module Auth case requested_action when 'pull' - requested_project == project || can?(current_user, :read_container_registry, requested_project) + requested_project == project || can?(current_user, :read_container_image, requested_project) when 'push' - requested_project == project || can?(current_user, :create_container_registry, requested_project) + requested_project == project || can?(current_user, :create_container_image, requested_project) else false end diff --git a/lib/jwt/rsa_token.rb b/lib/json_web_token/rsa_token.rb similarity index 97% rename from lib/jwt/rsa_token.rb rename to lib/json_web_token/rsa_token.rb index d7df9269e1..d6d6af7089 100644 --- a/lib/jwt/rsa_token.rb +++ b/lib/json_web_token/rsa_token.rb @@ -1,4 +1,4 @@ -module JWT +module JSONWebToken class RSAToken < Token attr_reader :key_file diff --git a/lib/jwt/token.rb b/lib/json_web_token/token.rb similarity index 97% rename from lib/jwt/token.rb rename to lib/json_web_token/token.rb index f13abf2b71..5b67715b0b 100644 --- a/lib/jwt/token.rb +++ b/lib/json_web_token/token.rb @@ -1,4 +1,4 @@ -module JWT +module JSONWebToken class Token attr_accessor :issuer, :subject, :audience, :id attr_accessor :issued_at, :not_before, :expire_time diff --git a/spec/lib/jwt/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb similarity index 95% rename from spec/lib/jwt/rsa_token_spec.rb rename to spec/lib/json_web_token/rsa_token_spec.rb index a5b1d3a67d..4462cdde9a 100644 --- a/spec/lib/jwt/rsa_token_spec.rb +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -1,4 +1,4 @@ -describe JWT::RSAToken do +describe JSONWebToken::RSAToken do let(:rsa_key) { generate_key } let(:rsa_token) { described_class.new(nil) } let(:rsa_encoded) { rsa_token.encoded } diff --git a/spec/lib/jwt/token_spec.rb b/spec/lib/json_web_token/token_spec.rb similarity index 92% rename from spec/lib/jwt/token_spec.rb rename to spec/lib/json_web_token/token_spec.rb index 92fdc3f1b7..3d955e4d77 100644 --- a/spec/lib/jwt/token_spec.rb +++ b/spec/lib/json_web_token/token_spec.rb @@ -1,4 +1,4 @@ -describe JWT::Token do +describe JSONWebToken::Token do let(:token) { described_class.new } context 'custom parameters' do diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 6e86a3dcf5..a293736813 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -18,7 +18,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do before do allow(Gitlab.config.registry).to receive_messages(registry_settings) - allow_any_instance_of(JWT::RSAToken).to receive(:key).and_return(rsa_key) + allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key) end shared_examples 'an authenticated' do From 0d93e93e794d179a4cbc5302aece27296e4abfe4 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Sat, 14 May 2016 23:14:25 +0900 Subject: [PATCH 159/199] Add tests for unintentional filtering bug in MR (cf. !3872) --- CHANGELOG | 2 +- .../user_lists_merge_requests_spec.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..b5ee7681df 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,7 +43,7 @@ v 8.8.0 (unreleased) - API support for the 'since' and 'until' operators on commit requests (Paco Guzman) - Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko) - Expire repository exists? and has_visible_content? caches after a push if necessary - - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) + - Fix unintentional filtering bug in Issue/MR sorted by milestone due (Takuya Noguchi) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 - Total method execution timings are no longer tracked diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index cc7f78e732..2c7e1c748a 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -38,6 +38,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true expect(page).to have_content 'lfs' expect(page).not_to have_content 'fix' expect(page).not_to have_content 'markdown' + expect(count_merge_requests).to eq(1) end it 'filters on a specific assignee' do @@ -46,6 +47,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true expect(page).not_to have_content 'lfs' expect(page).to have_content 'fix' expect(page).to have_content 'markdown' + expect(count_merge_requests).to eq(2) end it 'sorts by newest' do @@ -53,6 +55,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true expect(first_merge_request).to include('lfs') expect(last_merge_request).to include('fix') + expect(count_merge_requests).to eq(3) end it 'sorts by oldest' do @@ -60,30 +63,35 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true expect(first_merge_request).to include('fix') expect(last_merge_request).to include('lfs') + expect(count_merge_requests).to eq(3) end it 'sorts by last updated' do visit_merge_requests(project, sort: sort_value_recently_updated) expect(first_merge_request).to include('lfs') + expect(count_merge_requests).to eq(3) end it 'sorts by oldest updated' do visit_merge_requests(project, sort: sort_value_oldest_updated) expect(first_merge_request).to include('markdown') + expect(count_merge_requests).to eq(3) end it 'sorts by milestone due soon' do visit_merge_requests(project, sort: sort_value_milestone_soon) expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(3) end it 'sorts by milestone due later' do visit_merge_requests(project, sort: sort_value_milestone_later) expect(first_merge_request).to include('markdown') + expect(count_merge_requests).to eq(3) end it 'filters on one label and sorts by due soon' do @@ -94,6 +102,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true sort: sort_value_due_date_soon) expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) end context 'while filtering on two labels' do @@ -110,6 +119,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true sort: sort_value_due_date_soon) expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) end context 'filter on assignee and' do @@ -119,6 +129,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true sort: sort_value_due_date_soon) expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) end end end @@ -134,4 +145,8 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true def last_merge_request page.all('ul.mr-list > li').last.text end + + def count_merge_requests + page.all('ul.mr-list > li').count + end end From 8d445fe665df313c16a88c319fde9bdfb97339df Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 19:45:33 -0500 Subject: [PATCH 160/199] Improve JwtController --- app/controllers/jwt_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 0edf084e9e..f5aa5397ff 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -9,7 +9,7 @@ class JwtController < ApplicationController def auth service = SERVICES[params[:service]] - head :not_found unless service + return head :not_found unless service result = service.new(@project, @user, auth_params).execute From e8f7e5516bf0449f0d7f435ae720aa35f40e1dd0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 19:45:48 -0500 Subject: [PATCH 161/199] Added specs for JwtController --- spec/requests/jwt_controller_spec.rb | 87 ++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 spec/requests/jwt_controller_spec.rb diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb new file mode 100644 index 0000000000..782d710e1b --- /dev/null +++ b/spec/requests/jwt_controller_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe JwtController do + let(:services) { { 'test' => TestService } } + let(:parameters) { { service: 'test' } } + let(:ok_status) { { status: 'OK' } } + + before { allow_any_instance_of(JwtController).to receive(:SERVICES).and_return services } + + context 'existing service' do + before { expect_any_instance_of(TestService).to receive(:execute).and_return(ok_status) } + + subject! { get '/jwt/auth', parameters } + + it { expect(response.status).to eq(200) } + end + + context 'when using authorized request' do + context 'using CI token' do + let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } + let(:headers) { { HTTP_AUTHENTICATION: authorize('gitlab-ci-token', project.runners_token) } } + + context 'project with enabled CI' do + let(:builds_enabled) { true } + + it do + expect(TestService).to receive(:new).with(project, nil, parameters).and_call_original + + get '/jwt/auth', parameters, headers + end + end + + context 'project with disabled CI' do + let(:builds_enabled) { false } + + it do + expect(TestService).to receive(:new).with(project, nil, parameters).and_call_original + + get '/jwt/auth', parameters, headers + end + end + end + + context 'using User login' do + let(:user) { create(:user) } + let(:headers) { { HTTP_AUTHENTICATION: authorize('user', 'password') } } + + before { expect_any_instance_of(Gitlab::Auth).to receive(:find).with('user', 'password').and_return(user) } + + it do + expect(TestService).to receive(:new).with(nil, user, parameters).and_call_original + + get '/jwt/auth', parameters, headers + end + end + + context 'using invalid login' do + let(:headers) { { HTTP_AUTHENTICATION: authorize('invalid', 'password') } } + + subject! { get '/jwt/auth', parameters, headers } + + it { expect(response.status).to eq(403) } + end + end + + context 'unknown service' do + subject! { get '/jwt/auth', service: 'unknown' } + + it { expect(response.status).to eq(404) } + end + + def authorize(login, password) + ActionController::HttpAuthentication::Basic.encode_credentials(login, password) + end + + class TestService + attr_accessor :project, :current_user, :params + + def initialize(project, user, params = {}) + @project, @current_user, @params = project, user, params.dup + end + + def execute + { status: 'OK' } + end + end +end From 7b88dca77eeb2a93b5a343d27af513ea28222379 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 00:33:06 -0500 Subject: [PATCH 162/199] Update JwtController specs --- spec/requests/jwt_controller_spec.rb | 57 ++++++++++------------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 782d710e1b..7bb71365a4 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -1,61 +1,58 @@ require 'spec_helper' describe JwtController do - let(:services) { { 'test' => TestService } } - let(:parameters) { { service: 'test' } } - let(:ok_status) { { status: 'OK' } } + let(:service) { double(execute: {}) } + let(:service_class) { double(new: service) } + let(:service_name) { 'test' } + let(:parameters) { { service: service_name } } - before { allow_any_instance_of(JwtController).to receive(:SERVICES).and_return services } + before { stub_const('JwtController::SERVICES', service_name => service_class) } context 'existing service' do - before { expect_any_instance_of(TestService).to receive(:execute).and_return(ok_status) } - subject! { get '/jwt/auth', parameters } it { expect(response.status).to eq(200) } + + context 'returning custom http code' do + let(:service) { double(execute: { http_status: 505 }) } + + it { expect(response.status).to eq(505) } + end end context 'when using authorized request' do context 'using CI token' do let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } - let(:headers) { { HTTP_AUTHENTICATION: authorize('gitlab-ci-token', project.runners_token) } } + let(:headers) { { authorization: credentials('gitlab_ci_token', project.runners_token) } } + + subject! { get '/jwt/auth', parameters, headers } context 'project with enabled CI' do let(:builds_enabled) { true } - it do - expect(TestService).to receive(:new).with(project, nil, parameters).and_call_original - - get '/jwt/auth', parameters, headers - end + it { expect(service_class).to have_received(:new).with(project, nil, parameters) } end context 'project with disabled CI' do let(:builds_enabled) { false } - it do - expect(TestService).to receive(:new).with(project, nil, parameters).and_call_original - - get '/jwt/auth', parameters, headers - end + it { expect(response.status).to eq(403) } end end context 'using User login' do let(:user) { create(:user) } - let(:headers) { { HTTP_AUTHENTICATION: authorize('user', 'password') } } + let(:headers) { { authorization: credentials('user', 'password') } } before { expect_any_instance_of(Gitlab::Auth).to receive(:find).with('user', 'password').and_return(user) } - it do - expect(TestService).to receive(:new).with(nil, user, parameters).and_call_original + subject! { get '/jwt/auth', parameters, headers } - get '/jwt/auth', parameters, headers - end + it { expect(service_class).to have_received(:new).with(nil, user, parameters) } end context 'using invalid login' do - let(:headers) { { HTTP_AUTHENTICATION: authorize('invalid', 'password') } } + let(:headers) { { authorization: credentials('invalid', 'password') } } subject! { get '/jwt/auth', parameters, headers } @@ -69,19 +66,7 @@ describe JwtController do it { expect(response.status).to eq(404) } end - def authorize(login, password) + def credentials(login, password) ActionController::HttpAuthentication::Basic.encode_credentials(login, password) end - - class TestService - attr_accessor :project, :current_user, :params - - def initialize(project, user, params = {}) - @project, @current_user, @params = project, user, params.dup - end - - def execute - { status: 'OK' } - end - end end From dfd0e2450aabc3b5c322c4a4382edb84caa7101b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 08:52:26 -0500 Subject: [PATCH 163/199] Improve authentication service specs --- ...ntainer_registry_authentication_service.rb | 6 +-- spec/lib/json_web_token/rsa_token_spec.rb | 28 ++++++++---- ...er_registry_authentication_service_spec.rb | 44 +++++++++++++++---- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index bbbc84475c..c61d339ffd 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -7,10 +7,10 @@ module Auth if params[:offline_token] return error('forbidden', 403) unless current_user + else + return error('forbidden', 401) unless scope end - return error('forbidden', 401) unless scope - { token: authorized_token(scope).encoded } end @@ -21,7 +21,7 @@ module Auth token.issuer = registry.issuer token.audience = params[:service] token.subject = current_user.try(:username) - token[:access] = accesses + token[:access] = accesses.compact token end diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb index 4462cdde9a..0c3d3ea701 100644 --- a/spec/lib/json_web_token/rsa_token_spec.rb +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -1,5 +1,17 @@ describe JSONWebToken::RSAToken do - let(:rsa_key) { generate_key } + let(:rsa_key) do + OpenSSL::PKey::RSA.new <<-eos.strip_heredoc + -----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAMA5sXIBE0HwgIB40iNidN4PGWzOyLQK0bsdOBNgpEXkDlZBvnak + OUgAPF+rME4PB0Yl415DabUI40T5UNmlwxcCAwEAAQJAZtY2pSwIFm3JAXIh0cZZ + iXcAfiJ+YzuqinUOS+eW2sBCAEzjcARlU/o6sFQgtsOi4FOMczAd1Yx8UDMXMmrw + 2QIhAPBgVhJiTF09pdmeFWutCvTJDlFFAQNbrbo2X2x/9WF9AiEAzLgqMKeStSRu + H9N16TuDrUoO8R+DPqriCwkKrSHaWyMCIFzMhE4inuKcSywBaLmiG4m3GQzs++Al + A6PRG/PSTpQtAiBxtBg6zdf+JC3GH3zt/dA0/10tL4OF2wORfYQghRzyYQIhAL2l + 0ZQW+yLIZAGrdBFWYEAa52GZosncmzBNlsoTgwE4 + -----END RSA PRIVATE KEY----- + eos + end let(:rsa_token) { described_class.new(nil) } let(:rsa_encoded) { rsa_token.encoded } @@ -13,19 +25,19 @@ describe JSONWebToken::RSAToken do it { expect{subject}.to_not raise_error } it { expect(subject.first).to include('key' => 'value') } + it do + expect(subject.second).to eq( + "typ" => "JWT", + "alg" => "RS256", + "kid" => "OGXY:4TR7:FAVO:WEM2:XXEW:E4FP:TKL7:7ACK:TZAF:D54P:SUIA:P3B2") + end end context 'for invalid key to raise an exception' do - let(:new_key) { generate_key } + let(:new_key) { OpenSSL::PKey::RSA.generate(512) } subject { JWT.decode(rsa_encoded, new_key) } it { expect{subject}.to raise_error(JWT::DecodeError) } end end - - private - - def generate_key - OpenSSL::PKey::RSA.generate(512) - end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index a293736813..4a6cd132e8 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -57,15 +57,28 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end - shared_examples 'a forbidden' do + shared_examples 'a unauthorized' do it { is_expected.to include(http_status: 401) } it { is_expected.to_not include(:token) } end + shared_examples 'a forbidden' do + it { is_expected.to include(http_status: 403) } + it { is_expected.to_not include(:token) } + end + context 'user authorization' do let(:project) { create(:project) } let(:current_user) { create(:user) } + context 'allow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'an authenticated' + end + context 'allow developer to push images' do before { project.team << [current_user, :developer] } @@ -103,13 +116,21 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull,push" } end - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end end context 'project authorization' do let(:current_project) { create(:empty_project) } + context 'disallow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'a forbidden' + end + context 'allow to pull and push images' do let(:current_params) do { scope: "repository:#{current_project.path_with_namespace}:pull,push" } @@ -133,7 +154,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for private' do let(:project) { create(:empty_project, :private) } - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end end @@ -144,20 +165,27 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for all' do let(:project) { create(:empty_project, :public) } - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end end - end end context 'unauthorized' do + context 'disallow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'a forbidden' + end + context 'for invalid scope' do let(:current_params) do { scope: 'invalid:aa:bb' } end - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end context 'for private project' do @@ -167,7 +195,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull" } end - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end context 'for public project' do @@ -186,7 +214,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:push" } end - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end end end From 28ed9907047dd07089833d5b7bb8cd800e0ddff6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 10:46:54 -0500 Subject: [PATCH 164/199] Fix http status codes for container registry authentication service --- ...container_registry_authentication_service.rb | 4 ++-- ...iner_registry_authentication_service_spec.rb | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index c61d339ffd..72d28af7f7 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -6,9 +6,9 @@ module Auth return error('not found', 404) unless registry.enabled if params[:offline_token] - return error('forbidden', 403) unless current_user + return error('forbidden', 401) unless current_user else - return error('forbidden', 401) unless scope + return error('forbidden', 403) unless scope end { token: authorized_token(scope).encoded } diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 4a6cd132e8..3ea252ed44 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -57,11 +57,6 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end - shared_examples 'a unauthorized' do - it { is_expected.to include(http_status: 401) } - it { is_expected.to_not include(:token) } - end - shared_examples 'a forbidden' do it { is_expected.to include(http_status: 403) } it { is_expected.to_not include(:token) } @@ -116,7 +111,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull,push" } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end @@ -154,7 +149,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for private' do let(:project) { create(:empty_project, :private) } - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end @@ -165,7 +160,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for all' do let(:project) { create(:empty_project, :public) } - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end end @@ -185,7 +180,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: 'invalid:aa:bb' } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end context 'for private project' do @@ -195,7 +190,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull" } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end context 'for public project' do @@ -214,7 +209,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:push" } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end end From bc32a3ecc7ec2e5ebf6e8c9d9fc1b218a3620f36 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 10:47:38 -0500 Subject: [PATCH 165/199] Fix offline_token http_status code --- app/services/auth/container_registry_authentication_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 72d28af7f7..b636f55d03 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -6,7 +6,7 @@ module Auth return error('not found', 404) unless registry.enabled if params[:offline_token] - return error('forbidden', 401) unless current_user + return error('forbidden', 403) unless current_user else return error('forbidden', 403) unless scope end From b9306c2e82fec5b74416ccdd5481dfed3f8fbf51 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Date: Sun, 15 May 2016 19:47:41 +0100 Subject: [PATCH 166/199] Add cache count metrics to rails cache --- CHANGELOG | 1 + lib/gitlab/metrics/subscribers/rails_cache.rb | 12 +++++++----- .../metrics/subscribers/rails_cache_spec.rb | 16 +++++++++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..ca59f488e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,6 +51,7 @@ v 8.8.0 (unreleased) - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) - Hide left sidebar on phone screens to give more space for content - Redesign navigation for profile and group pages + - Add counter metrics for rails cache v 8.7.5 - Fix relative links in wiki pages. !4050 diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb index 49e5f86e6e..8e345e8ae4 100644 --- a/lib/gitlab/metrics/subscribers/rails_cache.rb +++ b/lib/gitlab/metrics/subscribers/rails_cache.rb @@ -6,26 +6,28 @@ module Gitlab attach_to :active_support def cache_read(event) - increment(:cache_read_duration, event.duration) + increment(:cache_read, event.duration) end def cache_write(event) - increment(:cache_write_duration, event.duration) + increment(:cache_write, event.duration) end def cache_delete(event) - increment(:cache_delete_duration, event.duration) + increment(:cache_delete, event.duration) end def cache_exist?(event) - increment(:cache_exists_duration, event.duration) + increment(:cache_exists, event.duration) end def increment(key, duration) return unless current_transaction current_transaction.increment(:cache_duration, duration) - current_transaction.increment(key, duration) + current_transaction.increment(:cache_count, 1) + current_transaction.increment("#{key}_duration".to_sym, duration) + current_transaction.increment("#{key}_count".to_sym, 1) end private diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb index e01b0b4bd2..d824dc5443 100644 --- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_read' do it 'increments the cache_read duration' do expect(subscriber).to receive(:increment). - with(:cache_read_duration, event.duration) + with(:cache_read, event.duration) subscriber.cache_read(event) end @@ -18,7 +18,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_write' do it 'increments the cache_write duration' do expect(subscriber).to receive(:increment). - with(:cache_write_duration, event.duration) + with(:cache_write, event.duration) subscriber.cache_write(event) end @@ -27,7 +27,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_delete' do it 'increments the cache_delete duration' do expect(subscriber).to receive(:increment). - with(:cache_delete_duration, event.duration) + with(:cache_delete, event.duration) subscriber.cache_delete(event) end @@ -36,7 +36,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_exist?' do it 'increments the cache_exists duration' do expect(subscriber).to receive(:increment). - with(:cache_exists_duration, event.duration) + with(:cache_exists, event.duration) subscriber.cache_exist?(event) end @@ -61,10 +61,16 @@ describe Gitlab::Metrics::Subscribers::RailsCache do expect(transaction).to receive(:increment). with(:cache_duration, event.duration) + expect(transaction).to receive(:increment). + with(:cache_count, 1) + expect(transaction).to receive(:increment). with(:cache_delete_duration, event.duration) - subscriber.increment(:cache_delete_duration, event.duration) + expect(transaction).to receive(:increment). + with(:cache_delete_count, 1) + + subscriber.increment(:cache_delete, event.duration) end end end From bec350528cdc81e26476780f1ca3db8171b3ece8 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 3 May 2016 12:42:55 +0100 Subject: [PATCH 167/199] Force password change after admin reset When an admin changes a user's password for them, force the user to reset the password after logging in by expiring the new password immediately. --- CHANGELOG | 1 + app/controllers/admin/users_controller.rb | 1 + .../admin/users_controller_spec.rb | 76 +++++++++++++++++++ spec/features/admin/admin_users_spec.rb | 3 + 4 files changed, 81 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ca59f488e0..02b94e5a0b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 8.8.0 (unreleased) - Display informative message when new milestone is created - Sanitize milestones and labels titles - Support multi-line tag messages. !3833 (Calin Seciu) + - Force users to reset their password after an admin changes it - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view - Backport GitHub Enterprise import support from EE diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f2f654c7bc..6908a3bf94 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -119,6 +119,7 @@ class Admin::UsersController < Admin::ApplicationController user_params_with_pass.merge!( password: params[:user][:password], password_confirmation: params[:user][:password_confirmation], + password_expires_at: Time.now ) end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index ce2a62ae1f..6caf37ddc2 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -114,6 +114,82 @@ describe Admin::UsersController do end end + describe 'POST update' do + context 'when the password has changed' do + def update_password(user, password, password_confirmation = nil) + params = { + id: user.to_param, + user: { + password: password, + password_confirmation: password_confirmation || password + } + } + + post :update, params + end + + context 'when the new password is valid' do + it 'redirects to the user' do + update_password(user, 'AValidPassword1') + + expect(response).to redirect_to(admin_user_path(user)) + end + + it 'updates the password' do + update_password(user, 'AValidPassword1') + + expect { user.reload }.to change { user.encrypted_password } + end + + it 'sets the new password to expire immediately' do + update_password(user, 'AValidPassword1') + + expect { user.reload }.to change { user.password_expires_at }.to(a_value <= Time.now) + end + end + + context 'when the new password is invalid' do + it 'shows the edit page again' do + update_password(user, 'invalid') + + expect(response).to render_template(:edit) + end + + it 'returns the error message' do + update_password(user, 'invalid') + + expect(assigns[:user].errors).to contain_exactly(a_string_matching(/too short/)) + end + + it 'does not update the password' do + update_password(user, 'invalid') + + expect { user.reload }.not_to change { user.encrypted_password } + end + end + + context 'when the new password does not match the password confirmation' do + it 'shows the edit page again' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect(response).to render_template(:edit) + end + + it 'returns the error message' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect(assigns[:user].errors).to contain_exactly(a_string_matching(/doesn't match/)) + end + + it 'does not update the password' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect { user.reload }.not_to change { user.encrypted_password } + end + end + end + end + describe "POST impersonate" do context "when the user is blocked" do before do diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 4570e40912..6dee0cd8d4 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -210,6 +210,8 @@ describe "Admin::Users", feature: true do before do fill_in "user_name", with: "Big Bang" fill_in "user_email", with: "bigbang@mail.com" + fill_in "user_password", with: "AValidPassword1" + fill_in "user_password_confirmation", with: "AValidPassword1" check "user_admin" click_button "Save changes" end @@ -223,6 +225,7 @@ describe "Admin::Users", feature: true do @simple_user.reload expect(@simple_user.name).to eq('Big Bang') expect(@simple_user.is_admin?).to be_truthy + expect(@simple_user.password_expires_at).to be <= Time.now end end end From 91480e5e7f2fb5e732839958b53c521bf7206939 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 12 May 2016 12:20:09 +0100 Subject: [PATCH 168/199] Tidy up IssuesFinder specs - Don't do setup in spec bodies. - Don't `describe` a symbol. - Don't use 'should'. --- spec/finders/issues_finder_spec.rb | 150 ++++++++++++++++------------- 1 file changed, 81 insertions(+), 69 deletions(-) diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index bc607a2975..f905703dd6 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe IssuesFinder do - let(:user) { create :user } - let(:user2) { create :user } - let(:project1) { create(:project) } - let(:project2) { create(:project) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:project1) { create(:empty_project) } + let(:project2) { create(:empty_project) } let(:milestone) { create(:milestone, project: project1) } let(:label) { create(:label, project: project2) } let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) } @@ -16,101 +16,113 @@ describe IssuesFinder do project1.team << [user, :master] project2.team << [user, :developer] project2.team << [user2, :developer] + + issue1 + issue2 + issue3 end - describe :execute do - before :each do - issue1 - issue2 - issue3 - end + describe '#execute' do + let(:search_user) { user } + let(:params) { {} } + let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute } context 'scope: all' do - it 'should filter by all' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(3) + let(:scope) { 'all' } + + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3) end - it 'should filter by assignee id' do - params = { scope: "all", assignee_id: user.id, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(2) + context 'filtering by assignee ID' do + let(:params) { { assignee_id: user.id } } + + it 'returns issues assigned to that user' do + expect(issues).to contain_exactly(issue1, issue2) + end end - it 'should filter by author id' do - params = { scope: "all", author_id: user2.id, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to eq([issue3]) + context 'filtering by author ID' do + let(:params) { { author_id: user2.id } } + + it 'returns issues created by that user' do + expect(issues).to contain_exactly(issue3) + end end - it 'should filter by milestone id' do - params = { scope: "all", milestone_title: milestone.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to eq([issue1]) + context 'filtering by milestone' do + let(:params) { { milestone_title: milestone.title } } + + it 'returns issues assigned to that milestone' do + expect(issues).to contain_exactly(issue1) + end end - it 'should filter by no milestone id' do - params = { scope: "all", milestone_title: Milestone::None.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to match_array([issue2, issue3]) + context 'filtering by no milestone' do + let(:params) { { milestone_title: Milestone::None.title } } + + it 'returns issues with no milestone' do + expect(issues).to contain_exactly(issue2, issue3) + end end - it 'should filter by label name' do - params = { scope: "all", label_name: label.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to eq([issue2]) + context 'filtering by label' do + let(:params) { { label_name: label.title } } + + it 'returns issues with that label' do + expect(issues).to contain_exactly(issue2) + end end - it 'returns unique issues when filtering by multiple labels' do - label2 = create(:label, project: project2) + context 'filtering by multiple labels' do + let(:params) { { label_name: [label.title, label2.title].join(',') } } + let(:label2) { create(:label, project: project2) } - create(:label_link, label: label2, target: issue2) + before { create(:label_link, label: label2, target: issue2) } - params = { - scope: 'all', - label_name: [label.title, label2.title].join(','), - state: 'opened' - } - - issues = IssuesFinder.new(user, params).execute - - expect(issues).to eq([issue2]) + it 'returns the unique issues with any of those labels' do + expect(issues).to contain_exactly(issue2) + end end - it 'should filter by no label name' do - params = { scope: "all", label_name: Label::None.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to match_array([issue1, issue3]) + context 'filtering by no label' do + let(:params) { { label_name: Label::None.title } } + + it 'returns issues with no labels' do + expect(issues).to contain_exactly(issue1, issue3) + end end - it 'should be empty for unauthorized user' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new(nil, params).execute - expect(issues.size).to be_zero + context 'when the user is unauthorized' do + let(:search_user) { nil } + + it 'returns no results' do + expect(issues).to be_empty + end end - it 'should not include unauthorized issues' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new(user2, params).execute - expect(issues.size).to eq(2) - expect(issues).not_to include(issue1) - expect(issues).to include(issue2) - expect(issues).to include(issue3) + context 'when the user can see some, but not all, issues' do + let(:search_user) { user2 } + + it 'returns only issues they can see' do + expect(issues).to contain_exactly(issue2, issue3) + end end end context 'personal scope' do - it 'should filter by assignee' do - params = { scope: "assigned-to-me", state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(2) + let(:scope) { 'assigned-to-me' } + + it 'returns issue assigned to the user' do + expect(issues).to contain_exactly(issue1, issue2) end - it 'should filter by project' do - params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(1) + context 'filtering by project' do + let(:params) { { project_id: project1.id } } + + it 'returns issues assigned to the user in that project' do + expect(issues).to contain_exactly(issue1) + end end end end From 750b2ff0eec67926e737a40c7975cce2b58e27f7 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 11 May 2016 17:38:34 +0100 Subject: [PATCH 169/199] Make upcoming milestone work across projects Before: we took the next milestone due across all projects in the search and found issues whose milestone title matched that one. Problems: 1. The milestone could be closed. 2. Different projects have milestones with different schedules. 3. Different projects have milestones with different titles. 4. Different projects can have milestones with different schedules, but the _same_ title. That means we could show issues from a past milestone, or one that's far in the future. After: gather the ID of the next milestone on each project we're looking at, and find issues with those milestone IDs. Problems: 1. For a lot of projects, this can return a lot of IDs. 2. The SQL query has to be different between Postgres and MySQL, because MySQL is much more lenient with HAVING: as well as the columns appearing in GROUP BY or in aggregate clauses, MySQL allows them to appear in the SELECT list (un-aggregated). --- CHANGELOG | 1 + app/finders/issuable_finder.rb | 4 ++-- app/models/milestone.rb | 14 ++++++++++-- spec/finders/issues_finder_spec.rb | 34 ++++++++++++++++++++++++++++++ spec/models/milestone_spec.rb | 31 +++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ca59f488e0..50937f5f83 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.8.0 (unreleased) - Make build status canceled if any of the jobs was canceled and none failed - Upgrade Sidekiq to 4.1.2 - Added /health_check endpoint for checking service status + - Make 'upcoming' filter for milestones work better across projects - Sanitize repo paths in new project error message - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f00f3f709e..5849e00662 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -252,8 +252,8 @@ class IssuableFinder if filter_by_no_milestone? items = items.where(milestone_id: [-1, nil]) elsif filter_by_upcoming_milestone? - upcoming = Milestone.where(project_id: projects).upcoming - items = items.joins(:milestone).where(milestones: { title: upcoming.try(:title) }) + upcoming_ids = Milestone.upcoming_ids_by_projects(projects) + items = items.joins(:milestone).where(milestone_id: upcoming_ids) else items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index e4fdd23bad..6b01e48d7f 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -67,8 +67,18 @@ class Milestone < ActiveRecord::Base @link_reference_pattern ||= super("milestones", /(?\d+)/) end - def self.upcoming - self.where('due_date > ?', Time.now).reorder(due_date: :asc).first + def self.upcoming_ids_by_projects(projects) + rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now) + + if Gitlab::Database.postgresql? + rel.order(:project_id, :due_date).pluck('DISTINCT ON (project_id) id') + else + rel. + group(:project_id). + having('due_date = MIN(due_date)'). + pluck(:id, :project_id, :due_date). + map(&:first) + end end def to_reference(from_project = nil) diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index f905703dd6..ec8809e692 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -66,6 +66,40 @@ describe IssuesFinder do end end + context 'filtering by upcoming milestone' do + let(:params) { { milestone_title: Milestone::Upcoming.name } } + + let(:project_no_upcoming_milestones) { create(:empty_project, :public) } + let(:project_next_1_1) { create(:empty_project, :public) } + let(:project_next_8_8) { create(:empty_project, :public) } + + let(:yesterday) { Date.today - 1.day } + let(:tomorrow) { Date.today + 1.day } + let(:two_days_from_now) { Date.today + 2.days } + let(:ten_days_from_now) { Date.today + 10.days } + + let(:milestones) do + [ + create(:milestone, :closed, project: project_no_upcoming_milestones), + create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now), + create(:milestone, project: project_next_1_1, title: '8.8', due_date: ten_days_from_now), + create(:milestone, project: project_next_8_8, title: '1.1', due_date: yesterday), + create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow) + ] + end + + before do + milestones.each do |milestone| + create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user) + end + end + + it 'returns issues in the upcoming milestone for each project' do + expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8') + expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now) + end + end + context 'filtering by label' do let(:params) { { label_name: label.title } } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 247a9fa991..210c5f7eb4 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -204,4 +204,35 @@ describe Milestone, models: true do to eq([milestone]) end end + + describe '.upcoming_ids_by_projects' do + let(:project_1) { create(:empty_project) } + let(:project_2) { create(:empty_project) } + let(:project_3) { create(:empty_project) } + let(:projects) { [project_1, project_2, project_3] } + + let!(:past_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now - 1.day) } + let!(:current_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 1.day) } + let!(:future_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 2.days) } + + let!(:past_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now - 1.day) } + let!(:closed_milestone_project_2) { create(:milestone, :closed, project: project_2, due_date: Time.now + 1.day) } + let!(:current_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now + 2.days) } + + let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) } + + let(:milestone_ids) { Milestone.upcoming_ids_by_projects(projects) } + + it 'returns the next upcoming open milestone ID for each project' do + expect(milestone_ids).to contain_exactly(current_milestone_project_1.id, current_milestone_project_2.id) + end + + context 'when the projects have no open upcoming milestones' do + let(:projects) { [project_3] } + + it 'returns no results' do + expect(milestone_ids).to be_empty + end + end + end end From e8058bd23100949607ac8c353f482067c0ecd25a Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 16 May 2016 10:23:21 +0100 Subject: [PATCH 170/199] Return a relation with Postgres Postgres only needs to select a single column, so that can used as a sub-query where `Milestone.upcoming_ids_by_projects` is actually used in `IssuableFinder`. MySQL needs to select the `due_date` column because it's used in the `HAVING` clause, so it has to return an array of IDs. --- app/models/milestone.rb | 2 +- spec/models/milestone_spec.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 6b01e48d7f..fe9a281f36 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -71,7 +71,7 @@ class Milestone < ActiveRecord::Base rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now) if Gitlab::Database.postgresql? - rel.order(:project_id, :due_date).pluck('DISTINCT ON (project_id) id') + rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id') else rel. group(:project_id). diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 210c5f7eb4..1e18c788b5 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -221,7 +221,9 @@ describe Milestone, models: true do let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) } - let(:milestone_ids) { Milestone.upcoming_ids_by_projects(projects) } + # The call to `#try` is because this returns a relation with a Postgres DB, + # and an array of IDs with a MySQL DB. + let(:milestone_ids) { Milestone.upcoming_ids_by_projects(projects).map { |id| id.try(:id) || id } } it 'returns the next upcoming open milestone ID for each project' do expect(milestone_ids).to contain_exactly(current_milestone_project_1.id, current_milestone_project_2.id) From 72577033888a77eeb2f4d362c2ae2331c1cbf6d7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 09:45:18 -0500 Subject: [PATCH 171/199] Bring back port to registry configuration --- config/gitlab.yml.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 2e383bc90f..d935121d88 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -178,7 +178,8 @@ production: &base registry: # enabled: true - # host: localhost + # host: registry.example.com + # port: 5000 # api_url: http://localhost:5000/ # key: config/registry.key # issuer: omnibus-certificate From 114d023b1b6f0e14adefb72980ec1658946bd897 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Tue, 17 May 2016 01:41:26 +0900 Subject: [PATCH 172/199] Clean up the images from GitLab CI --- app/assets/images/ci/arch.jpg | Bin 25222 -> 0 bytes app/assets/images/ci/favicon.ico | Bin 5430 -> 0 bytes app/assets/images/ci/loader.gif | Bin 4405 -> 0 bytes app/assets/images/ci/no_avatar.png | Bin 1337 -> 0 bytes app/assets/images/ci/rails.png | Bin 6646 -> 0 bytes app/assets/images/ci/service_sample.png | Bin 76024 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/assets/images/ci/arch.jpg delete mode 100644 app/assets/images/ci/favicon.ico delete mode 100644 app/assets/images/ci/loader.gif delete mode 100644 app/assets/images/ci/no_avatar.png delete mode 100644 app/assets/images/ci/rails.png delete mode 100644 app/assets/images/ci/service_sample.png diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg deleted file mode 100644 index 0e05674e840c6946d5a40f340790289af2dc25f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25222 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 diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif deleted file mode 100644 index 2fcb8f2da0d283628878cbc74b9dd848109a4535..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4405 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 AQ~&?~ diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png deleted file mode 100644 index 65d29e3fd891b7b8deba8670cb7d74b03056e3d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76024 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 z 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 Vw Date: Mon, 16 May 2016 12:44:07 -0500 Subject: [PATCH 173/199] Fix left sidebar overlap when scroll on long menu Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/gitlab-theme.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index f47eb1f233..16cf394c42 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -9,6 +9,8 @@ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { .page-with-sidebar { .header-logo { + background: $color-darker; + a { color: $color-light; From 71ca2de7aabf3191c4f486ca15a78a5b7f6abd94 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 21 Apr 2016 15:55:54 -0300 Subject: [PATCH 174/199] Toggle email signup confirmation in admin settings --- CHANGELOG | 1 + .../admin/application_settings_controller.rb | 1 + app/models/user.rb | 5 +++ .../application_settings/_form.html.haml | 6 ++++ ...firmation_email_to_application_settings.rb | 8 +++++ .../registrations_controller_spec.rb | 33 +++++++++++++++++++ 6 files changed, 54 insertions(+) create mode 100644 db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb create mode 100644 spec/controllers/registrations_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index ca59f488e0..7a86263c0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -189,6 +189,7 @@ v 8.7.0 - Add Slack notifications when Wiki is edited (Sebastian Klier) - Diffs load at the correct point when linking from from number - Selected diff rows highlight + - Toggle sign-up confirmation emails in application settings - Fix emoji categories in the emoji picker - API: Properly display annotated tags for GET /projects/:id/repository/tags (Robert Schilling) - Add encrypted credentials for imported projects and migrate old ones diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8c973f0e4a..956d145f02 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -106,6 +106,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :email_author_in_body, :repository_checks_enabled, :metrics_packet_size, + :skip_user_confirmation_email, restricted_visibility_levels: [], import_sources: [], disabled_oauth_sign_in_sources: [] diff --git a/app/models/user.rb b/app/models/user.rb index 489bff3fa4..470734f5c2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -112,6 +112,7 @@ class User < ActiveRecord::Base before_save :ensure_external_user_rights after_save :ensure_namespace_correct after_initialize :set_projects_limit + before_create :check_confirmation_email after_create :post_create_hook after_destroy :post_destroy_hook @@ -307,6 +308,10 @@ class User < ActiveRecord::Base @reset_token end + def check_confirmation_email + skip_confirmation! if current_application_settings.skip_user_confirmation_email + end + def recently_sent_password_reset? reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index f7c799c968..6d6d87cdd5 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -103,6 +103,12 @@ = f.label :signup_enabled do = f.check_box :signup_enabled Sign-up enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :skip_confirmation_email do + = f.check_box :skip_user_confirmation_email + Skip sign-up email confirmation .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb b/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb new file mode 100644 index 0000000000..953f1cea89 --- /dev/null +++ b/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb @@ -0,0 +1,8 @@ +class AddSkipConfirmationEmailToApplicationSettings < ActiveRecord::Migration + def change + #Skip confirmation emails just for new installations + default_value = User.count > 0 ? false : true + + add_column :application_settings, :skip_user_confirmation_email, :boolean, default: default_value + end +end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb new file mode 100644 index 0000000000..b4ab767f73 --- /dev/null +++ b/spec/controllers/registrations_controller_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe RegistrationsController do + describe '#create' do + around(:each) do |example| + perform_enqueued_jobs do + example.run + end + end + + let(:user_params) { { "user"=> {"name"=>"new_user", "username"=>"new_username", "email"=>"new@user.com", "password"=>"Any_password"} } } + + context 'when skipping email confirmation' do + before { allow(current_application_settings).to receive(:skip_user_confirmation_email).and_return(true) } + + it 'logs user in directly' do + post(:create, user_params) + expect(ActionMailer::Base.deliveries.last).to be_nil + expect(subject.current_user).to be + end + end + + context 'when not skipping email confirmation' do + before { allow(current_application_settings).to receive(:skip_user_confirmation_email).and_return(false) } + + it 'does not authenticate user and sends confirmation email' do + post(:create, user_params) + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params["user"]["email"]) + expect(subject.current_user).to be_nil + end + end + end +end From c5526a2d9a946d99d7b4a72fc488fe6e0a9ad60b Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 28 Apr 2016 17:09:15 -0300 Subject: [PATCH 175/199] Change skip_user_confirmation_email to send_user_confirmation_email --- CHANGELOG | 5 ++++- .../admin/application_settings_controller.rb | 2 +- app/models/user.rb | 3 ++- app/views/admin/application_settings/_form.html.haml | 4 ++-- ...end_confirmation_email_to_application_settings.rb | 12 ++++++++++++ ...kip_confirmation_email_to_application_settings.rb | 8 -------- spec/controllers/registrations_controller_spec.rb | 12 ++++++------ spec/features/signup_spec.rb | 2 ++ spec/models/user_spec.rb | 1 + 9 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb delete mode 100644 db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb diff --git a/CHANGELOG b/CHANGELOG index 7a86263c0a..5841da3ac9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,8 @@ v 8.8.0 (unreleased) - Display informative message when new milestone is created - Sanitize milestones and labels titles - Support multi-line tag messages. !3833 (Calin Seciu) + - Toggle sign-up confirmation emails in application settings + - Replace Devise Async with Devise ActiveJob integration. !3902 (Connor Shea) - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view - Backport GitHub Enterprise import support from EE @@ -93,6 +95,8 @@ v 8.7.1 v 8.7.0 - Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented - Fix vulnerability that made it possible to gain access to private labels and milestones + +v 8.7.0 (unreleased) - The number of InfluxDB points stored per UDP packet can now be configured - Fix error when cross-project label reference used with non-existent project - Transactions for /internal/allowed now have an "action" tag set @@ -189,7 +193,6 @@ v 8.7.0 - Add Slack notifications when Wiki is edited (Sebastian Klier) - Diffs load at the correct point when linking from from number - Selected diff rows highlight - - Toggle sign-up confirmation emails in application settings - Fix emoji categories in the emoji picker - API: Properly display annotated tags for GET /projects/:id/repository/tags (Robert Schilling) - Add encrypted credentials for imported projects and migrate old ones diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 956d145f02..ff7a5cad2f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -106,7 +106,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :email_author_in_body, :repository_checks_enabled, :metrics_packet_size, - :skip_user_confirmation_email, + :send_user_confirmation_email, restricted_visibility_levels: [], import_sources: [], disabled_oauth_sign_in_sources: [] diff --git a/app/models/user.rb b/app/models/user.rb index 470734f5c2..bcadbd3e2f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -309,7 +309,8 @@ class User < ActiveRecord::Base end def check_confirmation_email - skip_confirmation! if current_application_settings.skip_user_confirmation_email + byebug + skip_confirmation! unless current_application_settings.send_user_confirmation_email end def recently_sent_password_reset? diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 6d6d87cdd5..289dda9a43 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -107,8 +107,8 @@ .col-sm-offset-2.col-sm-10 .checkbox = f.label :skip_confirmation_email do - = f.check_box :skip_user_confirmation_email - Skip sign-up email confirmation + = f.check_box :send_user_confirmation_email + Send sign-up email confirmation .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb b/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb new file mode 100644 index 0000000000..0fef2a831e --- /dev/null +++ b/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb @@ -0,0 +1,12 @@ +class AddSendConfirmationEmailToApplicationSettings < ActiveRecord::Migration + def up + add_column :application_settings, :send_user_confirmation_email, :boolean, default: false + + #Sets confirmation email to true by default on existing installations. + ApplicationSetting.update_all(send_user_confirmation_email: true) + end + + def down + remove_column :application_settings, :send_user_confirmation_email + end +end diff --git a/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb b/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb deleted file mode 100644 index 953f1cea89..0000000000 --- a/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb +++ /dev/null @@ -1,8 +0,0 @@ -class AddSkipConfirmationEmailToApplicationSettings < ActiveRecord::Migration - def change - #Skip confirmation emails just for new installations - default_value = User.count > 0 ? false : true - - add_column :application_settings, :skip_user_confirmation_email, :boolean, default: default_value - end -end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index b4ab767f73..29f1847d9a 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -8,10 +8,10 @@ describe RegistrationsController do end end - let(:user_params) { { "user"=> {"name"=>"new_user", "username"=>"new_username", "email"=>"new@user.com", "password"=>"Any_password"} } } + let(:user_params) { { user: { name: "new_user", username: "new_username", email: "new@user.com", password: "Any_password" } } } - context 'when skipping email confirmation' do - before { allow(current_application_settings).to receive(:skip_user_confirmation_email).and_return(true) } + context 'when sending email confirmation' do + before { allow(current_application_settings).to receive(:send_user_confirmation_email).and_return(false) } it 'logs user in directly' do post(:create, user_params) @@ -20,12 +20,12 @@ describe RegistrationsController do end end - context 'when not skipping email confirmation' do - before { allow(current_application_settings).to receive(:skip_user_confirmation_email).and_return(false) } + context 'when not sending email confirmation' do + before { allow(current_application_settings).to receive(:send_user_confirmation_email).and_return(true) } it 'does not authenticate user and sends confirmation email' do post(:create, user_params) - expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params["user"]["email"]) + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) expect(subject.current_user).to be_nil end end diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 58aabd913e..c7840f26d8 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' feature 'Signup', feature: true do describe 'signup with no errors' do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } + it 'creates the user account and sends a confirmation email' do user = build(:user) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 10e7e69357..9581990666 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -141,6 +141,7 @@ describe User, models: true do end describe '#confirm' do + before { allow(current_application_settings).to receive(:send_user_confirmation_email).and_return(true) } let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') } it 'returns unconfirmed' do From 0baadd6cff63d5ac0048043381a0b7daff6e01c3 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 28 Apr 2016 17:22:35 -0300 Subject: [PATCH 176/199] Fix changelog file --- CHANGELOG | 2 -- app/models/user.rb | 1 - 2 files changed, 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5841da3ac9..7ebe10c0f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -95,8 +95,6 @@ v 8.7.1 v 8.7.0 - Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented - Fix vulnerability that made it possible to gain access to private labels and milestones - -v 8.7.0 (unreleased) - The number of InfluxDB points stored per UDP packet can now be configured - Fix error when cross-project label reference used with non-existent project - Transactions for /internal/allowed now have an "action" tag set diff --git a/app/models/user.rb b/app/models/user.rb index bcadbd3e2f..368a3f3cfb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -309,7 +309,6 @@ class User < ActiveRecord::Base end def check_confirmation_email - byebug skip_confirmation! unless current_application_settings.send_user_confirmation_email end From 7bb84e64979edda8e76f077bd58aeb35857aec23 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 6 May 2016 17:59:45 -0300 Subject: [PATCH 177/199] Change landing page when skipping confirmation email and add documentation --- app/controllers/registrations_controller.rb | 4 +- .../application_settings/_form.html.haml | 4 +- ...firmation_email_to_application_settings.rb | 2 +- doc/security/README.md | 1 + doc/security/user_email_confirmation.md | 7 +++ .../registrations_controller_spec.rb | 2 +- spec/features/signup_spec.rb | 45 ++++++++++++++----- 7 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 doc/security/user_email_confirmation.md diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 352bff1938..26eb15f49e 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -37,8 +37,8 @@ class RegistrationsController < Devise::RegistrationsController super end - def after_sign_up_path_for(_resource) - users_almost_there_path + def after_sign_up_path_for(user) + user.confirmed_at.present? ? dashboard_projects_path : users_almost_there_path end def after_inactive_sign_up_path_for(_resource) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 289dda9a43..df286852b9 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -106,9 +106,9 @@ .form-group .col-sm-offset-2.col-sm-10 .checkbox - = f.label :skip_confirmation_email do + = f.label :send_user_confirmation_email do = f.check_box :send_user_confirmation_email - Send sign-up email confirmation + Send confirmation email on sign-up .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb b/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb index 0fef2a831e..f92ef96050 100644 --- a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb +++ b/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb @@ -3,7 +3,7 @@ class AddSendConfirmationEmailToApplicationSettings < ActiveRecord::Migration add_column :application_settings, :send_user_confirmation_email, :boolean, default: false #Sets confirmation email to true by default on existing installations. - ApplicationSetting.update_all(send_user_confirmation_email: true) + execute "UPDATE application_settings SET send_user_confirmation_email=true" end def down diff --git a/doc/security/README.md b/doc/security/README.md index 4cd0fdd409..38706e48ec 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -8,3 +8,4 @@ - [User File Uploads](user_file_uploads.md) - [How we manage the CRIME vulnerability](crime_vulnerability.md) - [Enforce Two-factor authentication](two_factor_authentication.md) +- [Send email confirmation on sign-up](user_email_confirmation.md) diff --git a/doc/security/user_email_confirmation.md b/doc/security/user_email_confirmation.md new file mode 100644 index 0000000000..4293944ae8 --- /dev/null +++ b/doc/security/user_email_confirmation.md @@ -0,0 +1,7 @@ +# User email confirmation at sign-up + +Gitlab admin can enable email confirmation on sign-up, if you want to confirm all +user emails before they are able to sign-in. + +In the Admin area under **Settings** (`/admin/application_settings`), go to section +**Sign-in Restrictions** and look for **Send confirmation email on sign-up** option. diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 29f1847d9a..df70a589a8 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -16,7 +16,7 @@ describe RegistrationsController do it 'logs user in directly' do post(:create, user_params) expect(ActionMailer::Base.deliveries.last).to be_nil - expect(subject.current_user).to be + expect(subject.current_user).to_not be_nil end end diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index c7840f26d8..4229e82b44 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -2,22 +2,45 @@ require 'spec_helper' feature 'Signup', feature: true do describe 'signup with no errors' do - before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } - it 'creates the user account and sends a confirmation email' do - user = build(:user) + context "when sending confirmation email" do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } - visit root_path + it 'creates the user account and sends a confirmation email' do + user = build(:user) - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_password', with: user.password - click_button "Sign up" + visit root_path - expect(current_path).to eq users_almost_there_path - expect(page).to have_content("Please check your email to confirm your account") + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: user.password + click_button "Sign up" + + expect(current_path).to eq users_almost_there_path + expect(page).to have_content("Please check your email to confirm your account") + end end + + context "when not sending confirmation email" do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) } + + it 'creates the user account and goes to dashboard' do + user = build(:user) + + visit root_path + + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: user.password + click_button "Sign up" + + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Welcome! You have signed up successfully.") + end + end + end describe 'signup with errors' do From c9be74e24797c1dab5b443728349bb0c5ce969c3 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 16 May 2016 16:43:19 -0300 Subject: [PATCH 178/199] Fix single note api request --- lib/api/helpers.rb | 4 ++++ lib/api/notes.rb | 10 +++++----- spec/requests/api/notes_spec.rb | 17 ++++++++++++++--- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 40c967453f..1003b596ae 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -397,5 +397,9 @@ module API error!(errors[:access_level], 422) if errors[:access_level].any? not_found!(errors) end + + def noteable_ability_name(noteable) + "read_#{noteable.class.to_s.underscore.downcase}".to_sym + end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index f0116acd90..c49b107d1d 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -20,9 +20,8 @@ module API # GET /projects/:id/snippets/:noteable_id/notes get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) - read_ability_name = "read_#{@noteable.class.to_s.underscore.downcase}".to_sym - if can?(current_user, read_ability_name, @noteable) + if can?(current_user, noteable_ability_name(@noteable), @noteable) # We exclude notes that are cross-references and that cannot be viewed # by the current user. By doing this exclusion at this level and not # at the DB query level (which we cannot in that case), the current @@ -52,11 +51,12 @@ module API get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) @note = @noteable.notes.find(params[:note_id]) + can_read_note = can?(current_user, noteable_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user) - if @note.cross_reference_not_visible_for?(current_user) - not_found!("Note") - else + if can_read_note present @note, with: Entities::Note + else + not_found!("Note") end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index f9bfee9442..ed1ed5aeb9 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace) } + let!(:project) { create(:project, :public, namespace: user.namespace) } let!(:issue) { create(:issue, project: project, author: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } let!(:snippet) { create(:project_snippet, project: project, author: user) } @@ -51,7 +51,7 @@ describe API::API, api: true do expect(response.status).to eq(404) end - context "that references a private issue" do + context "and current user cannot view the notes" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) @@ -142,13 +142,24 @@ describe API::API, api: true do expect(response.status).to eq(404) end - context "that references a private issue" do + context "and current user cannot view the note" do it "should return a 404 error" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) expect(response.status).to eq(404) end + context "when issue is confidential" do + before { issue.update_attributes(confidential: true) } + + it "returns 404" do + get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user) + + expect(response.status).to eq(404) + end + end + + context "and current user can view the note" do it "should return an issue note by id" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) From 6286b28bbc9b62af729c45c45f953f5426f5bf52 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 14:05:45 -0400 Subject: [PATCH 179/199] Add an Event's target's title to its reference link Given an activity feed entry like: > Douwe Maan commented on [issue #123] at [gitlab-org/gitlab-ce] ...the `issue #123` link will now have a `title` attribute. --- app/helpers/events_helper.rb | 2 +- app/models/event.rb | 2 +- app/models/note.rb | 1 + app/views/events/event/_common.html.haml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 0bf328e7d1..2b33889b46 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -162,7 +162,7 @@ module EventsHelper "#{event.note_target_type} #{truncate event.note_target.to_reference}" end else - link_to event_note_target_path(event) do + link_to event_note_target_path(event), title: event.target_title do "#{event.note_target_type} #{truncate event.note_target.to_reference}" end end diff --git a/app/models/event.rb b/app/models/event.rb index 17ee48b91a..b54fbe6f7e 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -80,7 +80,7 @@ class Event < ActiveRecord::Base end def target_title - target.title if target && target.respond_to?(:title) + target.try(:title) end def created? diff --git a/app/models/note.rb b/app/models/note.rb index 7e5bdc09a8..55b9855724 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -19,6 +19,7 @@ class Note < ActiveRecord::Base delegate :gfm_reference, :local_reference, to: :noteable delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true + delegate :title, to: :noteable, allow_nil: true before_validation :set_award! diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index c994e3b997..f9f623cc03 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -4,7 +4,7 @@ = event_action_name(event) - if event.target - %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target] + %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target], title: event.target_title = event_preposition(event) From c05ed99bff637f51d16cc34663b911e46295a473 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 14:46:43 -0400 Subject: [PATCH 180/199] Simplify Event's target type-checking --- app/models/event.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/event.rb b/app/models/event.rb index b54fbe6f7e..a617694b7d 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -279,15 +279,15 @@ class Event < ActiveRecord::Base end def note_commit? - target.noteable_type == "Commit" + target.for_commit? end def issue_note? - note? && target && target.noteable_type == "Issue" + note? && target && target.for_issue? end def note_project_snippet? - target.noteable_type == "Snippet" + target.for_snippet? end def note_target From 3950f8074c078fb9320c3e9a468da15feb2d103e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 15:28:21 -0400 Subject: [PATCH 181/199] Fix titles for commit references in Event feed Prior, the `title` attribute was being included as an argument to the route helper rather than as an argument to `link_to`. --- app/helpers/events_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 2b33889b46..73320d8841 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -150,7 +150,8 @@ module EventsHelper link_to( namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, - anchor: dom_id(event.target), title: h(event.target_title)), + anchor: dom_id(event.target)), + title: h(event.target_title), class: "commit_short_id" ) do "#{event.note_target_type} #{event.note_short_commit_id}" From 8a7b5b17d325e13c989098c7e763de1f43505780 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 15:37:43 -0400 Subject: [PATCH 182/199] Remove redundant `html_escape` calls from `title` attributes These get escaped automatically. --- app/helpers/events_helper.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 73320d8841..d0a63d5880 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -3,7 +3,7 @@ module EventsHelper author = event.author if author - link_to author.name, user_path(author.username), title: h(author.name) + link_to author.name, user_path(author.username), title: author.name else event.author_name end @@ -151,7 +151,7 @@ module EventsHelper namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, anchor: dom_id(event.target)), - title: h(event.target_title), + title: event.target_title, class: "commit_short_id" ) do "#{event.note_target_type} #{event.note_short_commit_id}" @@ -159,7 +159,7 @@ module EventsHelper elsif event.note_project_snippet? link_to(namespace_project_snippet_path(event.project.namespace, event.project, - event.note_target), title: h(event.project.name)) do + event.note_target), title: event.project.name) do "#{event.note_target_type} #{truncate event.note_target.to_reference}" end else From b2fc0a51a61669b46516c2a21cbd0abd893ff124 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 12 May 2016 15:31:53 -0500 Subject: [PATCH 183/199] Rename `Event#note_commit?` to `commit_note?` --- app/helpers/events_helper.rb | 10 +++++----- app/models/event.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index d0a63d5880..d7ddb31dbb 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -57,7 +57,7 @@ module EventsHelper words << event.ref_name words << "at" elsif event.commented? - if event.note_commit? + if event.commit_note? words << event.note_short_commit_id else words << "##{truncate event.note_target_iid}" @@ -84,12 +84,12 @@ module EventsHelper elsif event.merge_request? namespace_project_merge_request_url(event.project.namespace, event.project, event.merge_request) - elsif event.note? && event.note_commit? + elsif event.note? && event.commit_note? namespace_project_commit_url(event.project.namespace, event.project, event.note_target) elsif event.note? if event.note_target - if event.note_commit? + if event.commit_note? namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, anchor: dom_id(event.target)) @@ -134,7 +134,7 @@ module EventsHelper end def event_note_target_path(event) - if event.note? && event.note_commit? + if event.note? && event.commit_note? namespace_project_commit_path(event.project.namespace, event.project, event.note_target) else @@ -146,7 +146,7 @@ module EventsHelper def event_note_title_html(event) if event.note_target - if event.note_commit? + if event.commit_note? link_to( namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, diff --git a/app/models/event.rb b/app/models/event.rb index a617694b7d..5629f1ee44 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -278,7 +278,7 @@ class Event < ActiveRecord::Base Commit.truncate_sha(note_commit_id) end - def note_commit? + def commit_note? target.for_commit? end @@ -295,7 +295,7 @@ class Event < ActiveRecord::Base end def note_target_id - if note_commit? + if commit_note? target.commit_id else target.noteable_id.to_s From 0ec2ee71b31ea604a677e28ffb84f3ded1701395 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 12 May 2016 15:37:00 -0500 Subject: [PATCH 184/199] Rename `Event#note_project_snippet?` to `project_snippet_note?` --- app/helpers/events_helper.rb | 4 ++-- app/models/event.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index d7ddb31dbb..19281e3f3a 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -93,7 +93,7 @@ module EventsHelper namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, anchor: dom_id(event.target)) - elsif event.note_project_snippet? + elsif event.project_snippet_note? namespace_project_snippet_path(event.project.namespace, event.project, event.note_target) else @@ -156,7 +156,7 @@ module EventsHelper ) do "#{event.note_target_type} #{event.note_short_commit_id}" end - elsif event.note_project_snippet? + elsif event.project_snippet_note? link_to(namespace_project_snippet_path(event.project.namespace, event.project, event.note_target), title: event.project.name) do diff --git a/app/models/event.rb b/app/models/event.rb index 5629f1ee44..2e9da42dea 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -286,7 +286,7 @@ class Event < ActiveRecord::Base note? && target && target.for_issue? end - def note_project_snippet? + def project_snippet_note? target.for_snippet? end From 020e616822f0e634932bf948013f94eb7ef413bb Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 12 May 2016 16:47:45 -0500 Subject: [PATCH 185/199] Reduce duplication and branching logic in EventsHelper Remove unused methods from Event model --- app/helpers/events_helper.rb | 56 ++++++++++-------------------------- app/models/event.rb | 21 ++++++-------- app/models/project.rb | 2 ++ 3 files changed, 25 insertions(+), 54 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 19281e3f3a..e148938170 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -57,11 +57,7 @@ module EventsHelper words << event.ref_name words << "at" elsif event.commented? - if event.commit_note? - words << event.note_short_commit_id - else - words << "##{truncate event.note_target_iid}" - end + words << event.note_target_reference words << "at" elsif event.milestone? words << "##{event.target_iid}" if event.target_iid @@ -89,16 +85,7 @@ module EventsHelper event.note_target) elsif event.note? if event.note_target - if event.commit_note? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target)) - elsif event.project_snippet_note? - namespace_project_snippet_path(event.project.namespace, - event.project, event.note_target) - else - event_note_target_path(event) - end + event_note_target_path(event) end elsif event.push? push_event_feed_url(event) @@ -135,42 +122,29 @@ module EventsHelper def event_note_target_path(event) if event.note? && event.commit_note? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_target) + namespace_project_commit_path(event.project.namespace, + event.project, + event.note_target, + anchor: dom_id(event.target)) + elsif event.project_snippet_note? + namespace_project_snippet_path(event.project.namespace, + event.project, + event.note_target, + anchor: dom_id(event.target)) else polymorphic_path([event.project.namespace.becomes(Namespace), event.project, event.note_target], - anchor: dom_id(event.target)) + anchor: dom_id(event.target)) end end def event_note_title_html(event) if event.note_target - if event.commit_note? - link_to( - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target)), - title: event.target_title, - class: "commit_short_id" - ) do - "#{event.note_target_type} #{event.note_short_commit_id}" - end - elsif event.project_snippet_note? - link_to(namespace_project_snippet_path(event.project.namespace, - event.project, - event.note_target), title: event.project.name) do - "#{event.note_target_type} #{truncate event.note_target.to_reference}" - end - else - link_to event_note_target_path(event), title: event.target_title do - "#{event.note_target_type} #{truncate event.note_target.to_reference}" - end + link_to(event_note_target_path(event), title: event.target_title, class: 'has-tooltip') do + "#{event.note_target_type} #{event.note_target_reference}" end else - content_tag :strong do - "(deleted)" - end + content_tag(:strong, '(deleted)') end end diff --git a/app/models/event.rb b/app/models/event.rb index 2e9da42dea..716039fb54 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -266,18 +266,10 @@ class Event < ActiveRecord::Base branch? && project.default_branch != branch_name end - def note_commit_id - target.commit_id - end - def target_iid target.respond_to?(:iid) ? target.iid : target_id end - def note_short_commit_id - Commit.truncate_sha(note_commit_id) - end - def commit_note? target.for_commit? end @@ -302,12 +294,15 @@ class Event < ActiveRecord::Base end end - def note_target_iid - if note_target.respond_to?(:iid) - note_target.iid + def note_target_reference + return unless note_target + + # Commit#to_reference returns the full SHA, but we want the short one here + if commit_note? + note_target.short_id else - note_target_id - end.to_s + note_target.to_reference + end end def note_target_type diff --git a/app/models/project.rb b/app/models/project.rb index 6e85841db4..a3c4f1d8e9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -50,6 +50,8 @@ class Project < ActiveRecord::Base attr_accessor :new_default_branch attr_accessor :old_path_with_namespace + alias_attribute :title, :name + # Relations belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' From ecce94d3aab57e099a8897a860023b8ec7acac05 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 16 May 2016 12:42:34 -0300 Subject: [PATCH 186/199] fix default send confirmation value --- CHANGELOG | 3 +-- app/models/application_setting.rb | 3 ++- ...dd_send_user_confirmation_email_to_application_settings.rb} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename db/migrate/{20160421141709_add_send_confirmation_email_to_application_settings.rb => 20160516174813_add_send_user_confirmation_email_to_application_settings.rb} (81%) diff --git a/CHANGELOG b/CHANGELOG index 7ebe10c0f7..eb5fee8b07 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 8.8.0 (unreleased) - Fix error when using link to uploads in global snippets - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) - Use a case-insensitive comparison in sanitizing URI schemes + - Toggle sign-up confirmation emails in application settings - Project#open_branches has been cleaned up and no longer loads entire records into memory. - Escape HTML in commit titles in system note messages - Improve multiple branch push performance by memoizing permission checking @@ -28,8 +29,6 @@ v 8.8.0 (unreleased) - Display informative message when new milestone is created - Sanitize milestones and labels titles - Support multi-line tag messages. !3833 (Calin Seciu) - - Toggle sign-up confirmation emails in application settings - - Replace Devise Async with Devise ActiveJob integration. !3902 (Connor Shea) - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view - Backport GitHub Enterprise import support from EE diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 1a10768655..f5079f9244 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -120,7 +120,8 @@ class ApplicationSetting < ActiveRecord::Base recaptcha_enabled: false, akismet_enabled: false, repository_checks_enabled: true, - disabled_oauth_sign_in_sources: [] + disabled_oauth_sign_in_sources: [], + send_user_confirmation_email: false ) end diff --git a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb similarity index 81% rename from db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb rename to db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb index f92ef96050..c34e7ba540 100644 --- a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb +++ b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb @@ -1,4 +1,4 @@ -class AddSendConfirmationEmailToApplicationSettings < ActiveRecord::Migration +class AddSendUserConfirmationEmailToApplicationSettings < ActiveRecord::Migration def up add_column :application_settings, :send_user_confirmation_email, :boolean, default: false From d71edf0dedb944be62390c0e6eea5f6e6da7a328 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 12 May 2016 17:32:09 -0600 Subject: [PATCH 187/199] Disallow search engines from indexing uploads from a GitLab project. This can sometimes include sensitive information from private projects and confidential issues. It shouldn't be indexed. Resolves #15551. --- public/robots.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/public/robots.txt b/public/robots.txt index 4f616c7f4c..334f4c0353 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -65,3 +65,4 @@ Disallow: /*/*/deploy_keys Disallow: /*/*/hooks Disallow: /*/*/services Disallow: /*/*/protected_branches +Disallow: /*/*/uploads/ From b933367ab3d8df50f0ebd0690d306536f7a654db Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 17 May 2016 00:10:32 +0200 Subject: [PATCH 188/199] Instrument all Grape API helpers --- CHANGELOG | 1 + config/initializers/metrics.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index e1252d4b94..20e2fcb806 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -57,6 +57,7 @@ v 8.8.0 (unreleased) - Redesign navigation for profile and group pages - Add counter metrics for rails cache - Import pull requests from GitHub where the source or target branches were removed + - All Grape API helpers are now instrumented v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index b2d08d87ba..2338916e9d 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -118,6 +118,8 @@ if Gitlab::Metrics.enabled? # Instrument the classes used for checking if somebody has push access. config.instrument_instance_methods(Gitlab::GitAccess) config.instrument_instance_methods(Gitlab::GitAccessWiki) + + config.instrument_instance_methods(API::Helpers) end GC::Profiler.enable From 256e0e8898d0b4a25e7674d2aaa6df06a1a5fe1f Mon Sep 17 00:00:00 2001 From: Carlos Feliciano Date: Mon, 16 May 2016 23:36:42 -0400 Subject: [PATCH 189/199] Remove bottom margin for flash message on home page --- app/assets/stylesheets/pages/projects.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c20f04653f..a3690e40e2 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -7,7 +7,7 @@ } .no-ssh-key-message, .project-limit-message { background-color: #f28d35; - margin-bottom: 16px; + margin-bottom: 0; } .new_project, .edit_project { From 4e9ce5abe2c18089f02ab9f3a3e12c4ccae16af6 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 17 May 2016 15:19:59 +0200 Subject: [PATCH 190/199] fix typo from bad refactoring --- lib/gitlab/gitlab_import/importer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index 96717b42ba..3e51c06877 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -5,7 +5,7 @@ module Gitlab def initialize(project) @project = project - credentials = import_data + credentials = project.import_data if credentials && credentials[:password] @client = Client.new(credentials[:password]) @formatter = Gitlab::ImportFormatter.new From ab2a1d23459294d3ce61e8558538f38cb5f29bb9 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 17 May 2016 15:29:53 +0200 Subject: [PATCH 191/199] added CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 20e2fcb806..bafdc8dbc8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ v 8.8.0 (unreleased) v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) + - Fix import from gitlab.com to a private instance failure v 8.7.5 - Fix relative links in wiki pages. !4050 From 5bf49bb63d88b1cce2d9a44716b54acfa63ea657 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 17 May 2016 21:41:53 -0500 Subject: [PATCH 192/199] Move note helper method to notes entity file --- lib/api/helpers.rb | 4 ---- lib/api/notes.rb | 10 ++++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 1003b596ae..40c967453f 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -397,9 +397,5 @@ module API error!(errors[:access_level], 422) if errors[:access_level].any? not_found!(errors) end - - def noteable_ability_name(noteable) - "read_#{noteable.class.to_s.underscore.downcase}".to_sym - end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index c49b107d1d..d4fcfd3d4d 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -21,7 +21,7 @@ module API get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) - if can?(current_user, noteable_ability_name(@noteable), @noteable) + if can?(current_user, noteable_read_ability_name(@noteable), @noteable) # We exclude notes that are cross-references and that cannot be viewed # by the current user. By doing this exclusion at this level and not # at the DB query level (which we cannot in that case), the current @@ -51,7 +51,7 @@ module API get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) @note = @noteable.notes.find(params[:note_id]) - can_read_note = can?(current_user, noteable_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user) + can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user) if can_read_note present @note, with: Entities::Note @@ -141,5 +141,11 @@ module API end end end + + helpers do + def noteable_read_ability_name(noteable) + "read_#{noteable.class.to_s.underscore.downcase}".to_sym + end + end end end From f7bdbc0b55dfed9f1bfabf052436bac2997e78f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 00:07:51 -0500 Subject: [PATCH 193/199] Make it clearer that /licenses is the OS license templates endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- doc/api/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/README.md b/doc/api/README.md index ff039f1886..27c5962dec 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -33,7 +33,7 @@ following locations: - [Build triggers](build_triggers.md) - [Build Variables](build_variables.md) - [Runners](runners.md) -- [Licenses](licenses.md) +- [Open source license templates](licenses.md) ## Authentication From cf3b149e6506c516d6f691f6e36c599f0beb0590 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 18 May 2016 10:14:53 +0100 Subject: [PATCH 194/199] Point group's issues atom feed to group This was using the user dashboard atom builder template, not one for the group's issues. --- app/views/groups/issues.atom.builder | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 486d1d8587..a6eb9abada 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@user.name} issues" - xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml" - xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" - xml.id issues_dashboard_url + xml.title "#{@group.name} issues" + xml.link href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: issues_group_url, rel: "alternate", type: "text/html" + xml.id issues_group_url xml.updated @issues.first.created_at.xmlschema if @issues.any? @issues.each do |issue| From 3eacb19781d7812b8164b8ef65d290b09682ba57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 10:38:01 -0500 Subject: [PATCH 195/199] Fix 8.7.6 CHANGELOG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable --- CHANGELOG | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b4d960c584..2dd8077954 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -62,10 +62,8 @@ v 8.8.0 (unreleased) v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) - - Fix import from gitlab.com to a private instance failure - -v 8.7.6 - - Fix external imports not finding the import data + - Fix import from GitLab.com to a private instance failure. !4181 + - Fix external imports not finding the import data. !4106 v 8.7.5 - Fix relative links in wiki pages. !4050 From 80817644a74e6ead62c2c66d4bd2826ecf634393 Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Wed, 16 Sep 2015 13:19:46 +0200 Subject: [PATCH 196/199] Improve issue formatting in Slack service Signed-off-by: Jeroen van Baarsen --- CHANGELOG | 3 +++ .../slack_service/issue_message.rb | 19 ++++++++++++++++--- .../slack_service/issue_message_spec.rb | 11 ++++++----- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2dd8077954..d30c0e2a3f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -894,6 +894,9 @@ v 8.1.3 - Add Facebook authentication v 8.1.2 + - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) + +v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) - Add migration to remove satellites directory - Fix specific runners visibility diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index 438ff33fdf..88e053ec19 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -34,7 +34,12 @@ class SlackService private def message - "#{user_name} #{state} #{issue_link} in #{project_link}: *#{title}*" + case state + when "opened" + "[#{project_link}] Issue #{state} by #{user_name}" + else + "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" + end end def opened_issue? @@ -42,7 +47,11 @@ class SlackService end def description_message - [{ text: format(description), color: attachment_color }] + [{ + title: issue_title, + title_link: issue_url, + text: format(description), + color: "#C95823" }] end def project_link @@ -50,7 +59,11 @@ class SlackService end def issue_link - "[issue ##{issue_iid}](#{issue_url})" + "[#{issue_title}](#{issue_url})" + end + + def issue_title + "##{issue_iid} #{title}" end end end diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb index f648cbe2de..0f8889bdf3 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -25,7 +25,7 @@ describe SlackService::IssueMessage, models: true do } end - let(:color) { '#345' } + let(:color) { '#C95823' } context '#initialize' do before do @@ -40,10 +40,11 @@ describe SlackService::IssueMessage, models: true do context 'open' do it 'returns a message regarding opening of issues' do expect(subject.pretext).to eq( - 'Test User opened in : '\ - '*Issue title*') + '] Issue opened by Test User') expect(subject.attachments).to eq([ { + title: "#100 Issue title", + title_link: "url", text: "issue description", color: color, } @@ -56,10 +57,10 @@ describe SlackService::IssueMessage, models: true do args[:object_attributes][:action] = 'close' args[:object_attributes][:state] = 'closed' end + it 'returns a message regarding closing of issues' do expect(subject.pretext). to eq( - 'Test User closed in : '\ - '*Issue title*') + '] Issue closed by Test User') expect(subject.attachments).to be_empty end end From afa3e1ded1ba6b2cb3837cf3da29ff191a18e2bd Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Tue, 10 May 2016 22:08:06 +0200 Subject: [PATCH 197/199] Add changelog entry --- CHANGELOG | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d30c0e2a3f..f060643c06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -70,6 +70,7 @@ v 8.7.5 - Fix always showing build notification message when switching between merge requests !4086 - Fix an issue when filtering merge requests with more than one label. !3886 - Fix short note for the default scope on build page (Takuya Noguchi) + - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) @@ -893,9 +894,6 @@ v 8.1.3 - Use issue editor as cross reference comment author when issue is edited with a new mention - Add Facebook authentication -v 8.1.2 - - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) - v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) - Add migration to remove satellites directory From 78e2d0ac3c6d4e4477625840063e0d4a54f36594 Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Wed, 18 May 2016 11:12:21 -0500 Subject: [PATCH 198/199] Move to the latest version in the CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f060643c06..9baab4734c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,6 +59,7 @@ v 8.8.0 (unreleased) - Add counter metrics for rails cache - Import pull requests from GitHub where the source or target branches were removed - All Grape API helpers are now instrumented + - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) @@ -70,7 +71,6 @@ v 8.7.5 - Fix always showing build notification message when switching between merge requests !4086 - Fix an issue when filtering merge requests with more than one label. !3886 - Fix short note for the default scope on build page (Takuya Noguchi) - - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) From 66c421c0a788ab88f871103a26dceefc67585eec Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 18 May 2016 10:51:11 +0100 Subject: [PATCH 199/199] Link to group feed from group activity page --- CHANGELOG | 1 + app/views/groups/_activities.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 48e3889960..61c6f80af2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -39,6 +39,7 @@ v 8.8.0 (unreleased) - Create tags using Rugged for performance reasons. !3745 - API: Expose Issue#user_notes_count. !3126 (Anton Popov) - Don't show forks button when user can't view forks + - Fix atom feed links and rendering - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) - Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724 diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index dc76599b77..71cc4d87b1 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -4,7 +4,7 @@ .nav-block - if current_user .controls - = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do + = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do %i.fa.fa-rss = render 'shared/event_filter'