From 19a5e7c95e91baca58836ad3ae189190c9ba4ca2 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 23 Mar 2016 14:04:09 +0100 Subject: [PATCH 001/507] Test Grack::Auth via a request spec --- .../git_http_spec.rb} | 167 ++++++++---------- 1 file changed, 74 insertions(+), 93 deletions(-) rename spec/{lib/gitlab/backend/grack_auth_spec.rb => requests/git_http_spec.rb} (57%) diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/requests/git_http_spec.rb similarity index 57% rename from spec/lib/gitlab/backend/grack_auth_spec.rb rename to spec/requests/git_http_spec.rb index cd26dca099..7e274b4209 100644 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -1,83 +1,60 @@ require "spec_helper" -describe Grack::Auth, lib: true do +describe 'Git HTTP requests', lib: true do let(:user) { create(:user) } let(:project) { create(:project) } - let(:app) { lambda { |env| [200, {}, "Success!"] } } - let!(:auth) { Grack::Auth.new(app) } - let(:env) do - { - 'rack.input' => '', - 'REQUEST_METHOD' => 'GET', - 'QUERY_STRING' => 'service=git-upload-pack' - } - end - let(:status) { auth.call(env).first } - describe "#call" do context "when the project doesn't exist" do - before do - env["PATH_INFO"] = "doesnt/exist.git" - end - context "when no authentication is provided" do it "responds with status 401" do - expect(status).to eq(401) + clone_get '/doesnt/exist.git/info/refs' + + expect(response.status).to eq(401) end end context "when username and password are provided" do context "when authentication fails" do - before do - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope") - end - it "responds with status 401" do - expect(status).to eq(401) + clone_get '/doesnt/exist.git/info/refs', user: user.username, password: "nope" + + expect(response.status).to eq(401) end end context "when authentication succeeds" do - before do - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) - end - it "responds with status 404" do - expect(status).to eq(404) + clone_get '/doesnt/exist.git/info/refs', user: user.username, password: user.password + + expect(response.status).to eq(404) end end end end context "when the Wiki for a project exists" do - before do - @wiki = ProjectWiki.new(project) - env["PATH_INFO"] = "#{@wiki.repository.path_with_namespace}.git/info/refs" - project.update_attribute(:visibility_level, Project::PUBLIC) - end - it "responds with the right project" do - response = auth.call(env) - json_body = ActiveSupport::JSON.decode(response[2][0]) + wiki = ProjectWiki.new(project) + project.update_attribute(:visibility_level, Project::PUBLIC) - expect(response.first).to eq(200) - expect(json_body['RepoPath']).to include(@wiki.repository.path_with_namespace) + clone_get "/#{wiki.repository.path_with_namespace}.git/info/refs" + json_body = ActiveSupport::JSON.decode(response.body) + + expect(response.status).to eq(200) + expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) end end context "when the project exists" do - before do - env["PATH_INFO"] = project.path_with_namespace + ".git" - end + let(:path) { clone_path(project) } context "when the project is public" do - before do - project.update_attribute(:visibility_level, Project::PUBLIC) - end - it "responds with status 200" do - expect(status).to eq(200) + project.update_attribute(:visibility_level, Project::PUBLIC) + clone_get path + + expect(response.status).to eq(200) end end @@ -88,85 +65,74 @@ describe Grack::Auth, lib: true do context "when no authentication is provided" do it "responds with status 401" do - expect(status).to eq(401) + clone_get path + + expect(response.status).to eq(401) end end context "when username and password are provided" do context "when authentication fails" do - before do - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope") - end - it "responds with status 401" do - expect(status).to eq(401) + clone_get path, user: user.username, password: 'nope' + + expect(response.status).to eq(401) end context "when the user is IP banned" do - before do + it "responds with status 401" do expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') - end - it "responds with status 401" do - expect(status).to eq(401) + clone_get path, user: user.username, password: 'nope' + + expect(response.status).to eq(401) end end end context "when authentication succeeds" do - before do - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) - end - context "when the user has access to the project" do before do project.team << [user, :master] end context "when the user is blocked" do - before do + it "responds with status 404" do user.block project.team << [user, :master] - end - it "responds with status 404" do - expect(status).to eq(404) + clone_get path, user: user.username, password: user.password + + expect(response.status).to eq(404) end end context "when the user isn't blocked" do - before do - expect(Rack::Attack::Allow2Ban).to receive(:reset) - end - it "responds with status 200" do - expect(status).to eq(200) + expect(Rack::Attack::Allow2Ban).to receive(:reset) + + clone_get path, user: user.username, password: user.password + + expect(response.status).to eq(200) end end context "when blank password attempts follow a valid login" do - let(:options) { Gitlab.config.rack_attack.git_basic_auth } - let(:maxretry) { options[:maxretry] - 1 } - let(:ip) { '1.2.3.4' } - - before do - allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) - Rack::Attack::Allow2Ban.reset(ip, options) - end - - after do - Rack::Attack::Allow2Ban.reset(ip, options) - end - def attempt_login(include_password) password = include_password ? user.password : "" - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password) - Grack::Auth.new(app) - auth.call(env).first + clone_get path, user: user.username, password: password + response.status end it "repeated attempts followed by successful attempt" do + options = Gitlab.config.rack_attack.git_basic_auth + maxretry = options[:maxretry] - 1 + ip = '1.2.3.4' + + allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) + Rack::Attack::Allow2Ban.reset(ip, options) + maxretry.times.each do expect(attempt_login(false)).to eq(401) end @@ -177,33 +143,48 @@ describe Grack::Auth, lib: true do maxretry.times.each do expect(attempt_login(false)).to eq(401) end + + Rack::Attack::Allow2Ban.reset(ip, options) end end end context "when the user doesn't have access to the project" do it "responds with status 404" do - expect(status).to eq(404) + clone_get path, user: user.username, password: user.password + + expect(response.status).to eq(404) end end end end context "when a gitlab ci token is provided" do - let(:token) { "123" } - let(:project) { FactoryGirl.create :empty_project } - - before do + it "responds with status 200" do + token = "123" + project = FactoryGirl.create :empty_project project.update_attributes(runners_token: token, builds_enabled: true) - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token) - end + clone_get clone_path(project), user: 'gitlab-ci-token', password: token - it "responds with status 200" do - expect(status).to eq(200) + expect(response.status).to eq(200) end end end end end + + def clone_get(url, user: nil, password: nil) + if user && password + env = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } + else + env = {} + end + + get url, { 'service' => 'git-upload-pack' }, env + end + + def clone_path(project) + "/#{project.path_with_namespace}.git/info/refs" + end end From 55f5a68f092cc64ae4782c0d7fbbf1d3d1ce6284 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 23 Mar 2016 18:34:16 +0100 Subject: [PATCH 002/507] Get Grack::Auth tests to pass --- .../projects/application_controller.rb | 22 ++- .../projects/git_http_controller.rb | 167 ++++++++++++++++++ config/routes.rb | 10 +- 3 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 app/controllers/projects/git_http_controller.rb diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 657ee94cfd..5f5dc1adad 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -10,9 +10,6 @@ class Projects::ApplicationController < ApplicationController def project unless @project - namespace = params[:namespace_id] - id = params[:project_id] || params[:id] - # Redirect from # localhost/group/project.git # to @@ -23,8 +20,7 @@ class Projects::ApplicationController < ApplicationController return end - project_path = "#{namespace}/#{id}" - @project = Project.find_with_namespace(project_path) + @project = find_project if @project && can?(current_user, :read_project, @project) if @project.path_with_namespace != project_path @@ -44,6 +40,22 @@ class Projects::ApplicationController < ApplicationController @project end + def id + params[:project_id] || params[:id] + end + + def namespace + params[:namespace_id] + end + + def project_path + "#{namespace}/#{id}" + end + + def find_project + Project.find_with_namespace(project_path) + end + def repository @repository ||= project.repository end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb new file mode 100644 index 0000000000..129e87dbf1 --- /dev/null +++ b/app/controllers/projects/git_http_controller.rb @@ -0,0 +1,167 @@ +class Projects::GitHttpController < Projects::ApplicationController + skip_before_action :repository + before_action :authenticate_user + before_action :project_found? + + def git_rpc + if upload_pack? && upload_pack_allowed? + render_ok and return + end + + render_not_found + end + + %i{info_refs git_receive_pack git_upload_pack}.each do |method| + alias_method method, :git_rpc + end + + private + + def authenticate_user + return if project && project.public? && upload_pack? + + authenticate_or_request_with_http_basic do |login, password| + return @ci = true if ci_request?(login, password) + + @user = Gitlab::Auth.new.find(login, password) + @user ||= oauth_access_token_check(login, password) + rate_limit_ip!(login, @user) + end + end + + def project_found? + render_not_found if project.nil? + end + + def ci_request?(login, password) + matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) + + if project && matched_login.present? && upload_pack? + underscored_service = matched_login['s'].underscore + + if underscored_service == 'gitlab_ci' + return project && project.valid_build_token?(password) + elsif Service.available_services_names.include?(underscored_service) + service_method = "#{underscored_service}_service" + service = project.send(service_method) + + return service && service.activated? && service.valid_token?(password) + end + end + + false + end + + def oauth_access_token_check(login, password) + if login == "oauth2" && upload_pack? && password.present? + token = Doorkeeper::AccessToken.by_token(password) + token && token.accessible? && User.find_by(id: token.resource_owner_id) + end + end + + def rate_limit_ip!(login, user) + # 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 + return user unless 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 + + user + end + + def project + return @project if defined?(@project) + @project = find_project + end + + def id + id = params[:project_id] + return if id.nil? + + if id.end_with?('.wiki.git') + id.slice(0, id.length - 9) + elsif id.end_with?('.git') + id.slice(0, id.length - 4) + end + end + + def repo_path + @repo_path ||= begin + if params[:project_id].end_with?('.wiki.git') + project.wiki.wiki.path + else + repository.path_to_repo + end + end + end + + def upload_pack? + if action_name == 'info_refs' + params[:service] == 'git-upload-pack' + else + action_name == 'git_upload_pack' + end + end + + def render_ok + render json: { + 'GL_ID' => Gitlab::ShellEnv.gl_id(@user), + 'RepoPath' => repo_path, + } + end + + def render_not_found + render text: 'Not Found', status: :not_found + end + + def ci? + !!@ci + end + + def user + @user + end + + def upload_pack_allowed? + if !Gitlab.config.gitlab_shell.upload_pack + false + elsif ci? + true + elsif user + Gitlab::GitAccess.new(user, project).download_access_check.allowed? + elsif project.public? + # Allow clone/fetch for public projects + true + else + false + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 4a3c23b7c1..47ab1a89b8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -59,9 +59,6 @@ Rails.application.routes.draw do mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq end - # Enable Grack support - mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] - # Help get 'help' => 'help#index' get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ } @@ -426,6 +423,13 @@ Rails.application.routes.draw do end scope module: :projects do + # Git HTTP clients ('git clone' etc.) + scope constraints: { format: /(git|wiki\.git)/ } do + get '/info/refs', to: 'git_http#info_refs', only: :get + get '/git-upload-pack', to: 'git_http#git_upload_pack', only: :post + get '/git-receive-pack', to: 'git_http#git_receive_pack', only: :post + end + # Blob routes: get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' From 8f3e86d72c16294c8bcec8c9a3af86ec99d66ee8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 24 Mar 2016 14:53:20 +0100 Subject: [PATCH 003/507] Keep Grack::Auth in the routes for LFS only --- config/routes.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index 47ab1a89b8..021eab89c2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -59,6 +59,9 @@ Rails.application.routes.draw do mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq end + # Enable Grack support (for LFS only) + mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put] + # Help get 'help' => 'help#index' get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ } From 31bc876b7b34fa1785be022e9cffdc601f2192d7 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 24 Mar 2016 16:14:09 +0100 Subject: [PATCH 004/507] Test both GET and POST for git-upload-pack --- config/routes.rb | 4 +- spec/requests/git_http_spec.rb | 112 ++++++++++++++++++++------------- 2 files changed, 69 insertions(+), 47 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 021eab89c2..eace7516e9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -429,8 +429,8 @@ Rails.application.routes.draw do # Git HTTP clients ('git clone' etc.) scope constraints: { format: /(git|wiki\.git)/ } do get '/info/refs', to: 'git_http#info_refs', only: :get - get '/git-upload-pack', to: 'git_http#git_upload_pack', only: :post - get '/git-receive-pack', to: 'git_http#git_receive_pack', only: :post + post '/git-upload-pack', to: 'git_http#git_upload_pack', only: :post + post '/git-receive-pack', to: 'git_http#git_receive_pack', only: :post end # Blob routes: diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 7e274b4209..ef0b83fd47 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -8,26 +8,26 @@ describe 'Git HTTP requests', lib: true do context "when the project doesn't exist" do context "when no authentication is provided" do it "responds with status 401" do - clone_get '/doesnt/exist.git/info/refs' - - expect(response.status).to eq(401) + download('doesnt/exist.git') do |response| + expect(response.status).to eq(401) + end end end context "when username and password are provided" do context "when authentication fails" do it "responds with status 401" do - clone_get '/doesnt/exist.git/info/refs', user: user.username, password: "nope" - - expect(response.status).to eq(401) + download('doesnt/exist.git', user: user.username, password: "nope") do |response| + expect(response.status).to eq(401) + end end end context "when authentication succeeds" do it "responds with status 404" do - clone_get '/doesnt/exist.git/info/refs', user: user.username, password: user.password - - expect(response.status).to eq(404) + download('/doesnt/exist.git', user: user.username, password: user.password) do |response| + expect(response.status).to eq(404) + end end end end @@ -38,23 +38,25 @@ describe 'Git HTTP requests', lib: true do wiki = ProjectWiki.new(project) project.update_attribute(:visibility_level, Project::PUBLIC) - clone_get "/#{wiki.repository.path_with_namespace}.git/info/refs" - json_body = ActiveSupport::JSON.decode(response.body) - - expect(response.status).to eq(200) - expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) + download("/#{wiki.repository.path_with_namespace}.git") do |response| + json_body = ActiveSupport::JSON.decode(response.body) + + expect(response.status).to eq(200) + expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) + end end end context "when the project exists" do - let(:path) { clone_path(project) } + let(:path) { "#{project.path_with_namespace}.git" } + let(:env) { {} } context "when the project is public" do it "responds with status 200" do project.update_attribute(:visibility_level, Project::PUBLIC) - clone_get path - - expect(response.status).to eq(200) + download(path, env) do |response| + expect(response.status).to eq(200) + end end end @@ -65,33 +67,37 @@ describe 'Git HTTP requests', lib: true do context "when no authentication is provided" do it "responds with status 401" do - clone_get path - - expect(response.status).to eq(401) + download(path, env) do |response| + expect(response.status).to eq(401) + end end end context "when username and password are provided" do + let(:env) { { user: user.username, password: 'nope' } } + context "when authentication fails" do it "responds with status 401" do - clone_get path, user: user.username, password: 'nope' - - expect(response.status).to eq(401) + download(path, env) do |response| + expect(response.status).to eq(401) + end end context "when the user is IP banned" do it "responds with status 401" do expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') - - clone_get path, user: user.username, password: 'nope' - + + clone_get(path, env) + expect(response.status).to eq(401) end end end context "when authentication succeeds" do + let(:env) { { user: user.username, password: user.password } } + context "when the user has access to the project" do before do project.team << [user, :master] @@ -102,18 +108,18 @@ describe 'Git HTTP requests', lib: true do user.block project.team << [user, :master] - clone_get path, user: user.username, password: user.password - - expect(response.status).to eq(404) + download(path, env) do |response| + expect(response.status).to eq(404) + end end end context "when the user isn't blocked" do it "responds with status 200" do expect(Rack::Attack::Allow2Ban).to receive(:reset) - - clone_get path, user: user.username, password: user.password - + + clone_get(path, env) + expect(response.status).to eq(200) end end @@ -151,9 +157,9 @@ describe 'Git HTTP requests', lib: true do context "when the user doesn't have access to the project" do it "responds with status 404" do - clone_get path, user: user.username, password: user.password - - expect(response.status).to eq(404) + download(path, user: user.username, password: user.password) do |response| + expect(response.status).to eq(404) + end end end end @@ -165,7 +171,7 @@ describe 'Git HTTP requests', lib: true do project = FactoryGirl.create :empty_project project.update_attributes(runners_token: token, builds_enabled: true) - clone_get clone_path(project), user: 'gitlab-ci-token', password: token + clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token expect(response.status).to eq(200) end @@ -174,17 +180,33 @@ describe 'Git HTTP requests', lib: true do end end - def clone_get(url, user: nil, password: nil) - if user && password - env = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } - else - env = {} - end - - get url, { 'service' => 'git-upload-pack' }, env + def clone_get(project, options={}) + get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password)) + end + + def clone_post(project, options={}) + post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password)) end def clone_path(project) "/#{project.path_with_namespace}.git/info/refs" end + + def download(project, user: nil, password: nil) + args = [project, {user: user, password: password}] + + clone_get *args + yield response + + clone_post *args + yield response + end + + def auth_env(user, password) + if user && password + { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } + else + {} + end + end end From 0f8fe93c26f00eac14cbc33e9ed2e2260b7014cc Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 24 Mar 2016 16:21:19 +0100 Subject: [PATCH 005/507] Whitespace, remove unused method --- spec/requests/git_http_spec.rb | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index ef0b83fd47..1e3f3f3e61 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -40,7 +40,7 @@ describe 'Git HTTP requests', lib: true do download("/#{wiki.repository.path_with_namespace}.git") do |response| json_body = ActiveSupport::JSON.decode(response.body) - + expect(response.status).to eq(200) expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) end @@ -75,7 +75,7 @@ describe 'Git HTTP requests', lib: true do context "when username and password are provided" do let(:env) { { user: user.username, password: 'nope' } } - + context "when authentication fails" do it "responds with status 401" do download(path, env) do |response| @@ -87,9 +87,9 @@ describe 'Git HTTP requests', lib: true do it "responds with status 401" do expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') - + clone_get(path, env) - + expect(response.status).to eq(401) end end @@ -97,7 +97,7 @@ describe 'Git HTTP requests', lib: true do context "when authentication succeeds" do let(:env) { { user: user.username, password: user.password } } - + context "when the user has access to the project" do before do project.team << [user, :master] @@ -117,9 +117,9 @@ describe 'Git HTTP requests', lib: true do context "when the user isn't blocked" do it "responds with status 200" do expect(Rack::Attack::Allow2Ban).to receive(:reset) - + clone_get(path, env) - + expect(response.status).to eq(200) end end @@ -183,25 +183,21 @@ describe 'Git HTTP requests', lib: true do def clone_get(project, options={}) get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password)) end - + def clone_post(project, options={}) post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password)) end - def clone_path(project) - "/#{project.path_with_namespace}.git/info/refs" - end - def download(project, user: nil, password: nil) args = [project, {user: user, password: password}] clone_get *args yield response - + clone_post *args yield response end - + def auth_env(user, password) if user && password { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } From 1433068ad951045a3440d58b86e9489001ff3774 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 24 Mar 2016 16:28:18 +0100 Subject: [PATCH 006/507] Remove useles only: --- config/routes.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index eace7516e9..40c149abda 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -428,9 +428,9 @@ Rails.application.routes.draw do scope module: :projects do # Git HTTP clients ('git clone' etc.) scope constraints: { format: /(git|wiki\.git)/ } do - get '/info/refs', to: 'git_http#info_refs', only: :get - post '/git-upload-pack', to: 'git_http#git_upload_pack', only: :post - post '/git-receive-pack', to: 'git_http#git_receive_pack', only: :post + get '/info/refs', to: 'git_http#info_refs' + post '/git-upload-pack', to: 'git_http#git_upload_pack' + post '/git-receive-pack', to: 'git_http#git_receive_pack' end # Blob routes: From aae577f92141f3ec973b4dd362452502274147f5 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 24 Mar 2016 17:34:56 +0100 Subject: [PATCH 007/507] Add test for gitlab_shell.upload_pack config setting --- spec/requests/git_http_spec.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 1e3f3f3e61..3a6a9b7a70 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -52,12 +52,25 @@ describe 'Git HTTP requests', lib: true do let(:env) { {} } context "when the project is public" do - it "responds with status 200" do + before do project.update_attribute(:visibility_level, Project::PUBLIC) + end + + it "responds with status 200" do download(path, env) do |response| expect(response.status).to eq(200) end end + + context 'but git-upload-pack is disabled' do + it "responds with status 404" do + allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) + + download(path, env) do |response| + expect(response.status).to eq(404) + end + end + end end context "when the project is private" do From ccf5b21f28d41e10de450e31d6e8855d1ee2f81e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 24 Mar 2016 17:38:30 +0100 Subject: [PATCH 008/507] Remove useless "describe" --- spec/requests/git_http_spec.rb | 351 ++++++++++++++++----------------- 1 file changed, 174 insertions(+), 177 deletions(-) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 3a6a9b7a70..a26b986aeb 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -4,195 +4,192 @@ describe 'Git HTTP requests', lib: true do let(:user) { create(:user) } let(:project) { create(:project) } - describe "#call" do - context "when the project doesn't exist" do - context "when no authentication is provided" do + context "when the project doesn't exist" do + context "when no authentication is provided" do + it "responds with status 401" do + download('doesnt/exist.git') do |response| + expect(response.status).to eq(401) + end + end + end + + context "when username and password are provided" do + context "when authentication fails" do it "responds with status 401" do - download('doesnt/exist.git') do |response| + download('doesnt/exist.git', user: user.username, password: "nope") do |response| expect(response.status).to eq(401) end end end - context "when username and password are provided" do - context "when authentication fails" do - it "responds with status 401" do - download('doesnt/exist.git', user: user.username, password: "nope") do |response| - expect(response.status).to eq(401) - end - end - end - - context "when authentication succeeds" do - it "responds with status 404" do - download('/doesnt/exist.git', user: user.username, password: user.password) do |response| - expect(response.status).to eq(404) - end - end - end - end - end - - context "when the Wiki for a project exists" do - it "responds with the right project" do - wiki = ProjectWiki.new(project) - project.update_attribute(:visibility_level, Project::PUBLIC) - - download("/#{wiki.repository.path_with_namespace}.git") do |response| - json_body = ActiveSupport::JSON.decode(response.body) - - expect(response.status).to eq(200) - expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) - end - end - end - - context "when the project exists" do - let(:path) { "#{project.path_with_namespace}.git" } - let(:env) { {} } - - context "when the project is public" do - before do - project.update_attribute(:visibility_level, Project::PUBLIC) - end - - it "responds with status 200" do - download(path, env) do |response| - expect(response.status).to eq(200) - end - end - - context 'but git-upload-pack is disabled' do - it "responds with status 404" do - allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) - - download(path, env) do |response| - expect(response.status).to eq(404) - end - end - end - end - - context "when the project is private" do - before do - project.update_attribute(:visibility_level, Project::PRIVATE) - end - - context "when no authentication is provided" do - it "responds with status 401" do - download(path, env) do |response| - expect(response.status).to eq(401) - end - end - end - - context "when username and password are provided" do - let(:env) { { user: user.username, password: 'nope' } } - - context "when authentication fails" do - it "responds with status 401" do - download(path, env) do |response| - expect(response.status).to eq(401) - end - end - - context "when the user is IP banned" do - it "responds with status 401" do - expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) - allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') - - clone_get(path, env) - - expect(response.status).to eq(401) - end - end - end - - context "when authentication succeeds" do - let(:env) { { user: user.username, password: user.password } } - - context "when the user has access to the project" do - before do - project.team << [user, :master] - end - - context "when the user is blocked" do - it "responds with status 404" do - user.block - project.team << [user, :master] - - download(path, env) do |response| - expect(response.status).to eq(404) - end - end - end - - context "when the user isn't blocked" do - it "responds with status 200" do - expect(Rack::Attack::Allow2Ban).to receive(:reset) - - clone_get(path, env) - - expect(response.status).to eq(200) - end - end - - context "when blank password attempts follow a valid login" do - def attempt_login(include_password) - password = include_password ? user.password : "" - clone_get path, user: user.username, password: password - response.status - end - - it "repeated attempts followed by successful attempt" do - options = Gitlab.config.rack_attack.git_basic_auth - maxretry = options[:maxretry] - 1 - ip = '1.2.3.4' - - allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) - Rack::Attack::Allow2Ban.reset(ip, options) - - maxretry.times.each do - expect(attempt_login(false)).to eq(401) - end - - expect(attempt_login(true)).to eq(200) - expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey - - maxretry.times.each do - expect(attempt_login(false)).to eq(401) - end - - Rack::Attack::Allow2Ban.reset(ip, options) - end - end - end - - context "when the user doesn't have access to the project" do - it "responds with status 404" do - download(path, user: user.username, password: user.password) do |response| - expect(response.status).to eq(404) - end - end - end - end - end - - context "when a gitlab ci token is provided" do - it "responds with status 200" do - token = "123" - project = FactoryGirl.create :empty_project - project.update_attributes(runners_token: token, builds_enabled: true) - - clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token - - expect(response.status).to eq(200) + context "when authentication succeeds" do + it "responds with status 404" do + download('/doesnt/exist.git', user: user.username, password: user.password) do |response| + expect(response.status).to eq(404) end end end end end + context "when the Wiki for a project exists" do + it "responds with the right project" do + wiki = ProjectWiki.new(project) + project.update_attribute(:visibility_level, Project::PUBLIC) + + download("/#{wiki.repository.path_with_namespace}.git") do |response| + json_body = ActiveSupport::JSON.decode(response.body) + + expect(response.status).to eq(200) + expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) + end + end + end + + context "when the project exists" do + let(:path) { "#{project.path_with_namespace}.git" } + let(:env) { {} } + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + it "responds with status 200" do + download(path, env) do |response| + expect(response.status).to eq(200) + end + end + + context 'but git-upload-pack is disabled' do + it "responds with status 404" do + allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) + + download(path, env) do |response| + expect(response.status).to eq(404) + end + end + end + end + + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context "when no authentication is provided" do + it "responds with status 401" do + download(path, env) do |response| + expect(response.status).to eq(401) + end + end + end + + context "when username and password are provided" do + let(:env) { { user: user.username, password: 'nope' } } + + context "when authentication fails" do + it "responds with status 401" do + download(path, env) do |response| + expect(response.status).to eq(401) + end + end + + context "when the user is IP banned" do + it "responds with status 401" do + expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) + allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') + + clone_get(path, env) + + expect(response.status).to eq(401) + end + end + end + + context "when authentication succeeds" do + let(:env) { { user: user.username, password: user.password } } + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + it "responds with status 404" do + user.block + project.team << [user, :master] + + download(path, env) do |response| + expect(response.status).to eq(404) + end + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + expect(Rack::Attack::Allow2Ban).to receive(:reset) + + clone_get(path, env) + + expect(response.status).to eq(200) + end + end + + context "when blank password attempts follow a valid login" do + def attempt_login(include_password) + password = include_password ? user.password : "" + clone_get path, user: user.username, password: password + response.status + end + + it "repeated attempts followed by successful attempt" do + options = Gitlab.config.rack_attack.git_basic_auth + maxretry = options[:maxretry] - 1 + ip = '1.2.3.4' + + allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) + Rack::Attack::Allow2Ban.reset(ip, options) + + maxretry.times.each do + expect(attempt_login(false)).to eq(401) + end + + expect(attempt_login(true)).to eq(200) + expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey + + maxretry.times.each do + expect(attempt_login(false)).to eq(401) + end + + Rack::Attack::Allow2Ban.reset(ip, options) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + download(path, user: user.username, password: user.password) do |response| + expect(response.status).to eq(404) + end + end + end + end + end + + context "when a gitlab ci token is provided" do + it "responds with status 200" do + token = "123" + project = FactoryGirl.create :empty_project + project.update_attributes(runners_token: token, builds_enabled: true) + + clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token + + expect(response.status).to eq(200) + end + end + end + end def clone_get(project, options={}) get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password)) end From 57145483fc41cc73b7b41005ebac90779f817b5e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 24 Mar 2016 17:44:10 +0100 Subject: [PATCH 009/507] Spec Www-Authenticate --- spec/requests/git_http_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index a26b986aeb..967e0ab6e7 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -4,6 +4,12 @@ describe 'Git HTTP requests', lib: true do let(:user) { create(:user) } let(:project) { create(:project) } + it "gives WWW-Authenticate hints" do + clone_get('doesnt/exist.git') + + expect(response.header['WWW-Authenticate']).to start_with('Basic ') + end + context "when the project doesn't exist" do context "when no authentication is provided" do it "responds with status 401" do From 5f3708418ab71c47c6fffe63b1fac03c0e7c889f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 24 Mar 2016 17:44:13 +0100 Subject: [PATCH 010/507] Whitespace! --- spec/requests/git_http_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 967e0ab6e7..c1aad48ad0 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -60,13 +60,13 @@ describe 'Git HTTP requests', lib: true do before do project.update_attribute(:visibility_level, Project::PUBLIC) end - + it "responds with status 200" do download(path, env) do |response| expect(response.status).to eq(200) end end - + context 'but git-upload-pack is disabled' do it "responds with status 404" do allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) From 5fe06d7365f5552904add8027309d6216954793e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 24 Mar 2016 18:58:29 +0100 Subject: [PATCH 011/507] Add some upload specs --- .../projects/git_http_controller.rb | 40 +++++++++--- spec/requests/git_http_spec.rb | 63 ++++++++++++++++++- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 129e87dbf1..a26ab73611 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -5,10 +5,12 @@ class Projects::GitHttpController < Projects::ApplicationController def git_rpc if upload_pack? && upload_pack_allowed? - render_ok and return + render_ok + elsif receive_pack? && receive_pack_allowed? + render_ok + else + render_not_found end - - render_not_found end %i{info_refs git_receive_pack git_upload_pack}.each do |method| @@ -30,7 +32,7 @@ class Projects::GitHttpController < Projects::ApplicationController end def project_found? - render_not_found if project.nil? + render_not_found if project.blank? end def ci_request?(login, password) @@ -124,13 +126,21 @@ class Projects::GitHttpController < Projects::ApplicationController end def upload_pack? - if action_name == 'info_refs' - params[:service] == 'git-upload-pack' - else - action_name == 'git_upload_pack' - end + rpc == 'git-upload-pack' end + def receive_pack? + rpc == 'git-receive-pack' + end + + def rpc + if action_name == 'info_refs' + params[:service] + else + action_name.gsub('_', '-') + end + end + def render_ok render json: { 'GL_ID' => Gitlab::ShellEnv.gl_id(@user), @@ -164,4 +174,16 @@ class Projects::GitHttpController < Projects::ApplicationController false end end + + def receive_pack_allowed? + if !Gitlab.config.gitlab_shell.receive_pack + false + elsif user + # Skip user authorization on upload request. + # It will be done by the pre-receive hook in the repository. + true + else + false + end + end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index c1aad48ad0..1fa14cadc0 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -61,12 +61,38 @@ describe 'Git HTTP requests', lib: true do project.update_attribute(:visibility_level, Project::PUBLIC) end - it "responds with status 200" do + it "downloads get status 200" do download(path, env) do |response| expect(response.status).to eq(200) end end + it "uploads get status 401" do + upload(path, env) do |response| + expect(response.status).to eq(401) + end + end + + context "with correct credentials" do + let(:env) { { user: user.username, password: user.password } } + + it "uploads get status 200 (because Git hooks do the real check)" do + upload(path, env) do |response| + expect(response.status).to eq(200) + end + end + + context 'but git-receive-pack is disabled' do + it "responds with status 404" do + allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false) + + upload(path, env) do |response| + expect(response.status).to eq(404) + end + end + end + end + context 'but git-upload-pack is disabled' do it "responds with status 404" do allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) @@ -133,13 +159,19 @@ describe 'Git HTTP requests', lib: true do end context "when the user isn't blocked" do - it "responds with status 200" do + it "downloads status 200" do expect(Rack::Attack::Allow2Ban).to receive(:reset) clone_get(path, env) expect(response.status).to eq(200) end + + it "uploads get status 200" do + upload(path, env) do |response| + expect(response.status).to eq(200) + end + end end context "when blank password attempts follow a valid login" do @@ -174,11 +206,17 @@ describe 'Git HTTP requests', lib: true do end context "when the user doesn't have access to the project" do - it "responds with status 404" do + it "downloads get status 404" do download(path, user: user.username, password: user.password) do |response| expect(response.status).to eq(404) end end + + it "uploads get status 200 (because Git hooks do the real check)" do + upload(path, user: user.username, password: user.password) do |response| + expect(response.status).to eq(200) + end + end end end end @@ -196,6 +234,7 @@ describe 'Git HTTP requests', lib: true do end end end + def clone_get(project, options={}) get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password)) end @@ -204,6 +243,14 @@ describe 'Git HTTP requests', lib: true do post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password)) end + def push_get(project, options={}) + get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password)) + end + + def push_post(project, options={}) + post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password)) + end + def download(project, user: nil, password: nil) args = [project, {user: user, password: password}] @@ -214,6 +261,16 @@ describe 'Git HTTP requests', lib: true do yield response end + def upload(project, user: nil, password: nil) + args = [project, {user: user, password: password}] + + push_get *args + yield response + + push_post *args + yield response + end + def auth_env(user, password) if user && password { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } From ac4d3dc5ccba32e026250ab48fe7f29bcf4ddd97 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 6 Apr 2016 17:23:16 +0200 Subject: [PATCH 012/507] Rubocop --- spec/requests/git_http_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 1fa14cadc0..5d41d97308 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -252,7 +252,7 @@ describe 'Git HTTP requests', lib: true do end def download(project, user: nil, password: nil) - args = [project, {user: user, password: password}] + args = [project, { user: user, password: password }] clone_get *args yield response @@ -262,7 +262,7 @@ describe 'Git HTTP requests', lib: true do end def upload(project, user: nil, password: nil) - args = [project, {user: user, password: password}] + args = [project, { user: user, password: password }] push_get *args yield response From 6cc6d9730a234c2cc27869f9b9388ab61de9c460 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 6 Apr 2016 17:27:52 +0200 Subject: [PATCH 013/507] Delete dead code --- lib/gitlab/backend/grack_auth.rb | 53 +------------------------------- 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index cdcaae8094..e2363b9126 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -36,10 +36,7 @@ module Grack lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call return lfs_response unless lfs_response.nil? - if project && authorized_request? - # Tell gitlab-workhorse the request is OK, and what the GL_ID is - render_grack_auth_ok - elsif @user.nil? && !@ci + if @user.nil? && !@ci unauthorized else render_not_found @@ -141,36 +138,6 @@ module Grack user end - def authorized_request? - return true if @ci - - case git_cmd - when *Gitlab::GitAccess::DOWNLOAD_COMMANDS - if !Gitlab.config.gitlab_shell.upload_pack - false - elsif user - Gitlab::GitAccess.new(user, project).download_access_check.allowed? - elsif project.public? - # Allow clone/fetch for public projects - true - else - false - end - when *Gitlab::GitAccess::PUSH_COMMANDS - if !Gitlab.config.gitlab_shell.receive_pack - false - elsif user - # Skip user authorization on upload request. - # It will be done by the pre-receive hook in the repository. - true - else - false - end - else - false - end - end - def git_cmd if @request.get? @request.params['service'] @@ -197,24 +164,6 @@ module Grack end end - def render_grack_auth_ok - repo_path = - if @request.path_info =~ /^([\w\.\/-]+)\.wiki\.git/ - ProjectWiki.new(project).repository.path_to_repo - else - project.repository.path_to_repo - end - - [ - 200, - { "Content-Type" => "application/json" }, - [JSON.dump({ - 'GL_ID' => Gitlab::ShellEnv.gl_id(@user), - 'RepoPath' => repo_path, - })] - ] - end - def render_not_found [404, { "Content-Type" => "text/plain" }, ["Not Found"]] end From 91226c200151461b21e85cc8c85a103c93d6a17f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 6 Apr 2016 17:52:12 +0200 Subject: [PATCH 014/507] Move workhorse protocol code into lib --- app/controllers/projects/git_http_controller.rb | 13 +++++-------- lib/gitlab/workhorse.rb | 7 +++++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index a26ab73611..6dd7a683b0 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -115,12 +115,12 @@ class Projects::GitHttpController < Projects::ApplicationController end end - def repo_path - @repo_path ||= begin + def repository + @repository ||= begin if params[:project_id].end_with?('.wiki.git') - project.wiki.wiki.path + project.wiki.repository else - repository.path_to_repo + project.repository end end end @@ -142,10 +142,7 @@ class Projects::GitHttpController < Projects::ApplicationController end def render_ok - render json: { - 'GL_ID' => Gitlab::ShellEnv.gl_id(@user), - 'RepoPath' => repo_path, - } + render json: Gitlab::Workhorse.git_http_ok(repository, user) end def render_not_found diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index c3ddd4c268..5b2982e499 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -6,6 +6,13 @@ module Gitlab SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data' class << self + def git_http_ok(repository, user) + { + 'GL_ID' => Gitlab::ShellEnv.gl_id(user), + 'RepoPath' => repository.path_to_repo, + } + end + def send_git_blob(repository, blob) params = { 'RepoPath' => repository.path_to_repo, From ccb29955c9d7de69d99fe91425d6246cc723def4 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 6 Apr 2016 18:58:19 +0200 Subject: [PATCH 015/507] More tests, better descriptions --- spec/requests/git_http_spec.rb | 41 +++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 5d41d97308..8b21768491 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -12,7 +12,7 @@ describe 'Git HTTP requests', lib: true do context "when the project doesn't exist" do context "when no authentication is provided" do - it "responds with status 401" do + it "responds with status 401 (no project existence information leak)" do download('doesnt/exist.git') do |response| expect(response.status).to eq(401) end @@ -72,7 +72,7 @@ describe 'Git HTTP requests', lib: true do expect(response.status).to eq(401) end end - + context "with correct credentials" do let(:env) { { user: user.username, password: user.password } } @@ -81,11 +81,11 @@ describe 'Git HTTP requests', lib: true do expect(response.status).to eq(200) end end - + context 'but git-receive-pack is disabled' do it "responds with status 404" do allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false) - + upload(path, env) do |response| expect(response.status).to eq(404) end @@ -110,11 +110,17 @@ describe 'Git HTTP requests', lib: true do end context "when no authentication is provided" do - it "responds with status 401" do + it "responds with status 401 to downloads" do download(path, env) do |response| expect(response.status).to eq(401) end end + + it "responds with status 401 to uploads" do + upload(path, env) do |response| + expect(response.status).to eq(401) + end + end end context "when username and password are provided" do @@ -159,18 +165,18 @@ describe 'Git HTTP requests', lib: true do end context "when the user isn't blocked" do - it "downloads status 200" do + it "downloads get status 200" do expect(Rack::Attack::Allow2Ban).to receive(:reset) clone_get(path, env) expect(response.status).to eq(200) end - + it "uploads get status 200" do upload(path, env) do |response| expect(response.status).to eq(200) - end + end end end @@ -211,7 +217,7 @@ describe 'Git HTTP requests', lib: true do expect(response.status).to eq(404) end end - + it "uploads get status 200 (because Git hooks do the real check)" do upload(path, user: user.username, password: user.password) do |response| expect(response.status).to eq(200) @@ -222,15 +228,24 @@ describe 'Git HTTP requests', lib: true do end context "when a gitlab ci token is provided" do - it "responds with status 200" do - token = "123" - project = FactoryGirl.create :empty_project - project.update_attributes(runners_token: token, builds_enabled: true) + let(:token) { 123 } + let(:project) { FactoryGirl.create :empty_project } + before do + project.update_attributes(runners_token: token, builds_enabled: true) + end + + it "downloads get status 200" do clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token expect(response.status).to eq(200) end + + it "uploads get status 401 (no project existence information leak)" do + push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token + + expect(response.status).to eq(401) + end end end end From ab9dfa8fd681ac558cf988aa2cdb5bd69feea757 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 6 Apr 2016 19:25:47 +0200 Subject: [PATCH 016/507] Clarify intentions --- app/controllers/projects/git_http_controller.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 6dd7a683b0..11e17510cb 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -108,11 +108,14 @@ class Projects::GitHttpController < Projects::ApplicationController id = params[:project_id] return if id.nil? - if id.end_with?('.wiki.git') - id.slice(0, id.length - 9) - elsif id.end_with?('.git') - id.slice(0, id.length - 4) + %w{.wiki.git .git}.each do |suffix| + # Be careful to only remove the suffix from the end of 'id'. + # Accidentally removing it from the middle is how security + # vulnerabilities happen! + return id.slice(0, id.length - suffix.length) if id.end_with?(suffix) end + + nil end def repository From e7cea8cd75aa23ad4eb9705ddb0871775d65309b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 15 Apr 2016 11:22:08 +0200 Subject: [PATCH 017/507] Avoid path helper name clash --- app/controllers/projects/application_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 275e94d39e..817727d786 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -23,8 +23,8 @@ class Projects::ApplicationController < ApplicationController @project = find_project if @project && can?(current_user, :read_project, @project) - if @project.path_with_namespace != project_path - redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) + if @project.path_with_namespace != path_with_namespace + redirect_to request.original_url.gsub(path_with_namespace, @project.path_with_namespace) end else @project = nil @@ -48,12 +48,12 @@ class Projects::ApplicationController < ApplicationController params[:namespace_id] end - def project_path + def path_with_namespace "#{namespace}/#{id}" end def find_project - Project.find_with_namespace(project_path) + Project.find_with_namespace(path_with_namespace) end def repository From d3541da4ceaa0f5e2051edd2aa59d4275f93f0f8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 15 Apr 2016 12:40:43 +0200 Subject: [PATCH 018/507] Comment and whitespace --- .../projects/git_http_controller.rb | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 11e17510cb..13af17083b 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -2,7 +2,18 @@ class Projects::GitHttpController < Projects::ApplicationController skip_before_action :repository before_action :authenticate_user before_action :project_found? - + + # We support two actions (git push and git pull) which use four + # different HTTP requests: + # + # - GET /foo/bar.git/info/refs?service=git-upload-pack (pull) + # - GET /foo/bar.git/info/refs?service=git-receive-pack (push) + # - POST /foo/bar.git/git-upload-pack (pull) + # - POST /foo/bar.git/git-receive-pack" (push) + # + # The Rails routes divide these four requests over three methods: + # info_refs, git_upload_pack, and git_receive_pack. + def git_rpc if upload_pack? && upload_pack_allowed? render_ok @@ -12,7 +23,7 @@ class Projects::GitHttpController < Projects::ApplicationController render_not_found end end - + %i{info_refs git_receive_pack git_upload_pack}.each do |method| alias_method method, :git_rpc end @@ -60,7 +71,7 @@ class Projects::GitHttpController < Projects::ApplicationController token && token.accessible? && User.find_by(id: token.resource_owner_id) end end - + def rate_limit_ip!(login, user) # If the user authenticated successfully, we reset the auth failure count # from Rack::Attack for that IP. A client may attempt to authenticate @@ -95,7 +106,7 @@ class Projects::GitHttpController < Projects::ApplicationController "as #{login} but has been temporarily banned from Git auth" end end - + user end @@ -107,7 +118,7 @@ class Projects::GitHttpController < Projects::ApplicationController def id id = params[:project_id] return if id.nil? - + %w{.wiki.git .git}.each do |suffix| # Be careful to only remove the suffix from the end of 'id'. # Accidentally removing it from the middle is how security @@ -143,11 +154,11 @@ class Projects::GitHttpController < Projects::ApplicationController action_name.gsub('_', '-') end end - + def render_ok render json: Gitlab::Workhorse.git_http_ok(repository, user) end - + def render_not_found render text: 'Not Found', status: :not_found end @@ -155,11 +166,11 @@ class Projects::GitHttpController < Projects::ApplicationController def ci? !!@ci end - + def user @user end - + def upload_pack_allowed? if !Gitlab.config.gitlab_shell.upload_pack false From 9add3fbb3346460934d5990ede1b3216c03e62ee Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Apr 2016 13:24:53 +0200 Subject: [PATCH 019/507] =?UTF-8?q?Some=20changes=20after=20review=20from?= =?UTF-8?q?=20R=C3=A9my=20and=20Valery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../projects/git_http_controller.rb | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 13af17083b..cd8dd610bc 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,20 +1,11 @@ class Projects::GitHttpController < Projects::ApplicationController skip_before_action :repository before_action :authenticate_user - before_action :project_found? + before_action :ensure_project_found? - # We support two actions (git push and git pull) which use four - # different HTTP requests: - # - # - GET /foo/bar.git/info/refs?service=git-upload-pack (pull) - # - GET /foo/bar.git/info/refs?service=git-receive-pack (push) - # - POST /foo/bar.git/git-upload-pack (pull) - # - POST /foo/bar.git/git-receive-pack" (push) - # - # The Rails routes divide these four requests over three methods: - # info_refs, git_upload_pack, and git_receive_pack. - - def git_rpc + # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) + # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) + def info_refs if upload_pack? && upload_pack_allowed? render_ok elsif receive_pack? && receive_pack_allowed? @@ -24,8 +15,22 @@ class Projects::GitHttpController < Projects::ApplicationController end end - %i{info_refs git_receive_pack git_upload_pack}.each do |method| - alias_method method, :git_rpc + # POST /foo/bar.git/git-upload-pack (git pull) + def git_upload_pack + if upload_pack? && upload_pack_allowed? + render_ok + else + render_not_found + end + end + + # POST /foo/bar.git/git-receive-pack" (git push) + def git_receive_pack + if receive_pack? && receive_pack_allowed? + render_ok + else + render_not_found + end end private @@ -34,7 +39,7 @@ class Projects::GitHttpController < Projects::ApplicationController return if project && project.public? && upload_pack? authenticate_or_request_with_http_basic do |login, password| - return @ci = true if ci_request?(login, password) + return @ci = true if valid_ci_request?(login, password) @user = Gitlab::Auth.new.find(login, password) @user ||= oauth_access_token_check(login, password) @@ -42,19 +47,21 @@ class Projects::GitHttpController < Projects::ApplicationController end end - def project_found? + def ensure_project_found? render_not_found if project.blank? end - def ci_request?(login, password) - matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) + def valid_ci_request?(login, password) + matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) if project && matched_login.present? && upload_pack? - underscored_service = matched_login['s'].underscore + underscored_service = matched_login['service'].underscore if underscored_service == 'gitlab_ci' return project && project.valid_build_token?(password) elsif Service.available_services_names.include?(underscored_service) + # We treat underscored_service as a trusted input because it is included + # in the Service.available_services_names whitelist. service_method = "#{underscored_service}_service" service = project.send(service_method) @@ -126,6 +133,7 @@ class Projects::GitHttpController < Projects::ApplicationController return id.slice(0, id.length - suffix.length) if id.end_with?(suffix) end + # No valid id was found. nil end @@ -140,14 +148,14 @@ class Projects::GitHttpController < Projects::ApplicationController end def upload_pack? - rpc == 'git-upload-pack' + git_command == 'git-upload-pack' end def receive_pack? - rpc == 'git-receive-pack' + git_command == 'git-receive-pack' end - def rpc + def git_command if action_name == 'info_refs' params[:service] else @@ -178,11 +186,8 @@ class Projects::GitHttpController < Projects::ApplicationController true elsif user Gitlab::GitAccess.new(user, project).download_access_check.allowed? - elsif project.public? - # Allow clone/fetch for public projects - true else - false + project.public? end end From c161065e781a2c6d7a3b22954259809ffd7c5b26 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Apr 2016 13:58:40 +0200 Subject: [PATCH 020/507] Don't mess up our parent controller --- .../projects/application_controller.rb | 26 ++++----------- .../projects/git_http_controller.rb | 32 ++++++++++++------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 817727d786..74150ad606 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -10,6 +10,9 @@ class Projects::ApplicationController < ApplicationController def project unless @project + namespace = params[:namespace_id] + id = params[:project_id] || params[:id] + # Redirect from # localhost/group/project.git # to @@ -20,11 +23,12 @@ class Projects::ApplicationController < ApplicationController return end - @project = find_project + project_path = "#{namespace}/#{id}" + @project = Project.find_with_namespace(project_path) if @project && can?(current_user, :read_project, @project) - if @project.path_with_namespace != path_with_namespace - redirect_to request.original_url.gsub(path_with_namespace, @project.path_with_namespace) + if @project.path_with_namespace != project_path + redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) end else @project = nil @@ -40,22 +44,6 @@ class Projects::ApplicationController < ApplicationController @project end - def id - params[:project_id] || params[:id] - end - - def namespace - params[:namespace_id] - end - - def path_with_namespace - "#{namespace}/#{id}" - end - - def find_project - Project.find_with_namespace(path_with_namespace) - end - def repository @repository ||= project.repository end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index cd8dd610bc..e38552218e 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -119,27 +119,37 @@ class Projects::GitHttpController < Projects::ApplicationController def project return @project if defined?(@project) - @project = find_project + + project_id, _ = project_id_with_suffix + if project_id.blank? + @project = nil + else + @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") + end end - def id - id = params[:project_id] - return if id.nil? + # This method returns two values so that we can parse + # params[:project_id] (untrusted input!) in exactly one place. + def project_id_with_suffix + id = params[:project_id] || '' %w{.wiki.git .git}.each do |suffix| - # Be careful to only remove the suffix from the end of 'id'. - # Accidentally removing it from the middle is how security - # vulnerabilities happen! - return id.slice(0, id.length - suffix.length) if id.end_with?(suffix) + if id.end_with?(suffix) + # Be careful to only remove the suffix from the end of 'id'. + # Accidentally removing it from the middle is how security + # vulnerabilities happen! + return [id.slice(0, id.length - suffix.length), suffix] + end end - # No valid id was found. - nil + # Something is wrong with params[:project_id]; do not pass it on. + [nil, nil] end def repository @repository ||= begin - if params[:project_id].end_with?('.wiki.git') + _, suffix = project_id_with_suffix + if suffix == '.wiki.git' project.wiki.repository else project.repository From b64cbaccbe297c82b5af0dac94b491f86b17ddd3 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Apr 2016 14:04:36 +0200 Subject: [PATCH 021/507] Remove trivial 'let' --- spec/requests/git_http_spec.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 8b21768491..20c7357cba 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -54,7 +54,6 @@ describe 'Git HTTP requests', lib: true do context "when the project exists" do let(:path) { "#{project.path_with_namespace}.git" } - let(:env) { {} } context "when the project is public" do before do @@ -62,13 +61,13 @@ describe 'Git HTTP requests', lib: true do end it "downloads get status 200" do - download(path, env) do |response| + download(path, {}) do |response| expect(response.status).to eq(200) end end it "uploads get status 401" do - upload(path, env) do |response| + upload(path, {}) do |response| expect(response.status).to eq(401) end end @@ -97,7 +96,7 @@ describe 'Git HTTP requests', lib: true do it "responds with status 404" do allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) - download(path, env) do |response| + download(path, {}) do |response| expect(response.status).to eq(404) end end @@ -111,13 +110,13 @@ describe 'Git HTTP requests', lib: true do context "when no authentication is provided" do it "responds with status 401 to downloads" do - download(path, env) do |response| + download(path, {}) do |response| expect(response.status).to eq(401) end end it "responds with status 401 to uploads" do - upload(path, env) do |response| + upload(path, {}) do |response| expect(response.status).to eq(401) end end From d698d3e846c83f49cd363291dd811220c338c8e9 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 25 Apr 2016 18:05:05 +0200 Subject: [PATCH 022/507] =?UTF-8?q?More=20changes=20suggested=20by=20R?= =?UTF-8?q?=C3=A9my?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../projects/git_http_controller.rb | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index e38552218e..fafd9e445b 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,7 +1,9 @@ class Projects::GitHttpController < Projects::ApplicationController + attr_reader :user + skip_before_action :repository before_action :authenticate_user - before_action :ensure_project_found? + before_action :ensure_project_found! # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) @@ -47,29 +49,29 @@ class Projects::GitHttpController < Projects::ApplicationController end end - def ensure_project_found? + def ensure_project_found! render_not_found if project.blank? end def valid_ci_request?(login, password) matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) - if project && matched_login.present? && upload_pack? - underscored_service = matched_login['service'].underscore - - if underscored_service == 'gitlab_ci' - return project && project.valid_build_token?(password) - elsif Service.available_services_names.include?(underscored_service) - # We treat underscored_service as a trusted input because it is included - # in the Service.available_services_names whitelist. - service_method = "#{underscored_service}_service" - service = project.send(service_method) - - return service && service.activated? && service.valid_token?(password) - end + unless project && matched_login.present? && upload_pack? + return false end - false + underscored_service = matched_login['service'].underscore + + if underscored_service == 'gitlab_ci' + project && project.valid_build_token?(password) + elsif Service.available_services_names.include?(underscored_service) + # We treat underscored_service as a trusted input because it is included + # in the Service.available_services_names whitelist. + service_method = "#{underscored_service}_service" + service = project.send(service_method) + + service && service.activated? && service.valid_token?(password) + end end def oauth_access_token_check(login, password) @@ -185,10 +187,6 @@ class Projects::GitHttpController < Projects::ApplicationController !!@ci end - def user - @user - end - def upload_pack_allowed? if !Gitlab.config.gitlab_shell.upload_pack false From 9ef50db6279d722caed1ab1e4576275428e6a94f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 29 Apr 2016 18:56:53 +0200 Subject: [PATCH 023/507] Specify that oauth cannot push code --- spec/requests/git_http_spec.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 20c7357cba..14d126480a 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -179,6 +179,25 @@ describe 'Git HTTP requests', lib: true do end end + context "when an oauth token is provided" do + before do + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) + end + + it "downloads get status 200" do + clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token + + expect(response.status).to eq(200) + end + + it "uploads get status 401 (no project existence information leak)" do + push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token + + expect(response.status).to eq(401) + end + end + context "when blank password attempts follow a valid login" do def attempt_login(include_password) password = include_password ? user.password : "" From b1ffc9f0fee16251899e5a2efbc78c4781ef4902 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 29 Apr 2016 18:58:55 +0200 Subject: [PATCH 024/507] Make CI/Oauth/rate limiting reusable --- .../projects/git_http_controller.rb | 78 ++----------- config/initializers/doorkeeper.rb | 2 +- lib/api/session.rb | 8 +- lib/gitlab/auth.rb | 103 ++++++++++++++++-- lib/gitlab/backend/grack_auth.rb | 2 +- spec/lib/gitlab/auth_spec.rb | 56 ++++++++-- 6 files changed, 156 insertions(+), 93 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index fafd9e445b..16a85d6f62 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -41,11 +41,15 @@ class Projects::GitHttpController < Projects::ApplicationController return if project && project.public? && upload_pack? authenticate_or_request_with_http_basic do |login, password| - return @ci = true if valid_ci_request?(login, password) + user, type = Gitlab::Auth.find(login, password, project: project, ip: request.ip) - @user = Gitlab::Auth.new.find(login, password) - @user ||= oauth_access_token_check(login, password) - rate_limit_ip!(login, @user) + if (type == :ci) && upload_pack? + @ci = true + elsif (type == :oauth) && !upload_pack? + @user = nil + else + @user = user + end end end @@ -53,72 +57,6 @@ class Projects::GitHttpController < Projects::ApplicationController render_not_found if project.blank? end - def valid_ci_request?(login, password) - matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) - - unless project && matched_login.present? && upload_pack? - return false - end - - underscored_service = matched_login['service'].underscore - - if underscored_service == 'gitlab_ci' - project && project.valid_build_token?(password) - elsif Service.available_services_names.include?(underscored_service) - # We treat underscored_service as a trusted input because it is included - # in the Service.available_services_names whitelist. - service_method = "#{underscored_service}_service" - service = project.send(service_method) - - service && service.activated? && service.valid_token?(password) - end - end - - def oauth_access_token_check(login, password) - if login == "oauth2" && upload_pack? && password.present? - token = Doorkeeper::AccessToken.by_token(password) - token && token.accessible? && User.find_by(id: token.resource_owner_id) - end - end - - def rate_limit_ip!(login, user) - # 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 - return user unless 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 - - user - end - def project return @project if defined?(@project) diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 66ac88e9f4..0c694e0d37 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -12,7 +12,7 @@ Doorkeeper.configure do end resource_owner_from_credentials do |routes| - Gitlab::Auth.new.find(params[:username], params[:password]) + Gitlab::Auth.find_by_master_or_ldap(params[:username], params[:password]) end # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. diff --git a/lib/api/session.rb b/lib/api/session.rb index cc64689591..e308ccc300 100644 --- a/lib/api/session.rb +++ b/lib/api/session.rb @@ -11,8 +11,12 @@ module API # Example Request: # POST /session post "/session" do - auth = Gitlab::Auth.new - user = auth.find(params[:email] || params[:login], params[:password]) + user, _ = Gitlab::Auth.find( + params[:email] || params[:login], + params[:password], + project: nil, + ip: request.ip + ) return unauthorized! unless user present user, with: Entities::UserLogin diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 30509528b8..32e903905a 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,17 +1,100 @@ module Gitlab class Auth - def find(login, password) - user = User.by_login(login) + class << self + def find(login, password, project:, ip:) + raise "Must provide an IP for rate limiting" if ip.nil? - # If no user is found, or it's an LDAP server, try LDAP. - # LDAP users are only authenticated via LDAP - if user.nil? || user.ldap_user? - # Second chance - try LDAP authentication - return nil unless Gitlab::LDAP::Config.enabled? + user, type = nil, nil - Gitlab::LDAP::Authentication.login(login, password) - else - user if user.valid_password?(password) + if valid_ci_request?(login, password, project) + type = :ci + elsif user = find_by_master_or_ldap(login, password) + type = :master_or_ldap + elsif user = oauth_access_token_check(login, password) + type = :oauth + end + + rate_limit!(ip, success: !!user || (type == :ci), login: login) + [user, type] + end + + def find_by_master_or_ldap(login, password) + user = User.by_login(login) + + # If no user is found, or it's an LDAP server, try LDAP. + # LDAP users are only authenticated via LDAP + if user.nil? || user.ldap_user? + # Second chance - try LDAP authentication + return nil unless Gitlab::LDAP::Config.enabled? + + Gitlab::LDAP::Authentication.login(login, password) + else + user if user.valid_password?(password) + end + end + + private + + def valid_ci_request?(login, password, project) + matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) + + return false unless project && matched_login.present? + + underscored_service = matched_login['service'].underscore + + if underscored_service == 'gitlab_ci' + project && project.valid_build_token?(password) + elsif Service.available_services_names.include?(underscored_service) + # We treat underscored_service as a trusted input because it is included + # in the Service.available_services_names whitelist. + service_method = "#{underscored_service}_service" + service = project.send(service_method) + + service && service.activated? && service.valid_token?(password) + end + end + + def oauth_access_token_check(login, password) + if login == "oauth2" && password.present? + token = Doorkeeper::AccessToken.by_token(password) + token && token.accessible? && User.find_by(id: token.resource_owner_id) + end + end + + def rate_limit!(ip, success:, login:) + # 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 + return unless config.enabled + + if success + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(ip, config) + else + banned = Rack::Attack::Allow2Ban.filter(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?(ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end end end end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index e2363b9126..b263a27d4d 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -95,7 +95,7 @@ module Grack end def authenticate_user(login, password) - user = Gitlab::Auth.new.find(login, password) + user, _ = Gitlab::Auth.new.find_by_master_or_ldap(login, password) unless user user = oauth_access_token_check(login, password) diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index aad291c03c..2c2f7ed066 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -1,9 +1,47 @@ require 'spec_helper' describe Gitlab::Auth, lib: true do - let(:gl_auth) { Gitlab::Auth.new } + let(:gl_auth) { described_class } - describe :find do + describe 'find' do + it 'recognizes CI' do + token = '123' + project = create(:empty_project) + project.update_attributes(runners_token: token, builds_enabled: true) + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token') + expect(gl_auth.find('gitlab-ci-token', token, project: project, ip: ip)).to eq([nil, :ci]) + end + + it 'recognizes master passwords' do + user = create(:user, password: 'password') + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) + expect(gl_auth.find(user.username, 'password', project: nil, ip: ip)).to eq([user, :master_or_ldap]) + end + + it 'recognizes OAuth tokens' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') + expect(gl_auth.find("oauth2", token.token, project: nil, ip: ip)).to eq([user, :oauth]) + end + + it 'returns double nil for invalid credentials' do + login = 'foo' + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login) + expect(gl_auth.find(login, 'bar', project: nil, ip: ip)).to eq ([nil, nil]) + end + end + + describe 'find_by_master_or_ldap' do let!(:user) do create(:user, username: username, @@ -14,25 +52,25 @@ describe Gitlab::Auth, lib: true do let(:password) { 'my-secret' } it "should find user by valid login/password" do - expect( gl_auth.find(username, password) ).to eql user + expect( gl_auth.find_by_master_or_ldap(username, password) ).to eql user end it 'should find user by valid email/password with case-insensitive email' do - expect(gl_auth.find(user.email.upcase, password)).to eql user + expect(gl_auth.find_by_master_or_ldap(user.email.upcase, password)).to eql user end it 'should find user by valid username/password with case-insensitive username' do - expect(gl_auth.find(username.upcase, password)).to eql user + expect(gl_auth.find_by_master_or_ldap(username.upcase, password)).to eql user end it "should not find user with invalid password" do password = 'wrong' - expect( gl_auth.find(username, password) ).not_to eql user + expect( gl_auth.find_by_master_or_ldap(username, password) ).not_to eql user end it "should not find user with invalid login" do user = 'wrong' - expect( gl_auth.find(username, password) ).not_to eql user + expect( gl_auth.find_by_master_or_ldap(username, password) ).not_to eql user end context "with ldap enabled" do @@ -43,13 +81,13 @@ describe Gitlab::Auth, lib: true do it "tries to autheticate with db before ldap" do expect(Gitlab::LDAP::Authentication).not_to receive(:login) - gl_auth.find(username, password) + gl_auth.find_by_master_or_ldap(username, password) end it "uses ldap as fallback to for authentication" do expect(Gitlab::LDAP::Authentication).to receive(:login) - gl_auth.find('ldap_user', 'password') + gl_auth.find_by_master_or_ldap('ldap_user', 'password') end end end From d1f5019511a1dc630e97f99bdb1f6b9fe6b02bba Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 2 May 2016 13:19:39 +0200 Subject: [PATCH 025/507] Use correct auth finder --- lib/api/session.rb | 7 +------ lib/gitlab/backend/grack_auth.rb | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/api/session.rb b/lib/api/session.rb index e308ccc300..1156aab8cc 100644 --- a/lib/api/session.rb +++ b/lib/api/session.rb @@ -11,12 +11,7 @@ module API # Example Request: # POST /session post "/session" do - user, _ = Gitlab::Auth.find( - params[:email] || params[:login], - params[:password], - project: nil, - ip: request.ip - ) + user = Gitlab::Auth.find_by_master_or_ldap(params[:email] || params[:login], params[:password]) return unauthorized! unless user present user, with: Entities::UserLogin diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index b263a27d4d..3462c2dcfb 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -95,7 +95,7 @@ module Grack end def authenticate_user(login, password) - user, _ = Gitlab::Auth.new.find_by_master_or_ldap(login, password) + user = Gitlab::Auth.new.find_by_master_or_ldap(login, password) unless user user = oauth_access_token_check(login, password) From 9ce099429972726da22253407d98ae8aa1ef167b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 2 May 2016 13:21:59 +0200 Subject: [PATCH 026/507] Rubocop and whitespace --- lib/gitlab/workhorse.rb | 4 ++-- spec/lib/gitlab/auth_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 5b2982e499..f9ceee142d 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -36,9 +36,9 @@ module Gitlab "git-archive:#{encode(params)}", ] end - + protected - + def encode(hash) Base64.urlsafe_encode64(JSON.dump(hash)) end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 2c2f7ed066..16083f90bb 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::Auth, lib: true do ip = 'ip' expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login) - expect(gl_auth.find(login, 'bar', project: nil, ip: ip)).to eq ([nil, nil]) + expect(gl_auth.find(login, 'bar', project: nil, ip: ip)).to eq([nil, nil]) end end From 3dc276b367fe88c3c1026371d275d6078611f625 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 3 May 2016 11:46:14 +0200 Subject: [PATCH 027/507] Remove parallel assignment --- lib/gitlab/auth.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 32e903905a..0479006f99 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -4,7 +4,8 @@ module Gitlab def find(login, password, project:, ip:) raise "Must provide an IP for rate limiting" if ip.nil? - user, type = nil, nil + user = nil + type = nil if valid_ci_request?(login, password, project) type = :ci From 3bdc57f0a710b3769381ecad7ea4098223ecff56 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Sat, 16 Apr 2016 21:09:08 +0200 Subject: [PATCH 028/507] Create table for award emoji --- .../concerns/toggle_award_emoji.rb | 20 +++++ app/controllers/projects/issues_controller.rb | 4 +- .../projects/merge_requests_controller.rb | 6 +- app/controllers/projects/notes_controller.rb | 35 ++------ app/controllers/projects_controller.rb | 2 +- app/helpers/issues_helper.rb | 13 +-- app/models/award_emoji.rb | 35 ++++++++ app/models/concerns/awardable.rb | 81 ++++++++++++++++++ app/models/concerns/issuable.rb | 32 +------- app/models/merge_request.rb | 1 + app/models/note.rb | 53 +++--------- app/models/user.rb | 1 + app/services/notes/create_service.rb | 5 ++ app/services/notes/post_process_service.rb | 2 +- app/services/notification_service.rb | 1 - app/services/todo_service.rb | 8 ++ app/services/toggle_award_emoji_service.rb | 21 +++++ app/views/award_emoji/_awards_block.html.haml | 15 ++++ app/views/emojis/index.html.haml | 4 +- app/views/projects/issues/_issue.html.haml | 2 +- app/views/projects/issues/show.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- .../projects/merge_requests/_show.html.haml | 4 +- app/views/votes/_votes_block.html.haml | 8 +- config/initializers/inflections.rb | 4 + config/routes.rb | 7 +- db/migrate/20160416180807_add_award_emoji.rb | 15 ++++ ...82152_convert_award_note_to_emoji_award.rb | 17 ++++ .../20160416190505_remove_note_is_award.rb | 5 ++ db/schema.rb | 53 +++++++----- lib/api/entities.rb | 7 +- lib/award_emoji.rb | 80 ------------------ lib/gitlab/award_emoji.rb | 82 +++++++++++++++++++ spec/controllers/groups_controller_spec.rb | 12 +-- spec/factories/award_emoji.rb | 7 ++ spec/factories/notes.rb | 6 -- spec/helpers/issues_helper_spec.rb | 11 +-- spec/lib/{ => gitlab}/award_emoji_spec.rb | 6 +- spec/models/award_emoji_spec.rb | 31 +++++++ spec/models/concerns/issuable_spec.rb | 14 ---- spec/models/note_spec.rb | 39 --------- 41 files changed, 446 insertions(+), 307 deletions(-) create mode 100644 app/controllers/concerns/toggle_award_emoji.rb create mode 100644 app/models/award_emoji.rb create mode 100644 app/models/concerns/awardable.rb create mode 100644 app/services/toggle_award_emoji_service.rb create mode 100644 app/views/award_emoji/_awards_block.html.haml create mode 100644 db/migrate/20160416180807_add_award_emoji.rb create mode 100644 db/migrate/20160416182152_convert_award_note_to_emoji_award.rb create mode 100644 db/migrate/20160416190505_remove_note_is_award.rb delete mode 100644 lib/award_emoji.rb create mode 100644 lib/gitlab/award_emoji.rb create mode 100644 spec/factories/award_emoji.rb rename spec/lib/{ => gitlab}/award_emoji_spec.rb (75%) create mode 100644 spec/models/award_emoji_spec.rb diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb new file mode 100644 index 0000000000..9cd522d1c3 --- /dev/null +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -0,0 +1,20 @@ +module ToggleAwardEmoji + extend ActiveSupport::Concern + + included do + before_action :authenticate_user!, only: [:toggle_award_emoji] + end + + def toggle_award_emoji + name = params.require(:name) + awardable.toggle_award_emoji(name, current_user) + + render json: { ok: true } + end + + private + + def awardable + raise NotImplementedError + end +end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 38214f0479..86ba40facc 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,6 +1,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleSubscriptionAction include IssuableActions + include ToggleAwardEmoji before_action :module_enabled before_action :issue, @@ -61,7 +62,7 @@ class Projects::IssuesController < Projects::ApplicationController def show @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.nonawards.with_associations.fresh + @notes = @issue.notes.with_associations.fresh @noteable = @issue respond_to do |format| @@ -158,6 +159,7 @@ class Projects::IssuesController < Projects::ApplicationController end alias_method :subscribable_resource, :issue alias_method :issuable, :issue + alias_method :awardable, :issue def authorize_read_issue! return render_404 unless can?(current_user, :read_issue, @issue) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3e0cfc6aa6..9117f9242c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include ToggleSubscriptionAction include DiffHelper include IssuableActions + include ToggleAwardEmoji before_action :module_enabled before_action :merge_request, only: [ @@ -195,7 +196,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active? MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) - .execute(@merge_request) + .execute(@merge_request) @status = :merge_when_build_succeeds else MergeWorker.perform_async(@merge_request.id, current_user.id, params) @@ -264,6 +265,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end alias_method :subscribable_resource, :merge_request alias_method :issuable, :merge_request + alias_method :awardable, :merge_request def closes_issues @closes_issues ||= @merge_request.closes_issues @@ -299,7 +301,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_show_vars # 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 + @notes = @merge_request.mr_and_commit_notes.inc_author.fresh @discussions = Note.discussions_from_notes(@notes) @noteable = @merge_request diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 707a0d0e5c..9000e0adf6 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] before_action :authorize_admin_note!, only: [:update, :destroy] - before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle] + before_action :find_current_user_notes, only: [:index] def index current_fetched_at = Time.now.to_i @@ -22,8 +22,10 @@ class Projects::NotesController < Projects::ApplicationController def create @note = Notes::CreateService.new(project, current_user, note_params).execute + @note = note.is_a?(AwardEmoji) ? @note.to_note_json : note_json(@note) + respond_to do |format| - format.json { render json: note_json(@note) } + format.json { render json: @note } format.html { redirect_back_or_default } end end @@ -56,35 +58,12 @@ class Projects::NotesController < Projects::ApplicationController end end - def award_toggle - noteable = if note_params[:noteable_type] == "issue" - project.issues.find(note_params[:noteable_id]) - else - project.merge_requests.find(note_params[:noteable_id]) - end - - data = { - author: current_user, - is_award: true, - note: note_params[:note].delete(":") - } - - note = noteable.notes.find_by(data) - - if note - note.destroy - else - Notes::CreateService.new(project, current_user, note_params).execute - end - - render json: { ok: true } - end - private def note @note ||= @project.notes.find(params[:id]) end + alias_method :awardable, :note def note_to_html(note) render_to_string( @@ -137,7 +116,7 @@ class Projects::NotesController < Projects::ApplicationController id: note.id, discussion_id: note.discussion_id, html: note_to_html(note), - award: note.is_award, + award: false, note: note.note, discussion_html: note_to_discussion_html(note), discussion_with_diff_html: note_to_discussion_with_diff_html(note) @@ -145,7 +124,7 @@ class Projects::NotesController < Projects::ApplicationController else { valid: false, - award: note.is_award, + award: false, errors: note.errors } end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3768efe142..85a987c2cb 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -145,7 +145,7 @@ class ProjectsController < Projects::ApplicationController participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) @suggestions = { - emojis: AwardEmoji.urls, + emojis: Gitlab::AwardEmoji.urls, issues: autocomplete.issues, mergerequests: autocomplete.merge_requests, members: participants diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 4cb8adceba..38de0b442c 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -144,16 +144,17 @@ module IssuesHelper end end - def emoji_author_list(notes, current_user) - list = notes.map do |note| - note.author == current_user ? "me" : note.author.name - end + def award_user_list(awards, current_user) + list = + awards.map do |award| + award.user == current_user ? "me" : award.user.name + end list.join(", ") end - def note_active_class(notes, current_user) - if current_user && notes.pluck(:author_id).include?(current_user.id) + def award_active_class(awards, current_user) + if current_user && awards.find { |a| a.user_id == current_user.id } "active" else "" diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb new file mode 100644 index 0000000000..44a9b55a8a --- /dev/null +++ b/app/models/award_emoji.rb @@ -0,0 +1,35 @@ +class AwardEmoji < ActiveRecord::Base + DOWNVOTE_NAME = "thumbsdown".freeze + UPVOTE_NAME = "thumbsup".freeze + + include Participable + + belongs_to :awardable, polymorphic: true + belongs_to :user + + validates :awardable, :user, presence: true + validates :name, presence: true, inclusion: { in: Emoji.emojis_names } + validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] } + + participant :user + + scope :downvotes, -> { where(name: DOWNVOTE_NAME) } + scope :upvotes, -> { where(name: UPVOTE_NAME) } + + def downvote? + self.name == DOWNVOTE_NAME + end + + def upvote? + self.name == UPVOTE_NAME + end + + def to_note_json + { + valid: valid?, + award: true, + id: id, + name: name + } + end +end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb new file mode 100644 index 0000000000..b4e3e9eb3d --- /dev/null +++ b/app/models/concerns/awardable.rb @@ -0,0 +1,81 @@ +module Awardable + extend ActiveSupport::Concern + + included do + has_many :award_emoji, as: :awardable, dependent: :destroy + + if self < Participable + participant :award_emoji + end + end + + module ClassMethods + def order_upvotes_desc + order_votes_desc(AwardEmoji::UPVOTE_NAME) + end + + def order_downvotes_desc + order_votes_desc(AwardEmoji::DOWNVOTE_NAME) + end + + def order_votes_desc(emoji_name) + awardable_table = self.arel_table + awards_table = AwardEmoji.arel_table + + join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on( + awards_table[:awardable_id].eq(awardable_table[:id]).and( + awards_table[:awardable_type].eq(self.name).and( + awards_table[:name].eq(emoji_name) + ) + ) + ).join_sources + + joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC") + end + end + + def grouped_awards(with_thumbs = true) + awards = award_emoji.group_by(&:name) + + if with_thumbs + awards[AwardEmoji::UPVOTE_NAME] ||= AwardEmoji.none + awards[AwardEmoji::DOWNVOTE_NAME] ||= AwardEmoji.none + end + + awards + end + + def downvotes + award_emoji.where(name: AwardEmoji::DOWNVOTE_NAME).count + end + + def upvotes + award_emoji.where(name: AwardEmoji::UPVOTE_NAME).count + end + + def emoji_awardable? + true + end + + def awarded_emoji?(emoji_name, current_user) + award_emoji.where(name: emoji_name, user: current_user).exists? + end + + def create_award_emoji(name, current_user) + return unless emoji_awardable? + + award_emoji.create(name: name, user: current_user) + end + + def remove_award_emoji(name, current_user) + award_emoji.where(name: name, user: current_user).destroy_all + end + + def toggle_award_emoji(emoji_name, current_user) + if awarded_emoji?(emoji_name, current_user) + remove_award_emoji(emoji_name, current_user) + else + create_award_emoji(emoji_name, current_user) + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index afa2ca039a..6af76c97cd 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -10,6 +10,7 @@ module Issuable include Mentionable include Subscribable include StripAttribute + include Awardable included do belongs_to :author, class_name: "User" @@ -99,29 +100,6 @@ module Issuable order_by(method) end end - - def order_downvotes_desc - order_votes_desc('thumbsdown') - end - - def order_upvotes_desc - order_votes_desc('thumbsup') - end - - def order_votes_desc(award_emoji_name) - issuable_table = self.arel_table - note_table = Note.arel_table - - join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on( - note_table[:noteable_id].eq(issuable_table[:id]).and( - note_table[:noteable_type].eq(self.name).and( - note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name)) - ) - ) - ).join_sources - - joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC") - end end def today? @@ -144,14 +122,6 @@ module Issuable opened? || reopened? end - def downvotes - notes.awards.where(note: "thumbsdown").count - end - - def upvotes - notes.awards.where(note: "thumbsup").count - end - def subscribed_without_subscriptions?(user) participants(user).include?(user) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index e410febdff..2cb3e8b017 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -36,6 +36,7 @@ class MergeRequest < ActiveRecord::Base include Referable include Sortable include Taskable + include Awardable belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" diff --git a/app/models/note.rb b/app/models/note.rb index 87ced65c65..b992b2e76f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -16,7 +16,6 @@ # system :boolean default(FALSE), not null # st_diff :text # updated_by_id :integer -# is_award :boolean default(FALSE), not null # require 'carrierwave/orm/activerecord' @@ -43,12 +42,9 @@ class Note < ActiveRecord::Base delegate :name, to: :project, prefix: true 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 } @@ -60,8 +56,6 @@ class Note < ActiveRecord::Base mount_uploader :attachment, AttachmentUploader # Scopes - 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) } @@ -119,19 +113,6 @@ class Note < ActiveRecord::Base where(table[:note].matches(pattern)) end - - def grouped_awards - notes = {} - - awards.select(:note).distinct.map do |note| - notes[note.note] = where(note: note.note) - end - - notes["thumbsup"] ||= Note.none - notes["thumbsdown"] ||= Note.none - - notes - end end def cross_reference? @@ -347,37 +328,25 @@ class Note < ActiveRecord::Base Event.reset_event_cache_for(self) end - def downvote? - is_award && note == "thumbsdown" - end - - def upvote? - is_award && note == "thumbsup" + def system? + read_attribute(:system) end def editable? - !system? && !is_award + !system? end def cross_reference_not_visible_for?(user) cross_reference? && referenced_mentionables(user).empty? end - # Checks if note is an award added as a comment - # - # If note is an award, this method sets is_award to true - # and changes content of the note to award name. - # - # Method is executed as a before_validation callback. - # - def set_award! - return unless awards_supported? && contains_emoji_only? - - self.is_award = true - self.note = award_emoji_name + def award_emoji? + award_emoji_supported? && contains_emoji_only? end - private + def create_award_emoji + self.noteable.award_emoji(award_emoji_name, author) + end def clear_blank_line_code! self.line_code = nil if self.line_code.blank? @@ -389,8 +358,8 @@ class Note < ActiveRecord::Base diffs.find { |d| d.new_path == self.diff.new_path } end - def awards_supported? - (for_issue? || for_merge_request?) && !for_diff_line? + def award_emoji_supported? + noteable.is_a?(Awardable) && !for_diff_line? end def contains_emoji_only? @@ -399,6 +368,6 @@ class Note < ActiveRecord::Base def award_emoji_name original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] - AwardEmoji.normilize_emoji_name(original_name) + Gitlab::AwardEmoji.normilize_emoji_name(original_name) end end diff --git a/app/models/user.rb b/app/models/user.rb index 031315debd..52f2904f45 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -144,6 +144,7 @@ class User < ActiveRecord::Base has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy + has_many :award_emoji, as: :awardable, dependent: :destroy # # Validations diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 2bb312bb25..c5be21ba89 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -5,6 +5,11 @@ module Notes note.author = current_user note.system = false + if note.award_emoji? + return ToggleAwardEmojiService.new(project, current_user, params). + execute(note.noteable, note.note) + end + if note.save # Finish the harder work in the background NewNoteWorker.perform_in(2.seconds, note.id, params) diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index e818f58d13..c1bf46bdfb 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -8,7 +8,7 @@ module Notes def execute # Skip system notes, like status changes and cross-references and awards - unless @note.system || @note.is_award + unless @note.system EventCreateService.new.leave_note(@note, @note.author) @note.create_cross_references! execute_note_hooks diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 42ec1ac9e1..703636658b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -131,7 +131,6 @@ class NotificationService # ignore gitlab service messages return true if note.note.start_with?('Status changed to closed') return true if note.cross_reference? && note.system == true - return true if note.is_award target = note.noteable diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 42c5bca90f..da1b77c0f9 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -98,6 +98,14 @@ class TodoService handle_note(note, current_user) end + # When an emoji is awarded we should: + # + # * mark all pending todos related to the awardable for the current user as done + # + def new_award_emoji(awardable, current_user) + mark_pending_todos_as_done(awardable, current_user) + end + # When marking pending todos as done we should: # # * mark all pending todos related to the target for the current user as done diff --git a/app/services/toggle_award_emoji_service.rb b/app/services/toggle_award_emoji_service.rb new file mode 100644 index 0000000000..b77b4e79bf --- /dev/null +++ b/app/services/toggle_award_emoji_service.rb @@ -0,0 +1,21 @@ +require_relative 'base_service' + +class ToggleAwardEmojiService < BaseService + # For an award emoji being posted we should: + # - Mark the TODO as done for this issuable (skip on snippets) + # - Save the award emoji + def execute(awardable, emoji) + todo_service.new_award_emoji(awardable, current_user) + + # Needed if its posted as a note containing only :+1: + emoji = award_emoji_name(emoji) if emoji.start_with? ':' + awardable.toggle_award_emoji(emoji, current_user) + end + + private + + def award_emoji_name(emoji) + original_name = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] + Gitlab::AwardEmoji.normalize_emoji_name(original_name) + end +end diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml new file mode 100644 index 0000000000..63c953195f --- /dev/null +++ b/app/views/award_emoji/_awards_block.html.haml @@ -0,0 +1,15 @@ +- grouped_awards = awardable.grouped_awards(inline) +.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.size == 0), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } + - awards_sort(grouped_awards).each do |emoji, awards| + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), title: award_user_list(awards, current_user), data: { placement: "bottom" } } + = emoji_icon(emoji) + %span.award-control-text.js-counter + = awards.count + + - if current_user + .award-menu-holder.js-award-holder + %button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } + = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) + = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) + %span.award-control-text + Add diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml index 3443a8e230..97401a2e61 100644 --- a/app/views/emojis/index.html.haml +++ b/app/views/emojis/index.html.haml @@ -1,9 +1,9 @@ .emoji-menu .emoji-menu-content = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" - - AwardEmoji.emoji_by_category.each do |category, emojis| + - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis| %h5.emoji-menu-title - = AwardEmoji::CATEGORIES[category] + = Gitlab::AwardEmoji::CATEGORIES[category] %ul.clearfix.emoji-menu-list - emojis.each do |emoji| %li.pull-left.text-center.emoji-menu-list-item diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 7a8009f6da..4aa92d0b39 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -27,7 +27,7 @@ = icon('thumbs-down') = downvotes - - note_count = issue.notes.user.nonawards.count + - note_count = issue.notes.user.count - if note_count > 0 %li = link_to issue_path(issue) + "#notes" do diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 5fe5ddc081..c4cdd4b3d4 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -72,7 +72,7 @@ .content-block.content-block-small = render 'new_branch' - = render 'votes/votes_block', votable: @issue + = render 'award_emoji/awards_block', awardable: @issue, inline: true .row %section.col-md-12 diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index e740fe8c84..391193eed6 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -35,7 +35,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.nonawards.count + - note_count = merge_request.mr_and_commit_notes.user.count - if note_count > 0 %li = link_to merge_request_path(merge_request) + "#notes" do diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 2c34f9c454..e8cda51e75 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -50,7 +50,7 @@ %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count + %span.badge= @merge_request.mr_and_commit_notes.user.count %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits @@ -68,7 +68,7 @@ .tab-content #notes.notes.tab-pane.voting_notes .content-block.content-block-small.oneline-block - = render 'votes/votes_block', votable: @merge_request + = render 'award_emoji/awards_block', awardable: @merge_request, inline: true .row %section.col-md-12 diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index dc249155b9..8692c1ccce 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,9 +1,9 @@ -.awards.votes-block - - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| - %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}} +.awards.votes-block{data: { toggle_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) }} + - awards_sort(awardable.grouped_awards).each do |emoji, awards| + %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(awards, current_user)), data: {placement: "top", original_title: emoji_author_list(awards, current_user)}} = emoji_icon(emoji, sprite: false) %span.award-control-text.js-counter - = notes.count + = awards.count - if current_user %div.award-menu-holder.js-award-holder diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 9e8b0131f8..3d1a41a465 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -8,3 +8,7 @@ # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end +# +ActiveSupport::Inflector.inflections do |inflect| + inflect.uncountable %w(award_emoji) +end diff --git a/config/routes.rb b/config/routes.rb index 46a2526284..ecde83d854 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -639,6 +639,7 @@ Rails.application.routes.draw do post :cancel_merge_when_build_succeeds get :ci_status post :toggle_subscription + post :toggle_award_emoji post :remove_wip end @@ -703,6 +704,7 @@ Rails.application.routes.draw do resources :issues, constraints: { id: /\d+/ } do member do post :toggle_subscription + post :toggle_award_emoji get :referenced_merge_requests get :related_branches end @@ -731,10 +733,7 @@ Rails.application.routes.draw do resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do member do delete :delete_attachment - end - - collection do - post :award_toggle + post :toggle_award_emoji end end diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb new file mode 100644 index 0000000000..3177b86a13 --- /dev/null +++ b/db/migrate/20160416180807_add_award_emoji.rb @@ -0,0 +1,15 @@ +class AddAwardEmoji < ActiveRecord::Migration + def change + create_table :award_emoji do |t| + t.string :name + t.references :user + t.references :awardable, polymorphic: true + + t.timestamps + end + + add_index :award_emoji, :user_id + add_index :award_emoji, :awardable_type + add_index :award_emoji, :awardable_id + end +end diff --git a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb new file mode 100644 index 0000000000..76f4a3aa6a --- /dev/null +++ b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb @@ -0,0 +1,17 @@ +class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration + def change + def up + execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)" + end + + def down + execute <<-SQL + INSERT INTO notes (noteable_type, noteable_id, author_id, note, created_at, updated_at, is_award) + (SELECT awardable_type, awardable_id, user_id, name, created_at, updated_at, TRUE + FROM award_emoji + WHERE awardable_type IN ('Issue', 'MergeRequest') + ) + SQL + end + end +end diff --git a/db/migrate/20160416190505_remove_note_is_award.rb b/db/migrate/20160416190505_remove_note_is_award.rb new file mode 100644 index 0000000000..da16372a29 --- /dev/null +++ b/db/migrate/20160416190505_remove_note_is_award.rb @@ -0,0 +1,5 @@ +class RemoveNoteIsAward < ActiveRecord::Migration + def change + remove_column :notes, :is_award, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 42c261003b..354d7390a5 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: 20160412140240) do +ActiveRecord::Schema.define(version: 20160416190505) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -94,6 +94,19 @@ ActiveRecord::Schema.define(version: 20160412140240) do add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree + create_table "award_emoji", force: :cascade do |t| + t.string "name" + t.integer "user_id" + t.integer "awardable_id" + t.string "awardable_type" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "award_emoji", ["awardable_id"], name: "index_award_emoji_on_awardable_id", using: :btree + add_index "award_emoji", ["awardable_type"], name: "index_award_emoji_on_awardable_type", using: :btree + add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree + create_table "broadcast_messages", force: :cascade do |t| t.text "message", null: false t.datetime "starts_at" @@ -622,14 +635,12 @@ ActiveRecord::Schema.define(version: 20160412140240) do t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" - t.boolean "is_award", default: false, null: false end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree - add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree @@ -716,37 +727,37 @@ ActiveRecord::Schema.define(version: 20160412140240) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "wall_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false + t.boolean "issues_enabled", default: true, null: false + t.boolean "wall_enabled", default: true, null: false + t.boolean "merge_requests_enabled", default: true, null: false + t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false + t.string "issues_tracker", default: "gitlab", null: false t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false + t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false + t.integer "visibility_level", default: 0, null: false + t.boolean "archived", default: false, null: false t.string "avatar" t.string "import_status" - t.float "repository_size", default: 0.0 - t.integer "star_count", default: 0, null: false + t.float "repository_size", default: 0.0 + t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 + t.integer "commit_count", default: 0 t.text "import_error" t.integer "ci_id" - t.boolean "builds_enabled", default: true, null: false - t.boolean "shared_runners_enabled", default: true, null: false + t.boolean "builds_enabled", default: true, null: false + t.boolean "shared_runners_enabled", default: true, null: false t.string "runners_token" t.string "build_coverage_regex" - t.boolean "build_allow_git_fetch", default: true, null: false - t.integer "build_timeout", default: 3600, null: false - t.boolean "pending_delete", default: false - t.boolean "public_builds", default: true, null: false + t.boolean "build_allow_git_fetch", default: true, null: false + t.integer "build_timeout", default: 3600, null: false + t.boolean "pending_delete", default: false + t.boolean "public_builds", default: true, null: false t.string "main_language" - t.integer "pushes_since_gc", default: 0 + t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 60b9f5e0ec..b3769ba9c2 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -170,6 +170,7 @@ module API expose :label_names, as: :labels expose :milestone, using: Entities::Milestone expose :assignee, :author, using: Entities::UserBasic + expose :upvotes, :downvotes expose :subscribed do |issue, options| issue.subscribed?(options[:current_user]) @@ -178,7 +179,7 @@ module API class MergeRequest < ProjectEntity expose :target_branch, :source_branch - expose :upvotes, :downvotes + expose :upvotes, :downvotes expose :author, :assignee, using: Entities::UserBasic expose :source_project_id, :target_project_id expose :label_names, as: :labels @@ -216,8 +217,8 @@ module API expose :system?, as: :system expose :noteable_id, :noteable_type # upvote? and downvote? are deprecated, always return false - expose :upvote?, as: :upvote - expose :downvote?, as: :downvote + expose(:upvote?) { |note| false } + expose(:downvote?) { |note| false } end class MRNote < Grape::Entity diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb deleted file mode 100644 index 5f8ff01b0a..0000000000 --- a/lib/award_emoji.rb +++ /dev/null @@ -1,80 +0,0 @@ -class AwardEmoji - CATEGORIES = { - other: "Other", - objects: "Objects", - places: "Places", - travel_places: "Travel", - emoticons: "Emoticons", - objects_symbols: "Symbols", - nature: "Nature", - celebration: "Celebration", - people: "People", - activity: "Activity", - flags: "Flags", - food_drink: "Food" - }.with_indifferent_access - - CATEGORY_ALIASES = { - symbols: "objects_symbols", - foods: "food_drink", - travel: "travel_places" - }.with_indifferent_access - - def self.normilize_emoji_name(name) - aliases[name] || name - end - - def self.emoji_by_category - unless @emoji_by_category - @emoji_by_category = Hash.new { |h, key| h[key] = [] } - - emojis.each do |emoji_name, data| - data["name"] = emoji_name - - # Skip Fitzpatrick(tone) modifiers - next if data["category"] == "modifier" - - category = CATEGORY_ALIASES[data["category"]] || data["category"] - - @emoji_by_category[category] << data - end - - @emoji_by_category = @emoji_by_category.sort.to_h - end - - @emoji_by_category - end - - def self.emojis - @emojis ||= begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) - JSON.parse(File.read(json_path)) - end - end - - def self.aliases - @aliases ||= begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) - JSON.parse(File.read(json_path)) - end - end - - # Returns an Array of Emoji names and their asset URLs. - def self.urls - @urls ||= begin - path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') - prefix = Gitlab::Application.config.assets.prefix - digest = Gitlab::Application.config.assets.digest - - JSON.parse(File.read(path)).map do |hash| - if digest - fname = "#{hash['unicode']}-#{hash['digest']}" - else - fname = hash['unicode'] - end - - { name: hash['name'], path: "#{prefix}/#{fname}.png" } - end - end - end -end diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb new file mode 100644 index 0000000000..0ae220a86b --- /dev/null +++ b/lib/gitlab/award_emoji.rb @@ -0,0 +1,82 @@ +module Gitlab + class AwardEmoji + CATEGORIES = { + other: "Other", + objects: "Objects", + places: "Places", + travel_places: "Travel", + emoticons: "Emoticons", + objects_symbols: "Symbols", + nature: "Nature", + celebration: "Celebration", + people: "People", + activity: "Activity", + flags: "Flags", + food_drink: "Food" + }.with_indifferent_access + + CATEGORY_ALIASES = { + symbols: "objects_symbols", + foods: "food_drink", + travel: "travel_places" + }.with_indifferent_access + + def self.normalize_emoji_name(name) + aliases[name] || name + end + + def self.emoji_by_category + unless @emoji_by_category + @emoji_by_category = Hash.new { |h, key| h[key] = [] } + + emojis.each do |emoji_name, data| + data["name"] = emoji_name + + # Skip Fitzpatrick(tone) modifiers + next if data["category"] == "modifier" + + category = CATEGORY_ALIASES[data["category"]] || data["category"] + + @emoji_by_category[category] << data + end + + @emoji_by_category = @emoji_by_category.sort.to_h + end + + @emoji_by_category + end + + def self.emojis + @emojis ||= begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) + JSON.parse(File.read(json_path)) + end + end + + def self.aliases + @aliases ||= begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) + JSON.parse(File.read(json_path)) + end + end + + # Returns an Array of Emoji names and their asset URLs. + def self.urls + @urls ||= begin + path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') + prefix = Gitlab::Application.config.assets.prefix + digest = Gitlab::Application.config.assets.digest + + JSON.parse(File.read(path)).map do |hash| + if digest + fname = "#{hash['unicode']}-#{hash['digest']}" + else + fname = hash['unicode'] + end + + { name: hash['name'], path: "#{prefix}/#{fname}.png" } + end + end + end + end +end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 465531b2b3..82b2570217 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -31,9 +31,9 @@ describe GroupsController do let(:issue_2) { create(:issue, project: project) } before do - create_list(:upvote_note, 3, project: project, noteable: issue_2) - create_list(:upvote_note, 2, project: project, noteable: issue_1) - create_list(:downvote_note, 2, project: project, noteable: issue_2) + create_list(:award_emoji, 3, awardable: issue_2) + create_list(:award_emoji, 2, awardable: issue_1) + create_list(:award_emoji, 2, awardable: issue_2, name: "thumbsdown") sign_in(user) end @@ -56,9 +56,9 @@ describe GroupsController do let(:merge_request_2) { create(:merge_request, :simple, source_project: project) } before do - create_list(:upvote_note, 3, project: project, noteable: merge_request_2) - create_list(:upvote_note, 2, project: project, noteable: merge_request_1) - create_list(:downvote_note, 2, project: project, noteable: merge_request_2) + create_list(:award_emoji, 3, awardable: merge_request_2) + create_list(:award_emoji, 2, awardable: merge_request_1) + create_list(:award_emoji, 2, awardable: merge_request_2, name: "thumbsdown") sign_in(user) end diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb new file mode 100644 index 0000000000..a1173834b2 --- /dev/null +++ b/spec/factories/award_emoji.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :award_emoji do + name "thumbsup" + user + awardable factory: :issue + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index e5dcb15901..2bfc5effd7 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -36,8 +36,6 @@ FactoryGirl.define do factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] - factory :downvote_note, traits: [:award, :downvote] - factory :upvote_note, traits: [:award, :upvote] trait :on_commit do project @@ -69,10 +67,6 @@ FactoryGirl.define do system true end - trait :award do - is_award true - end - trait :downvote do note "thumbsdown" end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 543593cf38..2d4d9c18c9 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -127,18 +127,15 @@ describe IssuesHelper do it { is_expected.to eq("!1, !2, or !3") } end - describe "note_active_class" do - before do - @note = create :note - @note1 = create :note - end + describe '#award_active_class' do + let!(:upvote) { create(:award_emoji) } it "returns empty string for unauthenticated user" do - expect(note_active_class(Note.all, nil)).to eq("") + expect(award_active_class(AwardEmoji.all, nil)).to eq("") end it "returns active string for author" do - expect(note_active_class(Note.all, @note.author)).to eq("active") + expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active") end end diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb similarity index 75% rename from spec/lib/award_emoji_spec.rb rename to spec/lib/gitlab/award_emoji_spec.rb index 88c2291295..4e6c04a11b 100644 --- a/spec/lib/award_emoji_spec.rb +++ b/spec/lib/gitlab/award_emoji_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe AwardEmoji do +describe Gitlab::AwardEmoji do describe '.urls' do - subject { AwardEmoji.urls } + subject { Gitlab::AwardEmoji.urls } it { is_expected.to be_an_instance_of(Array) } it { is_expected.to_not be_empty } @@ -19,7 +19,7 @@ describe AwardEmoji do describe '.emoji_by_category' do it "only contains known categories" do - undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys + undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys expect(undefined_categories).to be_empty end end diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb new file mode 100644 index 0000000000..fd3712b7d4 --- /dev/null +++ b/spec/models/award_emoji_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe AwardEmoji, models: true do + describe 'Associations' do + it { is_expected.to belong_to(:awardable) } + it { is_expected.to belong_to(:user) } + end + + describe 'modules' do + it { is_expected.to include_module(Participable) } + end + + describe "validations" do + it { is_expected.to validate_presence_of(:awardable) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:awardable) } + + # To circumvent a bug in the shoulda matchers + describe "scoped uniqueness validation" do + it "rejects duplicate award emoji" do + user = create(:user) + issue = create(:issue) + create(:award_emoji, user: user, awardable: issue) + new_award = AwardEmoji.new(user: user, awardable: issue, name: "thumbsup") + + expect(new_award).not_to be_valid + end + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index b16ccc6e30..d5435916ea 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -198,18 +198,4 @@ describe Issue, "Issuable" do to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' }) end end - - describe "votes" do - before do - author = create :user - project = create :empty_project - issue.notes.awards.create!(note: "thumbsup", author: author, project: project) - issue.notes.awards.create!(note: "thumbsdown", author: author, project: project) - end - - it "returns correct values" do - expect(issue.upvotes).to eq(1) - expect(issue.downvotes).to eq(1) - end - end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 6b18936edb..bb591e9cb5 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -152,23 +152,6 @@ describe Note, models: true do end end - describe '.grouped_awards' do - before do - create :note, note: "smile", is_award: true - create :note, note: "smile", is_award: true - end - - it "returns grouped hash of notes" do - expect(Note.grouped_awards.keys.size).to eq(3) - expect(Note.grouped_awards["smile"]).to match_array(Note.all) - end - - it "returns thumbsup and thumbsdown always" do - expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none) - expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none) - end - end - describe '#active?' do it 'is always true when the note has no associated diff' do note = build(:note) @@ -239,11 +222,6 @@ describe Note, models: true do note = build(:note, system: true) expect(note.editable?).to be_falsy end - - it "returns false" do - note = build(:note, is_award: true, note: "smiley") - expect(note.editable?).to be_falsy - end end describe "cross_reference_not_visible_for?" do @@ -270,23 +248,6 @@ describe Note, models: true do end end - describe "set_award!" do - let(:merge_request) { create :merge_request } - - it "converts aliases to actual name" do - note = create(:note, note: ":+1:", noteable: merge_request) - expect(note.reload.note).to eq("thumbsup") - 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 = note.reload - - expect(note.note).to eq(":blowfish:") - expect(note.is_award?).to be_falsy - end - end - describe 'clear_blank_line_code!' do it 'clears a blank line code before validation' do note = build(:note, line_code: ' ') From 4eb16290e4e95c0a9bcf3d01ecc8060d91eec021 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Mon, 25 Apr 2016 09:09:39 +0100 Subject: [PATCH 029/507] move frontend logic from previous MR to new MR --- app/assets/javascripts/awards_handler.coffee | 211 +++++++++++------- app/assets/javascripts/dispatcher.js.coffee | 2 + .../lib/emoji_aliases.js.coffee.erb | 9 + app/assets/javascripts/notes.js.coffee | 4 +- app/assets/stylesheets/pages/awards.scss | 13 +- app/assets/stylesheets/pages/notes.scss | 41 +++- app/finders/notes_finder.rb | 4 +- app/views/award_emoji/_awards_block.html.haml | 6 +- 8 files changed, 190 insertions(+), 100 deletions(-) create mode 100644 app/assets/javascripts/lib/emoji_aliases.js.coffee.erb diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index af4462ece3..4c0a274b79 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,63 +1,109 @@ class @AwardsHandler - constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @aliases) -> - $(".js-add-award").on "click", (event) => - event.stopPropagation() - event.preventDefault() + constructor: -> + @aliases = gl.emoji.emojiAliases() - @showEmojiMenu() + $(document) + .off "click", ".js-add-award" + .on "click", ".js-add-award", (event) => + event.stopPropagation() + event.preventDefault() + + @showEmojiMenu $(event.currentTarget) $("html").on 'click', (event) -> if !$(event.target).closest(".emoji-menu").length if $(".emoji-menu").is(":visible") + $('.js-add-award.is-active').removeClass 'is-active' $(".emoji-menu").removeClass "is-visible" - $(".awards") - .off "click" - .on "click", ".js-emoji-btn", @handleClick - - @renderFrequentlyUsedBlock() + $(document) + .off "click", ".js-emoji-btn" + .on "click", ".js-emoji-btn", (e) => @handleClick(e) handleClick: (e) -> e.preventDefault() - emoji = $(this) + $emojiBtn = $(e.currentTarget) + $addAwardBtn = $('.js-add-award.is-active') + $votesBlock = $($addAwardBtn.closest('.js-award-holder').data('target')) + + if $addAwardBtn.length is 0 + $votesBlock = $emojiBtn.closest('.js-awards-block') + else if $votesBlock.length is 0 + $votesBlock = $addAwardBtn.closest('.js-awards-block') + + $votesBlock.addClass 'js-awards-block-current' + awardUrl = $votesBlock.data 'award-url' + emoji = $emojiBtn .find(".icon") .data "emoji" - if emoji is "thumbsup" and awards_handler.didUserClickEmoji $(this), "thumbsdown" - awards_handler.addAward "thumbsdown" + if emoji is "thumbsup" and @didUserClickEmoji $emojiBtn, "thumbsdown" + @addAward awardUrl, "thumbsdown" - else if emoji is "thumbsdown" and awards_handler.didUserClickEmoji $(this), "thumbsup" - awards_handler.addAward "thumbsup" + else if emoji is "thumbsdown" and @didUserClickEmoji $emojiBtn, "thumbsup" + @addAward awardUrl, "thumbsup" - awards_handler.addAward emoji + @addAward awardUrl, emoji - didUserClickEmoji: (that, emoji) -> - if $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title") - $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1 + didUserClickEmoji: (emojiBtn, emoji) -> + if emojiBtn.siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title") + emojiBtn.siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1 - showEmojiMenu: -> - if $(".emoji-menu").length - if $(".emoji-menu").is ".is-visible" - $(".emoji-menu").removeClass "is-visible" + showEmojiMenu: ($addBtn) -> + $menu = $('.emoji-menu') + if $menu.length + $holder = $addBtn.closest('.js-award-holder') + + if $menu.is ".is-visible" + $addBtn.removeClass "is-active" + $menu.removeClass "is-visible" $("#emoji_search").blur() else $(".emoji-menu").addClass "is-visible" + $addBtn.addClass "is-active" + @positionMenu($menu, $addBtn) + + $menu.addClass "is-visible" $("#emoji_search").focus() else - $('.js-add-award').addClass "is-loading" - $.get @get_emojis_url, (response) => - $('.js-add-award').removeClass "is-loading" - $(".js-award-holder").append response + $addBtn.addClass "is-loading is-active" + $.get $addBtn.data('award-menu-url'), (response) => + $addBtn.removeClass "is-loading" + $('body').append response + + $menu = $(".emoji-menu") + + @positionMenu($menu, $addBtn) + + @renderFrequentlyUsedBlock() setTimeout => - $(".emoji-menu").addClass "is-visible" + $menu.addClass "is-visible" $("#emoji_search").focus() @setupSearch() , 200 - addAward: (emoji) -> + positionMenu: ($menu, $addBtn) -> + position = $addBtn.data('position') + + # The menu could potentially be off-screen or in a hidden overflow element + # So we position the element absolute in the body + css = + top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px" + + if position? and position is 'right' + css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px" + $menu.addClass "is-aligned-right" + else + css.left = "#{$addBtn.offset().left}px" + $menu.removeClass "is-aligned-right" + + $menu.css(css) + + addAward: (awardUrl, emoji) -> emoji = @normilizeEmojiName(emoji) - @postEmoji emoji, => + @postEmoji awardUrl, emoji, => @addAwardToEmojiBar(emoji) + $('.js-awards-block').removeClass 'js-awards-block-current' $(".emoji-menu").removeClass "is-visible" @@ -65,58 +111,60 @@ class @AwardsHandler @addEmojiToFrequentlyUsedList(emoji) emoji = @normilizeEmojiName(emoji) - if @exist(emoji) - if @isActive(emoji) - @decrementCounter(emoji) + $emojiBtn = @findEmojiIcon(emoji).parent() + + if $emojiBtn.length > 0 + if @isActive($emojiBtn) + @decrementCounter($emojiBtn, emoji) else - counter = @findEmojiIcon(emoji).siblings(".js-counter") - counter.text(parseInt(counter.text()) + 1) - counter.parent().addClass("active") - @addMeToAuthorList(emoji) + $counter = $emojiBtn.find('.js-counter') + $counter.text(parseInt($counter.text()) + 1) + $emojiBtn.addClass("active") + @addMeToUserList(emoji) else @createEmoji(emoji) - exist: (emoji) -> - @findEmojiIcon(emoji).length > 0 + isActive: ($emojiBtn) -> + $emojiBtn.hasClass("active") - isActive: (emoji) -> - @findEmojiIcon(emoji).parent().hasClass("active") + decrementCounter: ($emojiBtn, emoji) -> + $awardsBlock = $emojiBtn.closest('.js-awards-block') + isntNoteBody = $emojiBtn.closest('.note-body').length is 0 + counter = $('.js-counter', $emojiBtn) + counterNumber = parseInt(counter.text()) - decrementCounter: (emoji) -> - counter = @findEmojiIcon(emoji).siblings(".js-counter") - emojiIcon = counter.parent() - if parseInt(counter.text()) > 1 - counter.text(parseInt(counter.text()) - 1) - emojiIcon.removeClass("active") - @removeMeFromAuthorList(emoji) - else if emoji == "thumbsup" || emoji == "thumbsdown" - emojiIcon.tooltip("destroy") - counter.text(0) - emojiIcon.removeClass("active") - @removeMeFromAuthorList(emoji) + if counterNumber > 1 + counter.text(counterNumber - 1) + @removeMeFromUserList($emojiBtn, emoji) + else if (emoji == "thumbsup" || emoji == "thumbsdown") && isntNoteBody + $emojiBtn.tooltip("destroy") + counter.text('0') + @removeMeFromUserList($emojiBtn, emoji) else - emojiIcon.tooltip("destroy") - emojiIcon.remove() + $emojiBtn.tooltip("destroy") + $emojiBtn.remove() - removeMeFromAuthorList: (emoji) -> - award_block = @findEmojiIcon(emoji).parent() + $emojiBtn.removeClass("active") + + removeMeFromUserList: ($emojiBtn, emoji) -> + award_block = $emojiBtn authors = award_block .attr("data-original-title") .split(", ") - authors.splice(authors.indexOf("me"),1) + authors.splice(authors.indexOf("me"), 1) award_block .closest(".js-emoji-btn") .attr("data-original-title", authors.join(", ")) @resetTooltip(award_block) - addMeToAuthorList: (emoji) -> + addMeToUserList: (emoji) -> award_block = @findEmojiIcon(emoji).parent() origTitle = award_block.attr("data-original-title").trim() - authors = [] + users = [] if origTitle - authors = origTitle.split(', ') - authors.push("me") - award_block.attr("data-original-title", authors.join(", ")) + users = origTitle.split(', ') + users.push("me") + award_block.attr("data-original-title", users.join(", ")) @resetTooltip(award_block) resetTooltip: (award) -> @@ -127,24 +175,24 @@ class @AwardsHandler award.tooltip() ), 200 - createEmoji: (emoji) -> emojiCssClass = @resolveNameToCssClass(emoji) - nodes = [] - nodes.push( - "" - ) + buttonHtml = "" - emoji_node = $(nodes.join("\n")) - .insertBefore(".js-award-holder") + emoji_node = $(buttonHtml) + .insertBefore(".js-awards-block-current .js-award-holder:not(.js-award-action-btn)") .find(".emoji-icon") .data("emoji", emoji) $('.award-control').tooltip() + $currentBlock = $('.js-awards-block-current') + if $currentBlock.is('.hidden') + $currentBlock.removeClass 'hidden' + resolveNameToCssClass: (emoji) -> emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") @@ -156,17 +204,13 @@ class @AwardsHandler "emoji-#{unicodeName}" - postEmoji: (emoji, callback) -> - $.post @post_emoji_url, { note: { - note: ":#{emoji}:" - noteable_type: @noteable_type - noteable_id: @noteable_id - }},(data) -> + postEmoji: (awardUrl, emoji, callback) -> + $.post awardUrl, { name: emoji }, (data) -> if data.ok callback.call() findEmojiIcon: (emoji) -> - $(".awards > .js-emoji-btn [data-emoji='#{emoji}']") + $(".js-awards-block-current.awards > .js-emoji-btn [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ @@ -189,16 +233,15 @@ class @AwardsHandler if $.cookie('frequently_used_emojis') frequently_used_emojis = @getFrequentlyUsedEmojis() - ul = $("
    ") + ul = $("
      ") for emoji in frequently_used_emojis - do (emoji) -> - $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) + $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) $("input.emoji-search").after(ul).after($("
      ").text("Frequently used")) setupSearch: -> - $("input.emoji-search").keyup (ev) => + $("input.emoji-search").on 'keyup', (ev) => term = $(ev.target).val() # Clean previous search results diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 0b9110d35f..610fa990fc 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -22,6 +22,7 @@ class Dispatcher new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() + awards_handler = new AwardsHandler() when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() when 'dashboard:todos:index' @@ -52,6 +53,7 @@ class Dispatcher new Diff() shortcut_handler = new ShortcutsIssuable(true) new ZenMode() + awards_handler = new AwardsHandler() when "projects:merge_requests:diffs" new Diff() new ZenMode() diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb new file mode 100644 index 0000000000..e005ab3684 --- /dev/null +++ b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb @@ -0,0 +1,9 @@ +((w) -> + + w.gl ?= {} + w.gl.emoji ?= {} + + w.gl.emoji.emojiAliases = -> + JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') + +) window \ No newline at end of file diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index fa91baa07c..ae8c1f22e4 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -150,12 +150,12 @@ class @Notes renderNote: (note) -> unless note.valid if note.award - flash = new Flash('You have already used this award emoji!', 'alert') + flash = new Flash('You have already awarded this emoji, and it we\'ve removed it', 'alert') flash.pinTo('.header-content') return if note.award - awards_handler.addAwardToEmojiBar(note.note) + awards_handler.addAwardToEmojiBar(note.name) awards_handler.scrollToAwards() # render note if it not present in loaded list diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 37bf38fa65..07d40f4055 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -1,6 +1,4 @@ .awards { - line-height: 34px; - .emoji-icon { width: 20px; height: 20px; @@ -9,8 +7,6 @@ .emoji-menu { position: absolute; - top: 100%; - left: 0; margin-top: 3px; z-index: 1000; min-width: 160px; @@ -23,7 +19,12 @@ opacity: 0; transform: scale(.2); transform-origin: 0 -45px; - transition: all .3s cubic-bezier(.87,-.41,.19,1.44); + transition: .3s cubic-bezier(.87,-.41,.19,1.44); + transition-property: transform, opacity; + + &.is-aligned-right { + transform-origin: 100% -45px; + } &.is-visible { pointer-events: all; @@ -107,7 +108,7 @@ } &.is-loading { - .award-control-icon { + .award-control-icon-normal { display: none; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index ce44f5aa13..9d808cce15 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -63,7 +63,8 @@ ul.notes { &.is-editting { .note-header, .note-text, - .edited-text { + .edited-text, + .note-awards { display: none; } @@ -73,8 +74,6 @@ ul.notes { } .note-body { - overflow: auto; - .note-text { overflow: auto; word-wrap: break-word; @@ -307,6 +306,42 @@ ul.notes { } } +.note-award-control { + display: block; + + &:hover, + &:focus { + text-decoration: none; + } + + .award-control-icon-loading { + display: none; + } + + &.is-loading { + .award-control-icon-normal { + display: none; + } + + .award-control-icon-loading { + display: block; + } + } +} + +.note-awards { + .awards { + padding-top: 10px; + } + + .award-control { + padding-top: 2px; + padding-bottom: 2px; + color: #8f8f8f; + font-size: 13px; + } +} + .disabled-comment { margin-left: -$gl-padding-top; margin-right: -$gl-padding-top; diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index fa4c635f55..ab252821b5 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -12,9 +12,9 @@ class NotesFinder when "commit" project.notes.for_commit_id(target_id).not_inline when "issue" - project.issues.find(target_id).notes.nonawards.inc_author + project.issues.find(target_id).notes.inc_author when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author + project.merge_requests.find(target_id).mr_and_commit_notes.inc_author when "snippet", "project_snippet" project.snippets.find(target_id).notes else diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 63c953195f..b57c9afcbd 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,7 +1,7 @@ -- grouped_awards = awardable.grouped_awards(inline) +- grouped_emojis = awardable.grouped_awards(inline) .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.size == 0), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } - - awards_sort(grouped_awards).each do |emoji, awards| - %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), title: award_user_list(awards, current_user), data: { placement: "bottom" } } + - awards_sort(grouped_emojis).each do |emoji, awards| + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)),data: { placement: "bottom", original_title: award_user_list(awards, current_user)} } = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count From dccf8a9fc8d4dde91942944f6b47387bfb13c380 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 25 Apr 2016 20:10:20 +0200 Subject: [PATCH 030/507] Add tests on Awardables and Award Emoji --- app/assets/javascripts/awards_handler.coffee | 20 ++++---- app/assets/javascripts/notes.js.coffee | 2 +- app/controllers/projects/notes_controller.rb | 3 +- app/helpers/issues_helper.rb | 11 ++--- app/models/merge_request.rb | 1 - db/schema.rb | 19 ++++++- features/steps/project/issues/issues.rb | 10 ++-- features/steps/project/merge_requests.rb | 10 ++-- .../projects/issues_controller_spec.rb | 14 ++++++ spec/factories/award_emoji.rb | 11 +++++ spec/features/issues_spec.rb | 2 +- spec/features/notes_on_merge_requests_spec.rb | 4 +- spec/models/concerns/awardable_spec.rb | 49 +++++++++++++++++++ spec/models/concerns/issuable_spec.rb | 4 ++ spec/models/user_spec.rb | 1 + spec/services/notes/create_service_spec.rb | 20 +++----- spec/services/todo_service_spec.rb | 17 ++++--- .../toggle_award_emoji_service_spec.rb | 39 +++++++++++++++ 18 files changed, 181 insertions(+), 56 deletions(-) create mode 100644 spec/models/concerns/awardable_spec.rb create mode 100644 spec/services/toggle_award_emoji_service_spec.rb diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 4c0a274b79..589caf011e 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -4,21 +4,21 @@ class @AwardsHandler $(document) .off "click", ".js-add-award" - .on "click", ".js-add-award", (event) => - event.stopPropagation() - event.preventDefault() + .on "click", ".js-add-award", (e) => + e.stopPropagation() + e.preventDefault() - @showEmojiMenu $(event.currentTarget) + @showEmojiMenu $(e.currentTarget) - $("html").on 'click', (event) -> - if !$(event.target).closest(".emoji-menu").length + $("html").on 'click', (e) -> + if !$(e.target).closest(".emoji-menu").length if $(".emoji-menu").is(":visible") $('.js-add-award.is-active').removeClass 'is-active' $(".emoji-menu").removeClass "is-visible" $(document) .off "click", ".js-emoji-btn" - .on "click", ".js-emoji-btn", (e) => @handleClick(e) + .on "click", ".js-emoji-btn", @handleClick.bind(@) handleClick: (e) -> e.preventDefault() @@ -31,7 +31,8 @@ class @AwardsHandler else if $votesBlock.length is 0 $votesBlock = $addAwardBtn.closest('.js-awards-block') - $votesBlock.addClass 'js-awards-block-current' + @currentVoteBlock = $votesBlock + awardUrl = $votesBlock.data 'award-url' emoji = $emojiBtn .find(".icon") @@ -103,7 +104,6 @@ class @AwardsHandler emoji = @normilizeEmojiName(emoji) @postEmoji awardUrl, emoji, => @addAwardToEmojiBar(emoji) - $('.js-awards-block').removeClass 'js-awards-block-current' $(".emoji-menu").removeClass "is-visible" @@ -210,7 +210,7 @@ class @AwardsHandler callback.call() findEmojiIcon: (emoji) -> - $(".js-awards-block-current.awards > .js-emoji-btn [data-emoji='#{emoji}']") + @currentVoteBlock.find(".js-emoji-btn [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ae8c1f22e4..74ae897b84 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -150,7 +150,7 @@ class @Notes renderNote: (note) -> unless note.valid if note.award - flash = new Flash('You have already awarded this emoji, and it we\'ve removed it', 'alert') + flash = new Flash('You have already awarded this emoji, it has been removed', 'alert') flash.pinTo('.header-content') return diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 9000e0adf6..eb5137fe99 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -22,7 +22,7 @@ class Projects::NotesController < Projects::ApplicationController def create @note = Notes::CreateService.new(project, current_user, note_params).execute - @note = note.is_a?(AwardEmoji) ? @note.to_note_json : note_json(@note) + @note = @note.is_a?(AwardEmoji) ? @note.to_note_json : note_json(@note) respond_to do |format| format.json { render json: @note } @@ -63,7 +63,6 @@ class Projects::NotesController < Projects::ApplicationController def note @note ||= @project.notes.find(params[:id]) end - alias_method :awardable, :note def note_to_html(note) render_to_string( diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 38de0b442c..ac6c6fb25b 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -131,7 +131,7 @@ module IssuesHelper class: "icon emoji-icon emoji-#{unicode}", title: name, data: data - else + else # Emoji icons displayed separately, used for the awards already given # to an issue or merge request. content_tag :img, "", @@ -145,12 +145,9 @@ module IssuesHelper end def award_user_list(awards, current_user) - list = - awards.map do |award| - award.user == current_user ? "me" : award.user.name - end - - list.join(", ") + awards.map do |award| + award.user == current_user ? 'me' : award.user.name + end.join(', ') end def award_active_class(awards, current_user) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2cb3e8b017..e410febdff 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -36,7 +36,6 @@ class MergeRequest < ActiveRecord::Base include Referable include Sortable include Taskable - include Awardable belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" diff --git a/db/schema.rb b/db/schema.rb index 354d7390a5..7dc7b78ec6 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: 20160416190505) do +ActiveRecord::Schema.define(version: 20160421130527) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -77,7 +77,9 @@ ActiveRecord::Schema.define(version: 20160416190505) do t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" - t.boolean "repository_checks_enabled", default: true + t.boolean "repository_checks_enabled", default: false + t.integer "metrics_packet_size", default: 1 + t.text "shared_runners_text" end create_table "audit_events", force: :cascade do |t| @@ -182,14 +184,21 @@ ActiveRecord::Schema.define(version: 20160416190505) do t.text "yaml_errors" t.datetime "committed_at" t.integer "gl_project_id" + t.string "status" + t.datetime "started_at" + t.datetime "finished_at" + t.integer "duration" end + add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree + add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree + add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree create_table "ci_events", force: :cascade do |t| t.integer "project_id" @@ -433,6 +442,7 @@ ActiveRecord::Schema.define(version: 20160416190505) do t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" + t.date "due_date" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -442,6 +452,7 @@ ActiveRecord::Schema.define(version: 20160416190505) do add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} + add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree @@ -635,12 +646,14 @@ ActiveRecord::Schema.define(version: 20160416190505) do t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" + t.boolean "is_award", default: false, null: false end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree + add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree @@ -829,6 +842,7 @@ ActiveRecord::Schema.define(version: 20160416190505) do t.boolean "build_events", default: false, null: false t.string "category", default: "common", null: false t.boolean "default", default: false + t.boolean "wiki_page_events", default: true end add_index "services", ["category"], name: "index_services_on_category", using: :btree @@ -1023,6 +1037,7 @@ ActiveRecord::Schema.define(version: 20160416190505) do t.boolean "note_events", default: false, null: false t.boolean "enable_ssl_verification", default: true t.boolean "build_events", default: false, null: false + t.boolean "wiki_page_events", default: false, null: false end add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index fc12843ea5..78ddaee877 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do - issue = Issue.find_by(title: 'Release 0.4') - create_list(:upvote_note, 2, project: project, noteable: issue) - create(:downvote_note, project: project, noteable: issue) + awardable = Issue.find_by(title: 'Release 0.4') + create_list(:upvote, 2, project: project, awardable: awardable) + create(:downvote, project: project, awardable: awardable) end step 'issue "Tweet control" have 1 upvote and 2 downvotes' do issue = Issue.find_by(title: 'Tweet control') - create(:upvote_note, project: project, noteable: issue) - create_list(:downvote_note, 2, project: project, noteable: issue) + create(:upvote, project: project, noteable: issue) + create_list(:downvote, 2, project: project, noteable: issue) end step 'The list should be sorted by "Least popular"' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 4f883fe7c2..1d619a11d2 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do merge_request = MergeRequest.find_by(title: 'Bug NS-04') - create_list(:upvote_note, 2, project: project, noteable: merge_request) - create(:downvote_note, project: project, noteable: merge_request) + create_list(:upvote, 2, project: project, awardable: merge_request) + create(:downvote, project: project, awardable: merge_request) end step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do - merge_request = MergeRequest.find_by(title: 'Bug NS-06') - create(:upvote_note, project: project, noteable: merge_request) - create_list(:downvote_note, 2, project: project, noteable: merge_request) + awardable = MergeRequest.find_by(title: 'Bug NS-06') + create(:upvote, project: project, awardable: awardable) + create_list(:downvote, 2, project: project, awardable: awardable) end step 'The list should be sorted by "Least popular"' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index d6e4cd71ce..f7cb7ca8a4 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -211,4 +211,18 @@ describe Projects::IssuesController do end end end + + describe 'POST #toggle_award_emoji' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it "yields status code 200" do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: issue.iid, name: "thumbsup") + + expect(response.status).to eq(200) + end + end end diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb index a1173834b2..b09f8b0bc7 100644 --- a/spec/factories/award_emoji.rb +++ b/spec/factories/award_emoji.rb @@ -3,5 +3,16 @@ FactoryGirl.define do name "thumbsup" user awardable factory: :issue + + trait :thumbs_up + trait :upvote + + trait :thumbs_down do + name "thumbsdown" + end + + trait :downvote do + name "thumbsdown" + end end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 1ce0024e93..7da87c2d17 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -67,7 +67,7 @@ describe 'Issues', feature: true do describe 'Issue info' do it 'excludes award_emoji from comment count' do issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar') - create(:upvote_note, noteable: issue) + create(:award_emoji, awardable: issue) visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 389812ff7e..9e2bdd7f5b 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -8,7 +8,7 @@ describe 'Comments', feature: true do it 'excludes award_emoji from comment count' do merge_request = create(:merge_request) project = merge_request.source_project - create(:upvote_note, noteable: merge_request, project: project) + create(:award_emoji, awardable: merge_request, project: project) login_as :admin visit namespace_project_merge_requests_path(project.namespace, project) @@ -146,7 +146,7 @@ describe 'Comments', feature: true do describe 'comment info' do it 'excludes award_emoji from comment count' do - create(:upvote_note, noteable: merge_request, project: project) + create(:award_emoji, awardable: merge_request, project: project) visit namespace_project_merge_request_path(project.namespace, project, merge_request) diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb new file mode 100644 index 0000000000..6851d06836 --- /dev/null +++ b/spec/models/concerns/awardable_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Issue, "Awardable" do + let!(:issue) { create(:issue) } + let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) } + + describe "Associations" do + it { is_expected.to have_many(:award_emoji).dependent(:destroy) } + end + + describe "ClassMethods" do + let!(:issue2) { create(:issue) } + + before do + create(:award_emoji, awardable: issue2) + end + + it "orders on upvotes" do + expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue] + end + + it "orders on downvotes" do + expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2] + end + end + + describe "#upvotes" do + it "counts the number of upvotes" do + expect(issue.upvotes).to be 0 + end + end + + describe "#downvotes" do + it "counts the number of downvotes" do + expect(issue.downvotes).to be 1 + end + end + + describe "#toggle_award_emoji" do + it "adds an emoji if it isn't awarded yet" do + expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by 1 + end + + it "toggles already awarded emoji" do + + expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by -1 + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index d5435916ea..86ad9de883 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -12,6 +12,10 @@ describe Issue, "Issuable" do it { is_expected.to have_many(:todos).dependent(:destroy) } end + describe 'Included modules' do + it { is_expected.to include_module(Awardable) } + end + describe "Validation" do before do allow(subject).to receive(:set_iid).and_return(false) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8b2fb77e28..de1a233dff 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -93,6 +93,7 @@ describe User, models: true do it { is_expected.to have_one(:abuse_report) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } + it { is_expected.to have_many(:award_emoji).dependent(:destroy) } end describe 'validations' do diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index ff23f13e1c..93e8a7094e 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -14,7 +14,7 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } - + @note = Notes::CreateService.new(project, user, opts).execute end @@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do project.team << [user, :master] end - it "creates emoji note" do + it "creates an award emoji" do opts = { note: ':smile: ', noteable_type: 'Issue', noteable_id: issue.id } + note = Notes::CreateService.new(project, user, opts).execute - @note = Notes::CreateService.new(project, user, opts).execute - - expect(@note).to be_valid - expect(@note.note).to eq('smile') - expect(@note.is_award).to be_truthy + expect(note).to be_valid + expect(note.name).to eq('smile') end it "creates regular note if emoji name is invalid" do @@ -48,12 +46,10 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } + note = Notes::CreateService.new(project, user, opts).execute - @note = Notes::CreateService.new(project, user, opts).execute - - expect(@note).to be_valid - expect(@note.note).to eq(opts[:note]) - expect(@note.is_award).to be_falsy + expect(note).to be_valid + expect(note.note).to eq(opts[:note]) end end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 82b7fbfa81..455b19c89c 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -137,7 +137,6 @@ describe TodoService, services: true do let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) } let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } - let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') } let(:system_note) { create(:system_note, project: project, noteable: issue) } it 'mark related pending todos to the noteable for the note author as done' do @@ -150,13 +149,6 @@ describe TodoService, services: true do expect(second_todo.reload).to be_done end - it 'mark related pending todos to the noteable for the award note author as done' do - service.new_note(award_note, john_doe) - - expect(first_todo.reload).to be_done - expect(second_todo.reload).to be_done - end - it 'does not mark related pending todos it is a system note' do service.new_note(system_note, john_doe) @@ -286,6 +278,15 @@ describe TodoService, services: true do expect(second_todo.reload).to be_done end end + + describe '#new_award_emoji' do + it 'marks related pending todos to the target for the user as done' do + todo = create(:todo, user: john_doe, project: project, target: mr_assigned, author: author) + service.new_award_emoji(mr_assigned, john_doe) + + expect(todo.reload).to be_done + end + end end def should_create_todo(attributes = {}) diff --git a/spec/services/toggle_award_emoji_service_spec.rb b/spec/services/toggle_award_emoji_service_spec.rb new file mode 100644 index 0000000000..3d2f497fde --- /dev/null +++ b/spec/services/toggle_award_emoji_service_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe ToggleAwardEmoji, services: true do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + + before do + project.team << [user, :master] + end + + describe '#execute' do + it 'removes related todos' do + expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user) + + ToggleAwardEmojiService.new(project, user).execute(issue, "thumbsdown") + end + + it 'normalizes the emoji name' do + expect(issue).to receive(:toggle_award_emoji).with("thumbsup", user) + + ToggleAwardEmojiService.new(project, user).execute(issue, ":+1:") + end + + context 'when the emoji is set' do + it 'removes the emoji' do + create(:award_emoji, awardable: issue, user: user) + + expect { ToggleAwardEmojiService.new(project, user).execute(issue, ":+1:") }.to change { AwardEmoji.count }.by(-1) + end + end + + context 'when the award is not set yet' do + it 'awards the emoji' do + expect { ToggleAwardEmojiService.new(project, user).execute(issue, ":+1:") }.to change { AwardEmoji.count }.by(1) + end + end + end +end From d8c27e4e2b332cb9ece780bfb06155c3d1739d92 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 11 May 2016 22:33:27 +0200 Subject: [PATCH 031/507] merge awards-handler.coffee from another branch --- app/assets/javascripts/awards_handler.coffee | 97 ++++++++++---------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index c18c9984c1..043ad697df 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,26 +1,26 @@ class @AwardsHandler constructor: -> - @aliases = gl.emoji.emojiAliases() + @aliases = emojiAliases() $(document) .off "click", ".js-add-award" - .on "click", ".js-add-award", (e) => - e.stopPropagation() - e.preventDefault() + .on "click", ".js-add-award", (event) => + event.stopPropagation() + event.preventDefault() - @showEmojiMenu $(e.currentTarget) + @showEmojiMenu $(event.currentTarget) - $("html").on 'click', (e) -> - if !$(e.target).closest(".emoji-menu").length + $("html").on 'click', (event) -> + if !$(event.target).closest(".emoji-menu").length if $(".emoji-menu").is(":visible") $('.js-add-award.is-active').removeClass 'is-active' $(".emoji-menu").removeClass "is-visible" $(document) .off "click", ".js-emoji-btn" - .on "click", ".js-emoji-btn", @handleClick.bind(@) + .on "click", ".js-emoji-btn", @handleClick - handleClick: (e) -> + handleClick: (e) => e.preventDefault() $emojiBtn = $(e.currentTarget) $addAwardBtn = $('.js-add-award.is-active') @@ -31,27 +31,16 @@ class @AwardsHandler else if $votesBlock.length is 0 $votesBlock = $addAwardBtn.closest('.js-awards-block') - @currentVoteBlock = $votesBlock - + $votesBlock.addClass 'js-awards-block-current' awardUrl = $votesBlock.data 'award-url' emoji = $emojiBtn .find(".icon") .data "emoji" - - if emoji is "thumbsup" and @didUserClickEmoji $emojiBtn, "thumbsdown" - @addAward awardUrl, "thumbsdown" - - else if emoji is "thumbsdown" and @didUserClickEmoji $emojiBtn, "thumbsup" - @addAward awardUrl, "thumbsup" - @addAward awardUrl, emoji - didUserClickEmoji: (emojiBtn, emoji) -> - if emojiBtn.siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title") - emojiBtn.siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1 - showEmojiMenu: ($addBtn) -> $menu = $('.emoji-menu') + if $menu.length $holder = $addBtn.closest('.js-award-holder') @@ -60,7 +49,6 @@ class @AwardsHandler $menu.removeClass "is-visible" $("#emoji_search").blur() else - $(".emoji-menu").addClass "is-visible" $addBtn.addClass "is-active" @positionMenu($menu, $addBtn) @@ -77,6 +65,7 @@ class @AwardsHandler @positionMenu($menu, $addBtn) @renderFrequentlyUsedBlock() + setTimeout => $menu.addClass "is-visible" $("#emoji_search").focus() @@ -101,16 +90,18 @@ class @AwardsHandler $menu.css(css) addAward: (awardUrl, emoji) -> - emoji = @normilizeEmojiName(emoji) + emoji = @normalizeEmojiName(emoji) @postEmoji awardUrl, emoji, => @addAwardToEmojiBar(emoji) - $('.emoji-menu').removeClass 'is-visible' + $('.js-awards-block-current').removeClass 'js-awards-block-current' + + $(".emoji-menu").removeClass "is-visible" addAwardToEmojiBar: (emoji) -> @addEmojiToFrequentlyUsedList(emoji) - emoji = @normilizeEmojiName(emoji) + emoji = @normalizeEmojiName(emoji) $emojiBtn = @findEmojiIcon(emoji).parent() if $emojiBtn.length > 0 @@ -146,12 +137,16 @@ class @AwardsHandler $emojiBtn.removeClass("active") + if !isntNoteBody and $awardsBlock.children('.js-emoji-btn').length is 0 + # If this is a note body, we just hide the award emoji row like the initial state + $awardsBlock.addClass 'hidden' + removeMeFromUserList: ($emojiBtn, emoji) -> award_block = $emojiBtn authors = award_block .attr("data-original-title") .split(", ") - authors.splice(authors.indexOf("me"), 1) + authors.splice(authors.indexOf("me"),1) award_block .closest(".js-emoji-btn") .attr("data-original-title", authors.join(", ")) @@ -164,17 +159,18 @@ class @AwardsHandler if origTitle users = origTitle.split(', ') users.push("me") - award_block.attr("data-original-title", users.join(", ")) + award_block.attr("title", users.join(", ")) @resetTooltip(award_block) resetTooltip: (award) -> - award.tooltip('destroy') + award.tooltip("destroy") # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. setTimeout (-> award.tooltip() ), 200 + createEmoji: (emoji) -> emojiCssClass = @resolveNameToCssClass(emoji) @@ -194,13 +190,13 @@ class @AwardsHandler $currentBlock.removeClass 'hidden' resolveNameToCssClass: (emoji) -> - emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']") + emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") - if emojiIcon.length > 0 - unicodeName = emojiIcon.data('unicode-name') + if emoji_icon.length > 0 + unicodeName = emoji_icon.data("unicode-name") else # Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') + unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name") "emoji-#{unicodeName}" @@ -210,49 +206,52 @@ class @AwardsHandler callback.call() findEmojiIcon: (emoji) -> - @currentVoteBlock.find(".js-emoji-btn [data-emoji='#{emoji}']") + $(".js-awards-block-current.awards > .js-emoji-btn [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ scrollTop: $('.awards').offset().top - 80 }, 200) + normalizeEmojiName: (emoji) -> + @aliases[emoji] || emoji + addEmojiToFrequentlyUsedList: (emoji) -> - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - frequentlyUsedEmojis.push(emoji) - $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }) + frequently_used_emojis = @getFrequentlyUsedEmojis() + frequently_used_emojis.push(emoji) + $.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 }) getFrequentlyUsedEmojis: -> - frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',') - _.compact(_.uniq(frequentlyUsedEmojis)) + frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",") + _.compact(_.uniq(frequently_used_emojis)) renderFrequentlyUsedBlock: -> if $.cookie('frequently_used_emojis') - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() + frequently_used_emojis = @getFrequentlyUsedEmojis() ul = $("
        ") for emoji in frequently_used_emojis $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) - $('input.emoji-search').after(ul).after($('
        ').text('Frequently used')) + $("input.emoji-search").after(ul).after($("
        ").text("Frequently used")) setupSearch: -> - $('input.emoji-search').keyup (ev) => + $("input.emoji-search").on 'keyup', (ev) => term = $(ev.target).val() # Clean previous search results - $('ul.emoji-menu-search, h5.emoji-search').remove() + $("ul.emoji-menu-search, h5.emoji-search").remove() if term # Generate a search result block - h5 = $('
        ').text('Search results').addClass('emoji-search') - foundEmojis = @searchEmojis(term).show() - ul = $('
          ').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis) - $('.emoji-menu-content ul, .emoji-menu-content h5').hide() - $('.emoji-menu-content').append(h5).append(ul) + h5 = $("
          ").text("Search results").addClass("emoji-search") + found_emojis = @searchEmojis(term).show() + ul = $("
            ").addClass("emoji-menu-list emoji-menu-search").append(found_emojis) + $(".emoji-menu-content ul, .emoji-menu-content h5").hide() + $(".emoji-menu-content").append(h5).append(ul) else - $('.emoji-menu-content').children().show() + $(".emoji-menu-content").children().show() searchEmojis: (term)-> $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() From 4558b5b9fe9f648903ad0dc01089e6118fe0af34 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 11 May 2016 22:43:58 +0200 Subject: [PATCH 032/507] Incorporate feedback --- app/models/concerns/awardable.rb | 10 +++--- app/models/note.rb | 4 --- app/models/user.rb | 2 +- app/services/notes/create_service.rb | 2 +- app/services/toggle_award_emoji_service.rb | 12 ------- app/views/award_emoji/_awards_block.html.haml | 4 +-- app/views/projects/issues/_issue.html.haml | 14 +++----- .../merge_requests/_merge_request.html.haml | 14 +++----- ...82152_convert_award_note_to_emoji_award.rb | 10 ------ lib/api/entities.rb | 3 +- .../projects/issues_controller_spec.rb | 6 ++-- spec/features/issues/award_emoji_spec.rb | 2 -- spec/models/concerns/issuable_spec.rb | 35 ++----------------- spec/requests/api/issues_spec.rb | 1 - spec/requests/api/merge_requests_spec.rb | 1 - 15 files changed, 23 insertions(+), 97 deletions(-) diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index b4e3e9eb3d..aa4b420125 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -34,23 +34,23 @@ module Awardable end end - def grouped_awards(with_thumbs = true) + def grouped_awards(with_thumbs: true) awards = award_emoji.group_by(&:name) if with_thumbs - awards[AwardEmoji::UPVOTE_NAME] ||= AwardEmoji.none - awards[AwardEmoji::DOWNVOTE_NAME] ||= AwardEmoji.none + awards[AwardEmoji::UPVOTE_NAME] ||= [] + awards[AwardEmoji::DOWNVOTE_NAME] ||= [] end awards end def downvotes - award_emoji.where(name: AwardEmoji::DOWNVOTE_NAME).count + award_emoji.downvotes.count end def upvotes - award_emoji.where(name: AwardEmoji::UPVOTE_NAME).count + award_emoji.upvotes.count end def emoji_awardable? diff --git a/app/models/note.rb b/app/models/note.rb index 0b038edb47..f9040f9f36 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -323,10 +323,6 @@ class Note < ActiveRecord::Base award_emoji_supported? && contains_emoji_only? end - def create_award_emoji - self.noteable.award_emoji(award_emoji_name, author) - end - def clear_blank_line_code! self.line_code = nil if self.line_code.blank? end diff --git a/app/models/user.rb b/app/models/user.rb index 5ca53e7c64..5284721261 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -79,7 +79,7 @@ class User < ActiveRecord::Base has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy - has_many :award_emoji, as: :awardable, dependent: :destroy + has_many :award_emoji, as: :awardable, dependent: :destroy # # Validations diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index da2a774b70..bbf7889166 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -7,7 +7,7 @@ module Notes if note.award_emoji? return ToggleAwardEmojiService.new(project, current_user, params). - execute(note.noteable, note.note) + execute(note.award_emoji_name, note.note) end return unless valid_project?(note) diff --git a/app/services/toggle_award_emoji_service.rb b/app/services/toggle_award_emoji_service.rb index b77b4e79bf..1820f57f56 100644 --- a/app/services/toggle_award_emoji_service.rb +++ b/app/services/toggle_award_emoji_service.rb @@ -1,21 +1,9 @@ require_relative 'base_service' class ToggleAwardEmojiService < BaseService - # For an award emoji being posted we should: - # - Mark the TODO as done for this issuable (skip on snippets) - # - Save the award emoji def execute(awardable, emoji) todo_service.new_award_emoji(awardable, current_user) - # Needed if its posted as a note containing only :+1: - emoji = award_emoji_name(emoji) if emoji.start_with? ':' awardable.toggle_award_emoji(emoji, current_user) end - - private - - def award_emoji_name(emoji) - original_name = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] - Gitlab::AwardEmoji.normalize_emoji_name(original_name) - end end diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index b57c9afcbd..86931c70f3 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,7 +1,7 @@ -- grouped_emojis = awardable.grouped_awards(inline) +- grouped_emojis = awardable.grouped_awards(with_thumbs: inline) .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.size == 0), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } - awards_sort(grouped_emojis).each do |emoji, awards| - %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)),data: { placement: "bottom", original_title: award_user_list(awards, current_user)} } + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)),data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 57ad2ec185..dec3e8809c 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -28,16 +28,10 @@ = downvotes - note_count = issue.notes.user.count - - if note_count > 0 - %li - = link_to issue_path(issue) + "#notes" do - = icon('comments') - = note_count - - else - %li - = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do - = icon('comments') - = note_count + %li + = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do + = icon('comments') + = note_count .issue-info #{issue.to_reference} · diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 391193eed6..eaee518017 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -36,16 +36,10 @@ = downvotes - note_count = merge_request.mr_and_commit_notes.user.count - - if note_count > 0 - %li - = link_to merge_request_path(merge_request) + "#notes" do - = icon('comments') - = note_count - - else - %li - = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do - = icon('comments') - = note_count + %li + = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do + = icon('comments') + = note_count .merge-request-info #{merge_request.to_reference} · diff --git a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb index 76f4a3aa6a..d2efbd0abe 100644 --- a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb +++ b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb @@ -3,15 +3,5 @@ class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration def up execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)" end - - def down - execute <<-SQL - INSERT INTO notes (noteable_type, noteable_id, author_id, note, created_at, updated_at, is_award) - (SELECT awardable_type, awardable_id, user_id, name, created_at, updated_at, TRUE - FROM award_emoji - WHERE awardable_type IN ('Issue', 'MergeRequest') - ) - SQL - end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index f91ca9d580..c210cfe513 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -174,7 +174,7 @@ module API expose :subscribed do |issue, options| issue.subscribed?(options[:current_user]) end - expose :user_notes_count + expose :upvotes, :downvotes end class MergeRequest < ProjectEntity @@ -191,7 +191,6 @@ module API expose :subscribed do |merge_request, options| merge_request.subscribed?(options[:current_user]) end - expose :user_notes_count end class MergeRequestChanges < MergeRequest diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 30d296fdad..706f538f26 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -257,9 +257,9 @@ describe Projects::IssuesController do project.team << [user, :developer] end - it "yields status code 200" do - post(:toggle_award_emoji, namespace_id: project.namespace.path, - project_id: project.path, id: issue.iid, name: "thumbsup") + it "toggles the award emoji" do + expect { post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: issue.iid, name: "thumbsup") }.to change { AwardEmoji.count }.by(1) expect(response.status).to eq(200) end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 41af789aae..07a854ea01 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do end context 'click the thumbsup emoji' do - it 'should increment the thumbsup emoji', js: true do find('[data-emoji="thumbsup"]').click sleep 2 @@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do end context 'click the thumbsdown emoji' do - it 'should increment the thumbsdown emoji', js: true do find('[data-emoji="thumbsdown"]').click sleep 2 diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 568bf4c932..ebc3968023 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -203,11 +203,10 @@ describe Issue, "Issuable" do end end - # TODO ZJ describe "votes" do before do - create!(:award_emoji, :upvote, awardable: issue) - create!(:award_emoji, :downvote, awardable: issue) + create(:award_emoji, :upvote, awardable: issue) + create(:award_emoji, :downvote, awardable: issue) end it "returns correct values" do @@ -215,34 +214,4 @@ describe Issue, "Issuable" do expect(issue.downvotes).to eq(1) end end - - describe ".with_label" do - let(:project) { create(:project, :public) } - let(:bug) { create(:label, project: project, title: 'bug') } - let(:feature) { create(:label, project: project, title: 'feature') } - let(:enhancement) { create(:label, project: project, title: 'enhancement') } - let(:issue1) { create(:issue, title: "Bugfix1", project: project) } - let(:issue2) { create(:issue, title: "Bugfix2", project: project) } - let(:issue3) { create(:issue, title: "Feature1", project: project) } - - before(:each) do - issue1.labels << bug - issue1.labels << feature - issue2.labels << bug - issue2.labels << enhancement - issue3.labels << feature - end - - it 'finds the correct issue containing just enhancement label' do - expect(Issue.with_label(enhancement.title)).to match_array([issue2]) - end - - it 'finds the correct issues containing the same label' do - expect(Issue.with_label(bug.title)).to match_array([issue1, issue2]) - end - - it 'finds the correct issues containing only both labels' do - expect(Issue.with_label([bug.title, enhancement.title])).to match_array([issue2]) - end - end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9dd43f4fab..7bc7e319ff 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -249,7 +249,6 @@ describe API::API, api: true do expect(json_response['milestone']).to be_a Hash expect(json_response['assignee']).to be_a Hash expect(json_response['author']).to be_a Hash - expect(json_response['user_notes_count']).to be(1) end it "should return a project issue by id" do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 4b0111df14..6392bdb1c9 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -138,7 +138,6 @@ describe API::API, api: true do expect(json_response['work_in_progress']).to be_falsy expect(json_response['merge_when_build_succeeds']).to be_falsy expect(json_response['merge_status']).to eq('can_be_merged') - expect(json_response['user_notes_count']).to be(2) end it "should return merge_request" do From 2bbe781d8b45fb9677f5fbe60cf86b2452ef3af4 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 11 May 2016 22:49:47 +0200 Subject: [PATCH 033/507] revert front end changes --- app/assets/javascripts/awards_handler.coffee | 298 ++++++++---------- app/assets/javascripts/dispatcher.js.coffee | 2 - .../lib/emoji_aliases.js.coffee.erb | 9 - app/assets/javascripts/notes.js.coffee | 6 +- app/assets/stylesheets/pages/awards.scss | 13 +- app/assets/stylesheets/pages/notes.scss | 41 +-- 6 files changed, 140 insertions(+), 229 deletions(-) delete mode 100644 app/assets/javascripts/lib/emoji_aliases.js.coffee.erb diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 043ad697df..bf95e06b4e 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,169 +1,126 @@ class @AwardsHandler - constructor: -> - @aliases = emojiAliases() + constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) -> + $('.js-add-award').on 'click', (event) => + event.stopPropagation() + event.preventDefault() - $(document) - .off "click", ".js-add-award" - .on "click", ".js-add-award", (event) => - event.stopPropagation() - event.preventDefault() + @showEmojiMenu() - @showEmojiMenu $(event.currentTarget) + $('html').on 'click', (event) -> + if !$(event.target).closest('.emoji-menu').length + if $('.emoji-menu').is(':visible') + $('.emoji-menu').removeClass 'is-visible' - $("html").on 'click', (event) -> - if !$(event.target).closest(".emoji-menu").length - if $(".emoji-menu").is(":visible") - $('.js-add-award.is-active').removeClass 'is-active' - $(".emoji-menu").removeClass "is-visible" + $('.awards') + .off 'click' + .on 'click', '.js-emoji-btn', @handleClick - $(document) - .off "click", ".js-emoji-btn" - .on "click", ".js-emoji-btn", @handleClick + @renderFrequentlyUsedBlock() - handleClick: (e) => + handleClick: (e) -> e.preventDefault() - $emojiBtn = $(e.currentTarget) - $addAwardBtn = $('.js-add-award.is-active') - $votesBlock = $($addAwardBtn.closest('.js-award-holder').data('target')) + emoji = $(this) + .find('.icon') + .data 'emoji' - if $addAwardBtn.length is 0 - $votesBlock = $emojiBtn.closest('.js-awards-block') - else if $votesBlock.length is 0 - $votesBlock = $addAwardBtn.closest('.js-awards-block') + if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown' + awardsHandler.addAward 'thumbsdown' - $votesBlock.addClass 'js-awards-block-current' - awardUrl = $votesBlock.data 'award-url' - emoji = $emojiBtn - .find(".icon") - .data "emoji" - @addAward awardUrl, emoji + else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup' + awardsHandler.addAward 'thumbsup' - showEmojiMenu: ($addBtn) -> - $menu = $('.emoji-menu') + awardsHandler.addAward emoji - if $menu.length - $holder = $addBtn.closest('.js-award-holder') + $(this).trigger 'blur' - if $menu.is ".is-visible" - $addBtn.removeClass "is-active" - $menu.removeClass "is-visible" - $("#emoji_search").blur() + didUserClickEmoji: (that, emoji) -> + if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title') + $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1 + + showEmojiMenu: -> + if $('.emoji-menu').length + if $('.emoji-menu').is '.is-visible' + $('.emoji-menu').removeClass 'is-visible' + $('#emoji_search').blur() else - $addBtn.addClass "is-active" - @positionMenu($menu, $addBtn) - - $menu.addClass "is-visible" - $("#emoji_search").focus() + $('.emoji-menu').addClass 'is-visible' + $('#emoji_search').focus() else - $addBtn.addClass "is-loading is-active" - $.get $addBtn.data('award-menu-url'), (response) => - $addBtn.removeClass "is-loading" - $('body').append response - - $menu = $(".emoji-menu") - - @positionMenu($menu, $addBtn) - - @renderFrequentlyUsedBlock() - + $('.js-add-award').addClass 'is-loading' + $.get @getEmojisUrl, (response) => + $('.js-add-award').removeClass 'is-loading' + $('.js-award-holder').append response setTimeout => - $menu.addClass "is-visible" - $("#emoji_search").focus() + $('.emoji-menu').addClass 'is-visible' + $('#emoji_search').focus() @setupSearch() , 200 - positionMenu: ($menu, $addBtn) -> - position = $addBtn.data('position') - - # The menu could potentially be off-screen or in a hidden overflow element - # So we position the element absolute in the body - css = - top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px" - - if position? and position is 'right' - css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px" - $menu.addClass "is-aligned-right" - else - css.left = "#{$addBtn.offset().left}px" - $menu.removeClass "is-aligned-right" - - $menu.css(css) - - addAward: (awardUrl, emoji) -> - emoji = @normalizeEmojiName(emoji) - @postEmoji awardUrl, emoji, => + addAward: (emoji) -> + @postEmoji emoji, => @addAwardToEmojiBar(emoji) - $('.js-awards-block-current').removeClass 'js-awards-block-current' - - $(".emoji-menu").removeClass "is-visible" + $('.emoji-menu').removeClass 'is-visible' addAwardToEmojiBar: (emoji) -> @addEmojiToFrequentlyUsedList(emoji) - emoji = @normalizeEmojiName(emoji) - $emojiBtn = @findEmojiIcon(emoji).parent() - - if $emojiBtn.length > 0 - if @isActive($emojiBtn) - @decrementCounter($emojiBtn, emoji) + if @exist(emoji) + if @isActive(emoji) + @decrementCounter(emoji) else - $counter = $emojiBtn.find('.js-counter') - $counter.text(parseInt($counter.text()) + 1) - $emojiBtn.addClass("active") - @addMeToUserList(emoji) + counter = @findEmojiIcon(emoji).siblings('.js-counter') + counter.text(parseInt(counter.text()) + 1) + counter.parent().addClass('active') + @addMeToAuthorList(emoji) else @createEmoji(emoji) - isActive: ($emojiBtn) -> - $emojiBtn.hasClass("active") + exist: (emoji) -> + @findEmojiIcon(emoji).length > 0 - decrementCounter: ($emojiBtn, emoji) -> - $awardsBlock = $emojiBtn.closest('.js-awards-block') - isntNoteBody = $emojiBtn.closest('.note-body').length is 0 - counter = $('.js-counter', $emojiBtn) - counterNumber = parseInt(counter.text()) + isActive: (emoji) -> + @findEmojiIcon(emoji).parent().hasClass('active') - if counterNumber > 1 - counter.text(counterNumber - 1) - @removeMeFromUserList($emojiBtn, emoji) - else if (emoji == "thumbsup" || emoji == "thumbsdown") && isntNoteBody - $emojiBtn.tooltip("destroy") - counter.text('0') - @removeMeFromUserList($emojiBtn, emoji) + decrementCounter: (emoji) -> + counter = @findEmojiIcon(emoji).siblings('.js-counter') + emojiIcon = counter.parent() + if parseInt(counter.text()) > 1 + counter.text(parseInt(counter.text()) - 1) + emojiIcon.removeClass('active') + @removeMeFromAuthorList(emoji) + else if emoji == 'thumbsup' || emoji == 'thumbsdown' + emojiIcon.tooltip('destroy') + counter.text(0) + emojiIcon.removeClass('active') + @removeMeFromAuthorList(emoji) else - $emojiBtn.tooltip("destroy") - $emojiBtn.remove() + emojiIcon.tooltip('destroy') + emojiIcon.remove() - $emojiBtn.removeClass("active") + removeMeFromAuthorList: (emoji) -> + awardBlock = @findEmojiIcon(emoji).parent() + authors = awardBlock + .attr('data-original-title') + .split(', ') + authors.splice(authors.indexOf('me'),1) + awardBlock + .closest('.js-emoji-btn') + .attr('data-original-title', authors.join(', ')) + @resetTooltip(awardBlock) - if !isntNoteBody and $awardsBlock.children('.js-emoji-btn').length is 0 - # If this is a note body, we just hide the award emoji row like the initial state - $awardsBlock.addClass 'hidden' - - removeMeFromUserList: ($emojiBtn, emoji) -> - award_block = $emojiBtn - authors = award_block - .attr("data-original-title") - .split(", ") - authors.splice(authors.indexOf("me"),1) - award_block - .closest(".js-emoji-btn") - .attr("data-original-title", authors.join(", ")) - @resetTooltip(award_block) - - addMeToUserList: (emoji) -> - award_block = @findEmojiIcon(emoji).parent() - origTitle = award_block.attr("data-original-title").trim() - users = [] + addMeToAuthorList: (emoji) -> + awardBlock = @findEmojiIcon(emoji).parent() + origTitle = awardBlock.attr('data-original-title').trim() + authors = [] if origTitle - users = origTitle.split(', ') - users.push("me") - award_block.attr("title", users.join(", ")) - @resetTooltip(award_block) + authors = origTitle.split(', ') + authors.push('me') + awardBlock.attr('data-original-title', authors.join(', ')) + @resetTooltip(awardBlock) resetTooltip: (award) -> - award.tooltip("destroy") + award.tooltip('destroy') # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. setTimeout (-> @@ -174,84 +131,85 @@ class @AwardsHandler createEmoji: (emoji) -> emojiCssClass = @resolveNameToCssClass(emoji) - buttonHtml = "" + nodes = [] + nodes.push( + "" + ) - emoji_node = $(buttonHtml) - .insertBefore(".js-awards-block-current .js-award-holder:not(.js-award-action-btn)") - .find(".emoji-icon") - .data("emoji", emoji) + $(nodes.join("\n")) + .insertBefore('.js-award-holder') + .find('.emoji-icon') + .data('emoji', emoji) $('.award-control').tooltip() - $currentBlock = $('.js-awards-block-current') - if $currentBlock.is('.hidden') - $currentBlock.removeClass 'hidden' - resolveNameToCssClass: (emoji) -> - emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") + emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']") - if emoji_icon.length > 0 - unicodeName = emoji_icon.data("unicode-name") + if emojiIcon.length > 0 + unicodeName = emojiIcon.data('unicode-name') else # Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name") + unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') "emoji-#{unicodeName}" - postEmoji: (awardUrl, emoji, callback) -> - $.post awardUrl, { name: emoji }, (data) -> + postEmoji: (emoji, callback) -> + $.post @postEmojiUrl, { note: { + note: ":#{emoji}:" + noteable_type: @noteableType + noteable_id: @noteableId + }},(data) -> if data.ok callback.call() findEmojiIcon: (emoji) -> - $(".js-awards-block-current.awards > .js-emoji-btn [data-emoji='#{emoji}']") + $(".awards > .js-emoji-btn [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ scrollTop: $('.awards').offset().top - 80 }, 200) - normalizeEmojiName: (emoji) -> - @aliases[emoji] || emoji - addEmojiToFrequentlyUsedList: (emoji) -> - frequently_used_emojis = @getFrequentlyUsedEmojis() - frequently_used_emojis.push(emoji) - $.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 }) + frequentlyUsedEmojis = @getFrequentlyUsedEmojis() + frequentlyUsedEmojis.push(emoji) + $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }) getFrequentlyUsedEmojis: -> - frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",") - _.compact(_.uniq(frequently_used_emojis)) + frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',') + _.compact(_.uniq(frequentlyUsedEmojis)) renderFrequentlyUsedBlock: -> if $.cookie('frequently_used_emojis') - frequently_used_emojis = @getFrequentlyUsedEmojis() + frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - ul = $("
              ") + ul = $('
                ') - for emoji in frequently_used_emojis - $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) + for emoji in frequentlyUsedEmojis + do (emoji) -> + $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) - $("input.emoji-search").after(ul).after($("
                ").text("Frequently used")) + $('input.emoji-search').after(ul).after($('
                ').text('Frequently used')) setupSearch: -> - $("input.emoji-search").on 'keyup', (ev) => + $('input.emoji-search').keyup (ev) => term = $(ev.target).val() # Clean previous search results - $("ul.emoji-menu-search, h5.emoji-search").remove() + $('ul.emoji-menu-search, h5.emoji-search').remove() if term # Generate a search result block - h5 = $("
                ").text("Search results").addClass("emoji-search") - found_emojis = @searchEmojis(term).show() - ul = $("
                  ").addClass("emoji-menu-list emoji-menu-search").append(found_emojis) - $(".emoji-menu-content ul, .emoji-menu-content h5").hide() - $(".emoji-menu-content").append(h5).append(ul) + h5 = $('
                  ').text('Search results').addClass('emoji-search') + foundEmojis = @searchEmojis(term).show() + ul = $('
                    ').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis) + $('.emoji-menu-content ul, .emoji-menu-content h5').hide() + $('.emoji-menu-content').append(h5).append(ul) else - $(".emoji-menu-content").children().show() + $('.emoji-menu-content').children().show() searchEmojis: (term)-> $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index e00ca2984b..f91aa3c5ad 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -23,7 +23,6 @@ class Dispatcher new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() - awards_handler = new AwardsHandler() when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() when 'dashboard:todos:index' @@ -54,7 +53,6 @@ class Dispatcher new Diff() shortcut_handler = new ShortcutsIssuable(true) new ZenMode() - awards_handler = new AwardsHandler() when "projects:merge_requests:diffs" new Diff() new ZenMode() diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb deleted file mode 100644 index e005ab3684..0000000000 --- a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb +++ /dev/null @@ -1,9 +0,0 @@ -((w) -> - - w.gl ?= {} - w.gl.emoji ?= {} - - w.gl.emoji.emojiAliases = -> - JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') - -) window \ No newline at end of file diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index e8a92b8012..efb3e8e219 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -162,13 +162,13 @@ class @Notes renderNote: (note) -> unless note.valid if note.award - flash = new Flash('You have already awarded this emoji, it has been removed', 'alert') + flash = new Flash('You have already used this award emoji!', 'alert') flash.pinTo('.header-content') return if note.award - awards_handler.addAwardToEmojiBar(note.name) - awards_handler.scrollToAwards() + awardsHandler.addAwardToEmojiBar(note.note) + awardsHandler.scrollToAwards() # render note if it not present in loaded list # or skip if rendered diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 07d40f4055..37bf38fa65 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -1,4 +1,6 @@ .awards { + line-height: 34px; + .emoji-icon { width: 20px; height: 20px; @@ -7,6 +9,8 @@ .emoji-menu { position: absolute; + top: 100%; + left: 0; margin-top: 3px; z-index: 1000; min-width: 160px; @@ -19,12 +23,7 @@ opacity: 0; transform: scale(.2); transform-origin: 0 -45px; - transition: .3s cubic-bezier(.87,-.41,.19,1.44); - transition-property: transform, opacity; - - &.is-aligned-right { - transform-origin: 100% -45px; - } + transition: all .3s cubic-bezier(.87,-.41,.19,1.44); &.is-visible { pointer-events: all; @@ -108,7 +107,7 @@ } &.is-loading { - .award-control-icon-normal { + .award-control-icon { display: none; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 8ad47a5054..624c8249f7 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -63,8 +63,7 @@ ul.notes { &.is-editting { .note-header, .note-text, - .edited-text, - .note-awards { + .edited-text { display: none; } @@ -74,6 +73,8 @@ ul.notes { } .note-body { + overflow: auto; + .note-text { overflow: auto; word-wrap: break-word; @@ -323,42 +324,6 @@ ul.notes { } } -.note-award-control { - display: block; - - &:hover, - &:focus { - text-decoration: none; - } - - .award-control-icon-loading { - display: none; - } - - &.is-loading { - .award-control-icon-normal { - display: none; - } - - .award-control-icon-loading { - display: block; - } - } -} - -.note-awards { - .awards { - padding-top: 10px; - } - - .award-control { - padding-top: 2px; - padding-bottom: 2px; - color: #8f8f8f; - font-size: 13px; - } -} - .disabled-comment { margin-left: -$gl-padding-top; margin-right: -$gl-padding-top; From 7a4e7ad04e1fc96953d9159e8e1a2208990d34f7 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 12 May 2016 09:23:21 +0200 Subject: [PATCH 034/507] Fix tests and wrong choices during merge --- app/models/concerns/issuable.rb | 8 +++++ app/models/note.rb | 2 +- app/services/notes/create_service.rb | 2 +- .../projects/issues_controller_spec.rb | 6 ++-- spec/models/concerns/issuable_spec.rb | 30 +++++++++++++++++++ spec/services/notes/create_service_spec.rb | 12 ++++++++ .../toggle_award_emoji_service_spec.rb | 10 ++----- 7 files changed, 58 insertions(+), 12 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 9b77b88ca8..83d3bfe8e1 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -100,6 +100,14 @@ module Issuable order_by(method) end end + + def with_label(title) + if title.is_a?(Array) && title.size > 1 + joins(:labels).where(labels: { title: title }).group(arel_table[:id]).having("COUNT(DISTINCT labels.title) = #{title.size}") + else + joins(:labels).where(labels: { title: title }) + end + end end def today? diff --git a/app/models/note.rb b/app/models/note.rb index f9040f9f36..7235274770 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -343,6 +343,6 @@ class Note < ActiveRecord::Base def award_emoji_name original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] - Gitlab::AwardEmoji.normilize_emoji_name(original_name) + Gitlab::AwardEmoji.normalize_emoji_name(original_name) end end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index bbf7889166..509deb898b 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -7,7 +7,7 @@ module Notes if note.award_emoji? return ToggleAwardEmojiService.new(project, current_user, params). - execute(note.award_emoji_name, note.note) + execute(note.noteable, note.award_emoji_name) end return unless valid_project?(note) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 706f538f26..13d9a49878 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -258,8 +258,10 @@ describe Projects::IssuesController do end it "toggles the award emoji" do - expect { post(:toggle_award_emoji, namespace_id: project.namespace.path, - project_id: project.path, id: issue.iid, name: "thumbsup") }.to change { AwardEmoji.count }.by(1) + expect do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: issue.iid, name: "thumbsup") + end.to change { AwardEmoji.count }.by(1) expect(response.status).to eq(200) end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index ebc3968023..424b6f5e27 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -214,4 +214,34 @@ describe Issue, "Issuable" do expect(issue.downvotes).to eq(1) end end + + describe ".with_label" do + let(:project) { create(:project, :public) } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:feature) { create(:label, project: project, title: 'feature') } + let(:enhancement) { create(:label, project: project, title: 'enhancement') } + let(:issue1) { create(:issue, title: "Bugfix1", project: project) } + let(:issue2) { create(:issue, title: "Bugfix2", project: project) } + let(:issue3) { create(:issue, title: "Feature1", project: project) } + + before(:each) do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + issue2.labels << enhancement + issue3.labels << feature + end + + it 'finds the correct issue containing just enhancement label' do + expect(Issue.with_label(enhancement.title)).to match_array([issue2]) + end + + it 'finds the correct issues containing the same label' do + expect(Issue.with_label(bug.title)).to match_array([issue1, issue2]) + end + + it 'finds the correct issues containing only both labels' do + expect(Issue.with_label([bug.title, enhancement.title])).to match_array([issue2]) + end + end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 93e8a7094e..4e62e3975e 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -51,5 +51,17 @@ describe Notes::CreateService, services: true do expect(note).to be_valid expect(note.note).to eq(opts[:note]) end + + it "normalizes the emoji name" do + opts = { + note: ':+1:', + noteable_type: 'Issue', + noteable_id: issue.id + } + + expect_any_instance_of(ToggleAwardEmojiService).to receive(:execute).with(issue, "thumbsup") + + Notes::CreateService.new(project, user, opts).execute + end end end diff --git a/spec/services/toggle_award_emoji_service_spec.rb b/spec/services/toggle_award_emoji_service_spec.rb index 3d2f497fde..b9d63bd70a 100644 --- a/spec/services/toggle_award_emoji_service_spec.rb +++ b/spec/services/toggle_award_emoji_service_spec.rb @@ -16,23 +16,17 @@ describe ToggleAwardEmoji, services: true do ToggleAwardEmojiService.new(project, user).execute(issue, "thumbsdown") end - it 'normalizes the emoji name' do - expect(issue).to receive(:toggle_award_emoji).with("thumbsup", user) - - ToggleAwardEmojiService.new(project, user).execute(issue, ":+1:") - end - context 'when the emoji is set' do it 'removes the emoji' do create(:award_emoji, awardable: issue, user: user) - expect { ToggleAwardEmojiService.new(project, user).execute(issue, ":+1:") }.to change { AwardEmoji.count }.by(-1) + expect { ToggleAwardEmojiService.new(project, user).execute(issue, "thumbsup") }.to change { AwardEmoji.count }.by(-1) end end context 'when the award is not set yet' do it 'awards the emoji' do - expect { ToggleAwardEmojiService.new(project, user).execute(issue, ":+1:") }.to change { AwardEmoji.count }.by(1) + expect { ToggleAwardEmojiService.new(project, user).execute(issue, "thumbsup") }.to change { AwardEmoji.count }.by(1) end end end From e0cabb67d0492907e6cef21bb0ef21a6e953b70b Mon Sep 17 00:00:00 2001 From: ZJ van de Weg Date: Tue, 17 May 2016 12:28:17 -0500 Subject: [PATCH 035/507] Fix latests concerns --- .../concerns/toggle_award_emoji.rb | 2 ++ app/controllers/projects/notes_controller.rb | 13 +++++--- app/models/award_emoji.rb | 9 ----- app/services/notes/create_service.rb | 6 ++-- app/services/toggle_award_emoji_service.rb | 9 ----- .../merge_requests/_merge_request.html.haml | 6 ++-- .../merge_requests/_merge_requests.html.haml | 1 - spec/features/notes_on_merge_requests_spec.rb | 8 ++--- spec/services/notes/create_service_spec.rb | 2 +- .../toggle_award_emoji_service_spec.rb | 33 ------------------- 10 files changed, 23 insertions(+), 66 deletions(-) delete mode 100644 app/services/toggle_award_emoji_service.rb delete mode 100644 spec/services/toggle_award_emoji_service_spec.rb diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 9cd522d1c3..09ff44f291 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -7,7 +7,9 @@ module ToggleAwardEmoji def toggle_award_emoji name = params.require(:name) + awardable.toggle_award_emoji(name, current_user) + TodoService.new.new_award_emoji(awardable, current_user) render json: { ok: true } end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index eb5137fe99..b097f609c6 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -22,10 +22,8 @@ class Projects::NotesController < Projects::ApplicationController def create @note = Notes::CreateService.new(project, current_user, note_params).execute - @note = @note.is_a?(AwardEmoji) ? @note.to_note_json : note_json(@note) - respond_to do |format| - format.json { render json: @note } + format.json { render json: note_json(@note) } format.html { redirect_back_or_default } end end @@ -109,7 +107,14 @@ class Projects::NotesController < Projects::ApplicationController end def note_json(note) - if note.valid? + if note.is_a?(AwardEmoji) + { + valid: note.valid?, + award: true, + id: note.id, + name: note.name + } + elsif note.valid? { valid: true, id: note.id, diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 44a9b55a8a..59c7d87f5d 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -23,13 +23,4 @@ class AwardEmoji < ActiveRecord::Base def upvote? self.name == UPVOTE_NAME end - - def to_note_json - { - valid: valid?, - award: true, - id: id, - name: name - } - end end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 509deb898b..44ff96f9bd 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -6,8 +6,10 @@ module Notes note.system = false if note.award_emoji? - return ToggleAwardEmojiService.new(project, current_user, params). - execute(note.noteable, note.award_emoji_name) + noteable = note.noteable + todo_service.new_award_emoji(noteable, current_user) + + return noteable.create_award_emoji(note.award_emoji_name, current_user) end return unless valid_project?(note) diff --git a/app/services/toggle_award_emoji_service.rb b/app/services/toggle_award_emoji_service.rb deleted file mode 100644 index 1820f57f56..0000000000 --- a/app/services/toggle_award_emoji_service.rb +++ /dev/null @@ -1,9 +0,0 @@ -require_relative 'base_service' - -class ToggleAwardEmojiService < BaseService - def execute(awardable, emoji) - todo_service.new_award_emoji(awardable, current_user) - - awardable.toggle_award_emoji(emoji, current_user) - end -end diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index eaee518017..bcc6c4f2d5 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -37,9 +37,9 @@ - note_count = merge_request.mr_and_commit_notes.user.count %li - = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do - = icon('comments') - = note_count + = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do + = icon('comments') + = note_count .merge-request-info #{merge_request.to_reference} · diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 5473fa1916..446887774a 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -6,4 +6,3 @@ - if @merge_requests.present? = paginate @merge_requests, theme: "gitlab" - diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 9e2bdd7f5b..c93497970d 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -8,12 +8,12 @@ describe 'Comments', feature: true do it 'excludes award_emoji from comment count' do merge_request = create(:merge_request) project = merge_request.source_project - create(:award_emoji, awardable: merge_request, project: project) + create(:award_emoji, awardable: merge_request) login_as :admin visit namespace_project_merge_requests_path(project.namespace, project) - expect(merge_request.mr_and_commit_notes.count).to eq 1 + expect(merge_request.mr_and_commit_notes.count).to eq 0 expect(page.all('.merge-request-no-comments').first.text).to eq "0" end end @@ -146,11 +146,11 @@ describe 'Comments', feature: true do describe 'comment info' do it 'excludes award_emoji from comment count' do - create(:award_emoji, awardable: merge_request, project: project) + create(:award_emoji, awardable: merge_request) visit namespace_project_merge_request_path(project.namespace, project, merge_request) - expect(merge_request.mr_and_commit_notes.count).to eq 2 + expect(merge_request.mr_and_commit_notes.count).to eq 1 expect(find('.notes-tab span.badge').text).to eq "1" end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 4e62e3975e..35f576874b 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -59,7 +59,7 @@ describe Notes::CreateService, services: true do noteable_id: issue.id } - expect_any_instance_of(ToggleAwardEmojiService).to receive(:execute).with(issue, "thumbsup") + expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user) Notes::CreateService.new(project, user, opts).execute end diff --git a/spec/services/toggle_award_emoji_service_spec.rb b/spec/services/toggle_award_emoji_service_spec.rb deleted file mode 100644 index b9d63bd70a..0000000000 --- a/spec/services/toggle_award_emoji_service_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe ToggleAwardEmoji, services: true do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:issue) { create(:issue, project: project) } - - before do - project.team << [user, :master] - end - - describe '#execute' do - it 'removes related todos' do - expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user) - - ToggleAwardEmojiService.new(project, user).execute(issue, "thumbsdown") - end - - context 'when the emoji is set' do - it 'removes the emoji' do - create(:award_emoji, awardable: issue, user: user) - - expect { ToggleAwardEmojiService.new(project, user).execute(issue, "thumbsup") }.to change { AwardEmoji.count }.by(-1) - end - end - - context 'when the award is not set yet' do - it 'awards the emoji' do - expect { ToggleAwardEmojiService.new(project, user).execute(issue, "thumbsup") }.to change { AwardEmoji.count }.by(1) - end - end - end -end From 4caf38a1d583f1561affc278ff0ddc0174e47b72 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 31 Mar 2016 09:21:33 +0100 Subject: [PATCH 036/507] Refactored JS slightly to make things easier # Conflicts: # app/assets/javascripts/awards_handler.coffee # app/views/emoji_awards/_awards_block.html.haml --- app/assets/javascripts/awards_handler.coffee | 209 ++++++++---------- .../emoji_awards/_awards_block.html.haml | 22 ++ 2 files changed, 120 insertions(+), 111 deletions(-) create mode 100644 app/views/emoji_awards/_awards_block.html.haml diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index bf95e06b4e..105b0c34e9 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,78 +1,69 @@ class @AwardsHandler - constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) -> - $('.js-add-award').on 'click', (event) => + constructor: (@aliases) -> + $(".js-add-award").on "click", (event) => event.stopPropagation() event.preventDefault() @showEmojiMenu() - $('html').on 'click', (event) -> - if !$(event.target).closest('.emoji-menu').length - if $('.emoji-menu').is(':visible') - $('.emoji-menu').removeClass 'is-visible' + $("html").on 'click', (event) -> + if !$(event.target).closest(".emoji-menu").length + if $(".emoji-menu").is(":visible") + $(".emoji-menu").removeClass "is-visible" - $('.awards') - .off 'click' - .on 'click', '.js-emoji-btn', @handleClick + $(".awards") + .off "click" + .on "click", ".js-emoji-btn", @handleClick @renderFrequentlyUsedBlock() handleClick: (e) -> e.preventDefault() - emoji = $(this) - .find('.icon') - .data 'emoji' - - if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown' - awardsHandler.addAward 'thumbsdown' - - else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup' - awardsHandler.addAward 'thumbsup' - - awardsHandler.addAward emoji - - $(this).trigger 'blur' - - didUserClickEmoji: (that, emoji) -> - if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title') - $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1 + $emojiBtn = $(e.currentTarget) + awardUrl = $emojiBtn.closest('.js-votes-block').data 'award-url' + emoji = $emojiBtn + .find(".icon") + .data "emoji" + @addAward awardUrl, emoji showEmojiMenu: -> - if $('.emoji-menu').length - if $('.emoji-menu').is '.is-visible' - $('.emoji-menu').removeClass 'is-visible' - $('#emoji_search').blur() + if $(".emoji-menu").length + if $(".emoji-menu").is ".is-visible" + $(".emoji-menu").removeClass "is-visible" + $("#emoji_search").blur() else - $('.emoji-menu').addClass 'is-visible' - $('#emoji_search').focus() + $(".emoji-menu").addClass "is-visible" + $("#emoji_search").focus() else - $('.js-add-award').addClass 'is-loading' - $.get @getEmojisUrl, (response) => - $('.js-add-award').removeClass 'is-loading' - $('.js-award-holder').append response + $('.js-add-award').addClass "is-loading" + $.get "/emojis", (response) => + $('.js-add-award').removeClass "is-loading" + $(".js-award-holder").append response setTimeout => - $('.emoji-menu').addClass 'is-visible' - $('#emoji_search').focus() + $(".emoji-menu").addClass "is-visible" + $("#emoji_search").focus() @setupSearch() , 200 - addAward: (emoji) -> - @postEmoji emoji, => + addAward: (awardUrl, emoji) -> + emoji = @normilizeEmojiName(emoji) + @postEmoji awardUrl, emoji, => @addAwardToEmojiBar(emoji) - $('.emoji-menu').removeClass 'is-visible' + $(".emoji-menu").removeClass "is-visible" addAwardToEmojiBar: (emoji) -> @addEmojiToFrequentlyUsedList(emoji) + emoji = @normilizeEmojiName(emoji) if @exist(emoji) if @isActive(emoji) @decrementCounter(emoji) else - counter = @findEmojiIcon(emoji).siblings('.js-counter') + counter = @findEmojiIcon(emoji).siblings(".js-counter") counter.text(parseInt(counter.text()) + 1) - counter.parent().addClass('active') - @addMeToAuthorList(emoji) + counter.parent().addClass("active") + @addMeToUserList(emoji) else @createEmoji(emoji) @@ -80,47 +71,47 @@ class @AwardsHandler @findEmojiIcon(emoji).length > 0 isActive: (emoji) -> - @findEmojiIcon(emoji).parent().hasClass('active') + @findEmojiIcon(emoji).parent().hasClass("active") decrementCounter: (emoji) -> - counter = @findEmojiIcon(emoji).siblings('.js-counter') + counter = @findEmojiIcon(emoji).siblings(".js-counter") emojiIcon = counter.parent() if parseInt(counter.text()) > 1 counter.text(parseInt(counter.text()) - 1) - emojiIcon.removeClass('active') - @removeMeFromAuthorList(emoji) - else if emoji == 'thumbsup' || emoji == 'thumbsdown' - emojiIcon.tooltip('destroy') + emojiIcon.removeClass("active") + @removeMeFromUserList(emoji) + else if emoji == "thumbsup" || emoji == "thumbsdown" + emojiIcon.tooltip("destroy") counter.text(0) - emojiIcon.removeClass('active') - @removeMeFromAuthorList(emoji) + emojiIcon.removeClass("active") + @removeMeFromUserList(emoji) else - emojiIcon.tooltip('destroy') + emojiIcon.tooltip("destroy") emojiIcon.remove() - removeMeFromAuthorList: (emoji) -> - awardBlock = @findEmojiIcon(emoji).parent() - authors = awardBlock - .attr('data-original-title') - .split(', ') - authors.splice(authors.indexOf('me'),1) - awardBlock - .closest('.js-emoji-btn') - .attr('data-original-title', authors.join(', ')) - @resetTooltip(awardBlock) + removeMeFromUserList: (emoji) -> + award_block = @findEmojiIcon(emoji).parent() + authors = award_block + .attr("data-original-title") + .split(", ") + authors.splice(authors.indexOf("me"),1) + award_block + .closest(".js-emoji-btn") + .attr("data-original-title", authors.join(", ")) + @resetTooltip(award_block) - addMeToAuthorList: (emoji) -> - awardBlock = @findEmojiIcon(emoji).parent() - origTitle = awardBlock.attr('data-original-title').trim() - authors = [] + addMeToUserList: (emoji) -> + award_block = @findEmojiIcon(emoji).parent() + origTitle = award_block.attr("data-original-title").trim() + users = [] if origTitle - authors = origTitle.split(', ') - authors.push('me') - awardBlock.attr('data-original-title', authors.join(', ')) - @resetTooltip(awardBlock) + users = origTitle.split(', ') + users.push("me") + award_block.attr("title", users.join(", ")) + @resetTooltip(award_block) resetTooltip: (award) -> - award.tooltip('destroy') + award.tooltip("destroy") # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. setTimeout (-> @@ -131,37 +122,30 @@ class @AwardsHandler createEmoji: (emoji) -> emojiCssClass = @resolveNameToCssClass(emoji) - nodes = [] - nodes.push( - "" - ) + buttonHtml = "" - $(nodes.join("\n")) - .insertBefore('.js-award-holder') - .find('.emoji-icon') - .data('emoji', emoji) + emoji_node = $(buttonHtml) + .insertBefore(".js-award-holder") + .find(".emoji-icon") + .data("emoji", emoji) $('.award-control').tooltip() resolveNameToCssClass: (emoji) -> - emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']") + emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") - if emojiIcon.length > 0 - unicodeName = emojiIcon.data('unicode-name') + if emoji_icon.length > 0 + unicodeName = emoji_icon.data("unicode-name") else # Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') + unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name") "emoji-#{unicodeName}" - postEmoji: (emoji, callback) -> - $.post @postEmojiUrl, { note: { - note: ":#{emoji}:" - noteable_type: @noteableType - noteable_id: @noteableId - }},(data) -> + postEmoji: (awardUrl, emoji, callback) -> + $.post awardUrl, { name: emoji }, (data) -> if data.ok callback.call() @@ -173,43 +157,46 @@ class @AwardsHandler scrollTop: $('.awards').offset().top - 80 }, 200) + normilizeEmojiName: (emoji) -> + @aliases[emoji] || emoji + addEmojiToFrequentlyUsedList: (emoji) -> - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - frequentlyUsedEmojis.push(emoji) - $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }) + frequently_used_emojis = @getFrequentlyUsedEmojis() + frequently_used_emojis.push(emoji) + $.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 }) getFrequentlyUsedEmojis: -> - frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',') - _.compact(_.uniq(frequentlyUsedEmojis)) + frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",") + _.compact(_.uniq(frequently_used_emojis)) renderFrequentlyUsedBlock: -> if $.cookie('frequently_used_emojis') - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() + frequently_used_emojis = @getFrequentlyUsedEmojis() - ul = $('
                      ') + ul = $("
                        ") - for emoji in frequentlyUsedEmojis + for emoji in frequently_used_emojis do (emoji) -> - $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) + $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) - $('input.emoji-search').after(ul).after($('
                        ').text('Frequently used')) + $("input.emoji-search").after(ul).after($("
                        ").text("Frequently used")) setupSearch: -> - $('input.emoji-search').keyup (ev) => + $("input.emoji-search").keyup (ev) => term = $(ev.target).val() # Clean previous search results - $('ul.emoji-menu-search, h5.emoji-search').remove() + $("ul.emoji-menu-search, h5.emoji-search").remove() if term # Generate a search result block - h5 = $('
                        ').text('Search results').addClass('emoji-search') - foundEmojis = @searchEmojis(term).show() - ul = $('
                          ').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis) - $('.emoji-menu-content ul, .emoji-menu-content h5').hide() - $('.emoji-menu-content').append(h5).append(ul) + h5 = $("
                          ").text("Search results").addClass("emoji-search") + found_emojis = @searchEmojis(term).show() + ul = $("
                            ").addClass("emoji-menu-list emoji-menu-search").append(found_emojis) + $(".emoji-menu-content ul, .emoji-menu-content h5").hide() + $(".emoji-menu-content").append(h5).append(ul) else - $('.emoji-menu-content').children().show() + $(".emoji-menu-content").children().show() searchEmojis: (term)-> $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() diff --git a/app/views/emoji_awards/_awards_block.html.haml b/app/views/emoji_awards/_awards_block.html.haml new file mode 100644 index 0000000000..da1de1c1de --- /dev/null +++ b/app/views/emoji_awards/_awards_block.html.haml @@ -0,0 +1,22 @@ +.awards.votes-block.js-votes-block{ data: { award_url: url_for([:toggle_emoji_award, @project.namespace.becomes(Namespace), @project, awardable]) } } + - awards_sort(awardable.grouped_awards).each do |emoji, awards| + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), title: award_user_list(awards, current_user) } + = emoji_icon(emoji) + %span.award-control-text.js-counter + = awards.count + + - if current_user + %div.award-menu-holder.js-award-holder + %button.btn.award-control.js-add-award{ type: "button" } + = icon('smile-o', {class: "award-control-icon"}) + = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) + %span.award-control-text + Add + +- if current_user + :javascript + var aliases = #{AwardEmoji.aliases.to_json}; + + window.awards_handler = new AwardsHandler( + aliases + ); From 0b45cf55dddb66b7c6328bc6d5233d023c031e5c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 31 Mar 2016 10:55:37 +0100 Subject: [PATCH 037/507] Uses the same emoji-menu and just moves it around depending where it should be viewed # Conflicts: # app/assets/javascripts/awards_handler.coffee # app/assets/stylesheets/pages/notes.scss # app/helpers/issues_helper.rb # app/views/projects/notes/_note.html.haml --- app/assets/javascripts/awards_handler.coffee | 41 +-- app/assets/javascripts/dispatcher.js.coffee | 2 + .../lib/emoji_aliases.js.coffee.erb | 2 + app/assets/stylesheets/pages/awards.scss | 2 +- app/assets/stylesheets/pages/notes.scss | 249 ++++++++---------- .../emoji_awards/_awards_block.html.haml | 14 +- app/views/projects/notes/_note.html.haml | 23 +- 7 files changed, 155 insertions(+), 178 deletions(-) create mode 100644 app/assets/javascripts/lib/emoji_aliases.js.coffee.erb diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 105b0c34e9..9e3d9f0912 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,23 +1,23 @@ class @AwardsHandler - constructor: (@aliases) -> - $(".js-add-award").on "click", (event) => + constructor: -> + @aliases = emojiAliases() + + $(document).on "click", ".js-add-award", (event) => event.stopPropagation() event.preventDefault() - @showEmojiMenu() + @showEmojiMenu $(event.currentTarget) $("html").on 'click', (event) -> if !$(event.target).closest(".emoji-menu").length if $(".emoji-menu").is(":visible") $(".emoji-menu").removeClass "is-visible" - $(".awards") - .off "click" + $(document) + .off "click", ".js-emoji-btn" .on "click", ".js-emoji-btn", @handleClick - @renderFrequentlyUsedBlock() - - handleClick: (e) -> + handleClick: (e) => e.preventDefault() $emojiBtn = $(e.currentTarget) awardUrl = $emojiBtn.closest('.js-votes-block').data 'award-url' @@ -26,8 +26,13 @@ class @AwardsHandler .data "emoji" @addAward awardUrl, emoji - showEmojiMenu: -> + showEmojiMenu: ($addBtn) -> if $(".emoji-menu").length + $holder = $addBtn.closest('.js-award-holder') + + if $holder.find('.emoji-menu').length is 0 + $(".emoji-menu").detach().appendTo $holder + if $(".emoji-menu").is ".is-visible" $(".emoji-menu").removeClass "is-visible" $("#emoji_search").blur() @@ -35,10 +40,11 @@ class @AwardsHandler $(".emoji-menu").addClass "is-visible" $("#emoji_search").focus() else - $('.js-add-award').addClass "is-loading" - $.get "/emojis", (response) => - $('.js-add-award').removeClass "is-loading" - $(".js-award-holder").append response + $addBtn.addClass "is-loading" + $.get $addBtn.data('award-menu-url'), (response) => + $addBtn.removeClass "is-loading" + $addBtn.closest('.js-award-holder').append response + @renderFrequentlyUsedBlock() setTimeout => $(".emoji-menu").addClass "is-visible" $("#emoji_search").focus() @@ -128,7 +134,7 @@ class @AwardsHandler " emoji_node = $(buttonHtml) - .insertBefore(".js-award-holder") + .insertBefore(".js-award-holder:not(.js-award-action-btn)") .find(".emoji-icon") .data("emoji", emoji) $('.award-control').tooltip() @@ -173,16 +179,15 @@ class @AwardsHandler if $.cookie('frequently_used_emojis') frequently_used_emojis = @getFrequentlyUsedEmojis() - ul = $("
                              ") + ul = $("
                                ") for emoji in frequently_used_emojis - do (emoji) -> - $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) + $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) $("input.emoji-search").after(ul).after($("
                                ").text("Frequently used")) setupSearch: -> - $("input.emoji-search").keyup (ev) => + $("input.emoji-search").on 'keyup', (ev) => term = $(ev.target).val() # Clean previous search results diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index f91aa3c5ad..ff8bcf8991 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -23,6 +23,7 @@ class Dispatcher new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() + window.awards_handler = new AwardsHandler() when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() when 'dashboard:todos:index' @@ -53,6 +54,7 @@ class Dispatcher new Diff() shortcut_handler = new ShortcutsIssuable(true) new ZenMode() + window.awards_handler = new AwardsHandler() when "projects:merge_requests:diffs" new Diff() new ZenMode() diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb new file mode 100644 index 0000000000..66f640a3cb --- /dev/null +++ b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb @@ -0,0 +1,2 @@ +window.emojiAliases = -> + JSON.parse('<%= AwardEmoji.aliases.to_json %>') diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 37bf38fa65..c7c8d2dda4 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -107,7 +107,7 @@ } &.is-loading { - .award-control-icon { + .award-control-icon-normal { display: none; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index a3e1ac13a4..411cd3cd23 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -20,15 +20,9 @@ ul.notes { .timeline-content { margin-left: 55px; - - &.timeline-content-form { - @media (max-width: $screen-sm-max) { - margin-left: 0; - } - } } - .note-created-ago, .note-updated-at { + .note_created_ago, .note-updated-at { white-space: nowrap; } @@ -45,6 +39,53 @@ ul.notes { } } + .discussion-header, + .note-header { + @extend .cgray; + + a:hover { + text-decoration: none; + } + + .avatar { + float: left; + margin-right: 10px; + } + + .discussion-last-update, + .note-last-update { + &:before { + content: "\00b7"; + } + + a { + color: $gl-gray; + + &:hover { + text-decoration: underline; + } + } + } + .author { + color: #4c4e54; + margin-right: 3px; + + &:hover { + color: $gl-link-color; + } + } + .author-username { + } + + .note-role { + float: right; + margin-top: 1px; + border: 1px solid #bbb; + background-color: transparent; + color: $gl-gray; + } + } + .discussion-body { padding-top: 15px; } @@ -58,7 +99,6 @@ ul.notes { .note { display: block; position: relative; - border-bottom: 1px solid $table-border-gray; &.is-editting { .note-header, @@ -73,16 +113,16 @@ ul.notes { } .note-body { - overflow: auto; - .note-text { overflow: auto; word-wrap: break-word; @include md-typography; // On diffs code should wrap nicely and not overflow - code { - white-space: pre-wrap; + pre { + code { + white-space: pre-wrap; + } } // Reset ul style types since we're nested inside a ul already @@ -109,10 +149,6 @@ ul.notes { border-color: darken(#f5f5f5, 8%); margin: 10px 0; } - - code { - word-break: keep-all; - } } } @@ -120,6 +156,9 @@ ul.notes { padding-bottom: 3px; } + &:last-child { + border-bottom: 1px solid $border-color; + } } } @@ -137,149 +176,60 @@ ul.notes { font-family: $regular_font; td { - border: 1px solid $table-border-gray; + border: 1px solid #ddd; border-left: none; &.notes_line { vertical-align: middle; text-align: center; padding: 10px 0; - background: $background-color; + background: #fff; color: $text-color; } - &.notes_line2 { text-align: center; padding: 10px 0; border-left: 1px solid #ddd !important; } - &.notes_content { - background-color: $background-color; + background-color: #fff; border-width: 1px 0; - padding: 0; + padding-top: 0; vertical-align: top; - white-space: normal; - - &.parallel { + &.parallel{ border-width: 1px; } - - .notes { - background-color: $white-light; - } - - a code { - top: 0; - margin-right: 0; - } } } } -.discussion-header, -.note-header { - a { - color: inherit; - - &:hover { - color: $gl-link-color; - text-decoration: none; - } - } - - .author_link { - color: $gl-gray; - } -} - -.note-headline-light, -.discussion-headline-light { - color: $notes-light-color; -} - -.discussion-headline-light { - a { - color: $gl-link-color; - } -} - /** * Actions for Discussions/Notes */ -.discussion-actions, -.note-actions { - float: right; - margin-left: 10px; - color: $notes-action-color; -} +.discussion, +.note { + .discussion-actions, + .note-actions { + float: right; + margin-left: 10px; -.discussion-actions { - @media (max-width: $screen-md-max) { - float: none; - margin-left: 0; + a { + margin-left: 5px; + color: $gl-gray; - .note-action-button { - margin-left: 0; - } - } -} - -.note-action-button { - display: inline-block; - margin-left: 10px; - line-height: 24px; - - .fa { - color: $notes-action-color; - position: relative; - top: 1px; - font-size: 17px; - } - - &.js-note-delete { - i { - &:hover { - color: $gl-text-red; + i.fa { + font-size: 16px; + line-height: 16px; } - } - } - &.js-note-edit { - i { &:hover { - color: $gl-link-color; + @extend .cgray; + &.danger { @extend .cred; } } } } } - -.discussion-toggle-button { - line-height: 20px; - font-size: 13px; - - .fa { - margin-right: 3px; - font-size: 10px; - line-height: 18px; - vertical-align: top; - } -} - -.note-role { - position: relative; - top: -2px; - display: inline-block; - padding-left: 4px; - padding-right: 4px; - color: $notes-role-color; - font-size: 12px; - line-height: 20px; - border: 1px solid $notes-role-border-color; - border-radius: $border-radius-base; -} - .diff-file .note .note-actions { right: 0; top: 0; @@ -292,7 +242,8 @@ ul.notes { .diff-file tr.line_holder { @mixin show-add-diff-note { - display: inline-block; + filter: alpha(opacity=100); + opacity: 1.0; } .add-diff-note { @@ -302,12 +253,17 @@ ul.notes { padding: 4px; font-size: 16px; color: $gl-link-color; - margin-left: -56px; + margin-left: -60px; position: absolute; z-index: 10; width: 32px; + + transition: all 0.2s ease; + // "hide" it by default - display: none; + opacity: 0.0; + filter: alpha(opacity=0); + &:hover { background: $gl-info; color: #fff; @@ -323,20 +279,33 @@ ul.notes { } } -.disabled-comment { - margin-left: -$gl-padding-top; - margin-right: -$gl-padding-top; - background-color: $gray-light; - border-radius: $border-radius-base; - border: 1px solid $border-gray-normal; - color: $note-disabled-comment-color; - line-height: 200px; - - .disabled-comment-text { - line-height: normal; +.note-action-award-holder { + .emoji-menu { + left: auto; + right: -15px; + transform-origin: 100% -45px; } +} - a { - color: $gl-link-color; +.note-award-control { + display: block; + + &:hover, + &:focus { + text-decoration: none; + } + + .award-control-icon-loading { + display: none; + } + + &.is-loading { + .award-control-icon-normal { + display: none; + } + + .award-control-icon-loading { + display: block; + } } } diff --git a/app/views/emoji_awards/_awards_block.html.haml b/app/views/emoji_awards/_awards_block.html.haml index da1de1c1de..79ebaa6777 100644 --- a/app/views/emoji_awards/_awards_block.html.haml +++ b/app/views/emoji_awards/_awards_block.html.haml @@ -6,17 +6,9 @@ = awards.count - if current_user - %div.award-menu-holder.js-award-holder - %button.btn.award-control.js-add-award{ type: "button" } - = icon('smile-o', {class: "award-control-icon"}) + .award-menu-holder.js-award-holder + %button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } + = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) %span.award-control-text Add - -- if current_user - :javascript - var aliases = #{AwardEmoji.aliases.to_json}; - - window.awards_handler = new AwardsHandler( - aliases - ); diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 9fbc9a4554..b7fbd89da9 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -9,14 +9,20 @@ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header - = link_to_member(note.project, note.author, avatar: false) - .inline.note-headline-light - = 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') - .note-actions + - if note_editable?(note) + .note-actions + = link_to '#', title: 'Edit comment', class: 'js-note-edit' do + = icon('pencil-square-o') + + .award-menu-holder.note-action-award-holder.js-award-holder.js-award-action-btn + = link_to '#', title: 'Award emoji', class: 'note-award-control js-add-award', data: { award_menu_url: emojis_path } do + = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) + = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) + + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do + = icon('trash-o') + + - unless note.system - access = note.project.team.human_max_access(note.author.id) - if access %span.note-role @@ -32,6 +38,7 @@ = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) - if note_editable = render 'projects/notes/edit_form', note: note + = render 'emoji_awards/awards_block', awardable: note = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note.attachment.url From f2f2e072940fb24f77401f8b73e417a610aa9f36 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 31 Mar 2016 12:08:31 +0100 Subject: [PATCH 038/507] Shows the bar on notes if a new award is added Correctly adds/removes awards in notes # Conflicts: # app/models/concerns/awardable.rb # app/views/projects/issues/show.html.haml # app/views/projects/merge_requests/_show.html.haml --- app/assets/javascripts/awards_handler.coffee | 33 +++++++++++++------ .../emoji_awards/_awards_block.html.haml | 5 +-- app/views/projects/notes/_note.html.haml | 10 +++--- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 9e3d9f0912..4ee0c6e215 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -20,7 +20,13 @@ class @AwardsHandler handleClick: (e) => e.preventDefault() $emojiBtn = $(e.currentTarget) - awardUrl = $emojiBtn.closest('.js-votes-block').data 'award-url' + $votesBlock = $($emojiBtn.closest('.js-award-holder').data('target')) + + if $votesBlock.length is 0 + $votesBlock = $emojiBtn.closest('.js-awards-block') + + $votesBlock.addClass 'js-awards-block-current' + awardUrl = $votesBlock.data 'award-url' emoji = $emojiBtn .find(".icon") .data "emoji" @@ -44,7 +50,9 @@ class @AwardsHandler $.get $addBtn.data('award-menu-url'), (response) => $addBtn.removeClass "is-loading" $addBtn.closest('.js-award-holder').append response + @renderFrequentlyUsedBlock() + setTimeout => $(".emoji-menu").addClass "is-visible" $("#emoji_search").focus() @@ -56,14 +64,18 @@ class @AwardsHandler @postEmoji awardUrl, emoji, => @addAwardToEmojiBar(emoji) + $('.js-awards-block-current').removeClass 'js-awards-block-current' + $(".emoji-menu").removeClass "is-visible" addAwardToEmojiBar: (emoji) -> @addEmojiToFrequentlyUsedList(emoji) emoji = @normilizeEmojiName(emoji) - if @exist(emoji) - if @isActive(emoji) + $emojiBtn = @findEmojiIcon(emoji) + + if $emojiBtn.length > 0 + if @isActive($emojiBtn) @decrementCounter(emoji) else counter = @findEmojiIcon(emoji).siblings(".js-counter") @@ -73,11 +85,8 @@ class @AwardsHandler else @createEmoji(emoji) - exist: (emoji) -> - @findEmojiIcon(emoji).length > 0 - - isActive: (emoji) -> - @findEmojiIcon(emoji).parent().hasClass("active") + isActive: ($emojiBtn) -> + $emojiBtn.parent().hasClass("active") decrementCounter: (emoji) -> counter = @findEmojiIcon(emoji).siblings(".js-counter") @@ -134,11 +143,15 @@ class @AwardsHandler " emoji_node = $(buttonHtml) - .insertBefore(".js-award-holder:not(.js-award-action-btn)") + .insertBefore(".js-awards-block-current .js-award-holder:not(.js-award-action-btn)") .find(".emoji-icon") .data("emoji", emoji) $('.award-control').tooltip() + $currentBlock = $('.js-awards-block-current') + if $currentBlock.is('.hidden') + $currentBlock.removeClass 'hidden' + resolveNameToCssClass: (emoji) -> emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") @@ -156,7 +169,7 @@ class @AwardsHandler callback.call() findEmojiIcon: (emoji) -> - $(".awards > .js-emoji-btn [data-emoji='#{emoji}']") + $(".js-awards-block-current.awards > .js-emoji-btn [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ diff --git a/app/views/emoji_awards/_awards_block.html.haml b/app/views/emoji_awards/_awards_block.html.haml index 79ebaa6777..7df64253f2 100644 --- a/app/views/emoji_awards/_awards_block.html.haml +++ b/app/views/emoji_awards/_awards_block.html.haml @@ -1,5 +1,6 @@ -.awards.votes-block.js-votes-block{ data: { award_url: url_for([:toggle_emoji_award, @project.namespace.becomes(Namespace), @project, awardable]) } } - - awards_sort(awardable.grouped_awards).each do |emoji, awards| +- grouped_emojis = awardable.grouped_awards(inline) +.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.size == 0), data: { award_url: url_for([:toggle_emoji_award, @project.namespace.becomes(Namespace), @project, awardable]) } } + - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), title: award_user_list(awards, current_user) } = emoji_icon(emoji) %span.award-control-text.js-counter diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index b7fbd89da9..e11a7c52a9 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -11,14 +11,14 @@ .note-header - if note_editable?(note) .note-actions - = link_to '#', title: 'Edit comment', class: 'js-note-edit' do - = icon('pencil-square-o') - - .award-menu-holder.note-action-award-holder.js-award-holder.js-award-action-btn + .award-menu-holder.note-action-award-holder.js-award-holder.js-award-action-btn{ data: { target: "##{dom_id(note)} .js-awards-block" } } = link_to '#', title: 'Award emoji', class: 'note-award-control js-add-award', data: { award_menu_url: emojis_path } do = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) + = link_to '#', title: 'Edit comment', class: 'js-note-edit' do + = icon('pencil-square-o') + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do = icon('trash-o') @@ -38,7 +38,7 @@ = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) - if note_editable = render 'projects/notes/edit_form', note: note - = render 'emoji_awards/awards_block', awardable: note + = render 'emoji_awards/awards_block', awardable: note, inline: false = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note.attachment.url From 25a4893215143a5481282013781bc30667bfd75f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 31 Mar 2016 12:56:48 +0100 Subject: [PATCH 039/507] Removes buttons in notes body --- app/assets/javascripts/awards_handler.coffee | 54 +++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 4ee0c6e215..d4ce01354b 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -2,11 +2,13 @@ class @AwardsHandler constructor: -> @aliases = emojiAliases() - $(document).on "click", ".js-add-award", (event) => - event.stopPropagation() - event.preventDefault() + $(document) + .off "click", ".js-add-award" + .on "click", ".js-add-award", (event) => + event.stopPropagation() + event.preventDefault() - @showEmojiMenu $(event.currentTarget) + @showEmojiMenu $(event.currentTarget) $("html").on 'click', (event) -> if !$(event.target).closest(".emoji-menu").length @@ -72,40 +74,42 @@ class @AwardsHandler @addEmojiToFrequentlyUsedList(emoji) emoji = @normilizeEmojiName(emoji) - $emojiBtn = @findEmojiIcon(emoji) + $emojiBtn = @findEmojiIcon(emoji).parent() if $emojiBtn.length > 0 if @isActive($emojiBtn) - @decrementCounter(emoji) + @decrementCounter($emojiBtn, emoji) else - counter = @findEmojiIcon(emoji).siblings(".js-counter") + counter = $emojiBtn.siblings(".js-counter") counter.text(parseInt(counter.text()) + 1) - counter.parent().addClass("active") + $emojiBtn.addClass("active") @addMeToUserList(emoji) else @createEmoji(emoji) isActive: ($emojiBtn) -> - $emojiBtn.parent().hasClass("active") + $emojiBtn.hasClass("active") - decrementCounter: (emoji) -> - counter = @findEmojiIcon(emoji).siblings(".js-counter") - emojiIcon = counter.parent() - if parseInt(counter.text()) > 1 - counter.text(parseInt(counter.text()) - 1) - emojiIcon.removeClass("active") - @removeMeFromUserList(emoji) - else if emoji == "thumbsup" || emoji == "thumbsdown" - emojiIcon.tooltip("destroy") - counter.text(0) - emojiIcon.removeClass("active") - @removeMeFromUserList(emoji) + decrementCounter: ($emojiBtn, emoji) -> + isntNoteBody = $emojiBtn.closest('.note-body').length is 0 + counter = $('.js-counter', $emojiBtn) + counterNumber = parseInt(counter.text()) + + if counterNumber > 1 + counter.text(counterNumber - 1) + @removeMeFromUserList($emojiBtn, emoji) + else if (emoji == "thumbsup" || emoji == "thumbsdown") && isntNoteBody + $emojiBtn.tooltip("destroy") + counter.text('0') + @removeMeFromUserList($emojiBtn, emoji) else - emojiIcon.tooltip("destroy") - emojiIcon.remove() + $emojiBtn.tooltip("destroy") + $emojiBtn.remove() - removeMeFromUserList: (emoji) -> - award_block = @findEmojiIcon(emoji).parent() + $emojiBtn.removeClass("active") + + removeMeFromUserList: ($emojiBtn, emoji) -> + award_block = $emojiBtn authors = award_block .attr("data-original-title") .split(", ") From 9e8449b1afee35a6497db43ae6c040fae5f26186 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 31 Mar 2016 13:17:57 +0100 Subject: [PATCH 040/507] Hides the row in notes body when empty of emojis --- app/assets/javascripts/awards_handler.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index d4ce01354b..fb880d28f2 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -95,6 +95,11 @@ class @AwardsHandler counter = $('.js-counter', $emojiBtn) counterNumber = parseInt(counter.text()) + if !isntNoteBody + console.log $emojiBtn.get(0) + # If this is a note body, we just hide the award emoji row like the initial state + $emojiBtn.closest('.js-awards-block').addClass 'hidden' + if counterNumber > 1 counter.text(counterNumber - 1) @removeMeFromUserList($emojiBtn, emoji) From 7d4f41efdee8aae424ed624a8702c9eb863ac43f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 31 Mar 2016 13:20:53 +0100 Subject: [PATCH 041/507] Removed console log --- app/assets/javascripts/awards_handler.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index fb880d28f2..9d7ed87e59 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -96,7 +96,6 @@ class @AwardsHandler counterNumber = parseInt(counter.text()) if !isntNoteBody - console.log $emojiBtn.get(0) # If this is a note body, we just hide the award emoji row like the initial state $emojiBtn.closest('.js-awards-block').addClass 'hidden' From 3d4d8e59077ebd491741bb737b1e2af8f8335e4a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 31 Mar 2016 15:19:38 +0100 Subject: [PATCH 042/507] Changed design of inline award picker to be similar to designs # Conflicts: # app/views/votes/_votes_block.html.haml --- app/assets/stylesheets/pages/awards.scss | 2 -- app/assets/stylesheets/pages/notes.scss | 12 ++++++++++ app/views/projects/notes/_note.html.haml | 3 ++- app/views/votes/_votes_block.html.haml | 30 ------------------------ 4 files changed, 14 insertions(+), 33 deletions(-) delete mode 100644 app/views/votes/_votes_block.html.haml diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index c7c8d2dda4..ef73e2f0a5 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -1,6 +1,4 @@ .awards { - line-height: 34px; - .emoji-icon { width: 20px; height: 20px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 411cd3cd23..012799b226 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -309,3 +309,15 @@ ul.notes { } } } + +.note-awards { + .awards { + padding-top: 10px; + } + + .award-control { + padding-top: 2px; + padding-bottom: 2px; + font-size: 13px; + } +} diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index e11a7c52a9..ae5aae3432 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -38,7 +38,8 @@ = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) - if note_editable = render 'projects/notes/edit_form', note: note - = render 'emoji_awards/awards_block', awardable: note, inline: false + .note-awards + = render 'emoji_awards/awards_block', awardable: note, inline: false = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note.attachment.url diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml deleted file mode 100644 index ab8bb94986..0000000000 --- a/app/views/votes/_votes_block.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -.awards.votes-block{data: { toggle_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) }} - - awards_sort(awardable.grouped_awards).each do |emoji, awards| - %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(awards, current_user)), data: {placement: "top", original_title: emoji_author_list(awards, current_user)}} - = emoji_icon(emoji, sprite: false) - %span.award-control-text.js-counter - = awards.count - - - if current_user - %div.award-menu-holder.js-award-holder - %a.btn.award-control.js-add-award{"href" => "#"} - = icon('smile-o', {class: "award-control-icon"}) - = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) - %span.award-control-text - Add - -- if current_user - :javascript - var getEmojisUrl = "#{emojis_path}"; - var postEmojiUrl = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; - var noteableType = "#{votable.class.name.underscore}"; - var noteableId = "#{votable.id}"; - var unicodes = #{AwardEmoji.unicode.to_json}; - - window.awardsHandler = new AwardsHandler( - getEmojisUrl, - postEmojiUrl, - noteableType, - noteableId, - unicodes - ); From f6a7b3c37a431c5a44dec3aafe212557cb27f45e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 31 Mar 2016 17:34:42 +0100 Subject: [PATCH 043/507] Hides award bar when editing a note --- app/assets/stylesheets/pages/notes.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 012799b226..e240847388 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -103,7 +103,8 @@ ul.notes { &.is-editting { .note-header, .note-text, - .edited-text { + .edited-text, + .note-awards { display: none; } From 797acde04674ad58a599beea217de814d7dfe31d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 31 Mar 2016 18:19:38 +0100 Subject: [PATCH 044/507] Tooltip placement on award buttons --- app/assets/javascripts/awards_handler.coffee | 2 +- app/assets/stylesheets/pages/notes.scss | 1 + app/views/emoji_awards/_awards_block.html.haml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 9d7ed87e59..96f271be4d 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -145,7 +145,7 @@ class @AwardsHandler createEmoji: (emoji) -> emojiCssClass = @resolveNameToCssClass(emoji) - buttonHtml = "" diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e240847388..025897ca1f 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -319,6 +319,7 @@ ul.notes { .award-control { padding-top: 2px; padding-bottom: 2px; + color: #8f8f8f; font-size: 13px; } } diff --git a/app/views/emoji_awards/_awards_block.html.haml b/app/views/emoji_awards/_awards_block.html.haml index 7df64253f2..1b840d8875 100644 --- a/app/views/emoji_awards/_awards_block.html.haml +++ b/app/views/emoji_awards/_awards_block.html.haml @@ -1,7 +1,7 @@ - grouped_emojis = awardable.grouped_awards(inline) .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.size == 0), data: { award_url: url_for([:toggle_emoji_award, @project.namespace.becomes(Namespace), @project, awardable]) } } - awards_sort(grouped_emojis).each do |emoji, awards| - %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), title: award_user_list(awards, current_user) } + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), title: award_user_list(awards, current_user), data: { placement: "bottom" } } = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count From 44876032a2d6511060ac484259355b328b89ac0a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 1 Apr 2016 17:25:28 +0100 Subject: [PATCH 045/507] Added tests for issues --- app/views/projects/notes/_note.html.haml | 6 +- spec/features/issues/award_spec.rb | 140 +++++++++++++++++++++++ 2 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 spec/features/issues/award_spec.rb diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index ae5aae3432..bd478e315d 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -9,13 +9,13 @@ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header - - if note_editable?(note) - .note-actions + .note-actions + - if current_user .award-menu-holder.note-action-award-holder.js-award-holder.js-award-action-btn{ data: { target: "##{dom_id(note)} .js-awards-block" } } = link_to '#', title: 'Award emoji', class: 'note-award-control js-add-award', data: { award_menu_url: emojis_path } do = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) - + - if note_editable?(note) = link_to '#', title: 'Edit comment', class: 'js-note-edit' do = icon('pencil-square-o') diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb new file mode 100644 index 0000000000..46e1ed5d8c --- /dev/null +++ b/spec/features/issues/award_spec.rb @@ -0,0 +1,140 @@ +require 'rails_helper' + +feature 'Issue awards', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let!(:note) { create(:note_on_issue, project: project, noteable: issue, note: 'Looks good!') } + + describe 'logged in' do + before do + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should add award to issue' do + first('.js-emoji-btn').click + expect(page).to have_selector('.js-emoji-btn.active') + expect(first('.js-emoji-btn')).to have_content '1' + end + + it 'should remove award from issue' do + first('.js-emoji-btn').click + find('.js-emoji-btn.active').click + expect(first('.js-emoji-btn')).to have_content '0' + end + + it 'should show award menu button in notes' do + page.within('.note') do + expect(page).to have_selector('.js-award-action-btn') + end + end + + it 'should not show award bar on note if no awards given' do + page.within('.note') do + expect(find('.js-awards-block', visible: false)).not_to be_visible + end + end + + it 'should be able to show award menu when clicking add award button in note' do + show_note_award_menu + end + + it 'should only have one menu on the page' do + first('.js-add-award').click + expect(page).to have_selector('.emoji-menu') + + page.within('.note') do + find('.js-add-award').click + expect(page).to have_selector('.emoji-menu', count: 1) + end + end + + it 'should add award to note' do + show_note_award_menu + award_on_note + + page.within('.note') do + expect(find('.js-awards-block')).to be_visible + expect(find('.js-awards-block')).to have_selector('.active') + end + end + + it 'should remove award from note' do + show_note_award_menu + award_on_note + + page.within('.note') do + expect(find('.js-awards-block')).to be_visible + expect(find('.js-awards-block')).to have_selector('.active') + end + + remove_award_on_note + sleep 0.5 + + page.within('.note') do + expect(find('.js-awards-block', visible: false)).not_to be_visible + expect(find('.js-awards-block', visible: false)).not_to have_selector('.active') + end + end + + it 'should not hide award bar on notes with more than 1 award' do + show_note_award_menu + award_on_note + + show_note_award_menu + award_on_note(2) + + page.within('.note') do + expect(find('.js-awards-block')).to be_visible + expect(find('.js-awards-block')).to have_selector('.active') + end + + remove_award_on_note + + page.within('.note') do + expect(find('.js-awards-block')).to be_visible + end + end + end + + describe 'logged out' do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + + it 'should not see award menu button in note' do + page.within('.note') do + expect(page).not_to have_selector('.js-award-action-btn') + end + end + end + + def show_note_award_menu + page.within('.note') do + find('.js-add-award').click + expect(page).to have_selector('.emoji-menu') + end + end + + def award_on_note(index = 1) + page.within('.note') do + page.within('.emoji-menu') do + buttons = all('.js-emoji-btn') + buttons[index].click + end + end + end + + def remove_award_on_note + page.within('.note') do + page.within('.js-awards-block') do + first('.js-emoji-btn').click + end + end + end +end From bf96c30510c0efcd56338f60c902b5b64cf98bad Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 1 Apr 2016 17:35:10 +0100 Subject: [PATCH 046/507] Award spec for merge requests --- spec/features/merge_requests/award_spec.rb | 140 +++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 spec/features/merge_requests/award_spec.rb diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb new file mode 100644 index 0000000000..91e76e83c6 --- /dev/null +++ b/spec/features/merge_requests/award_spec.rb @@ -0,0 +1,140 @@ +require 'rails_helper' + +feature 'Merge request awards', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: project) } + let!(:note) { create(:note_on_merge_request, project: project, noteable: merge_request, note: 'Looks good!') } + + describe 'logged in' do + before do + login_as(user) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should add award to merge request' do + first('.js-emoji-btn').click + expect(page).to have_selector('.js-emoji-btn.active') + expect(first('.js-emoji-btn')).to have_content '1' + end + + it 'should remove award from merge request' do + first('.js-emoji-btn').click + find('.js-emoji-btn.active').click + expect(first('.js-emoji-btn')).to have_content '0' + end + + it 'should show award menu button in notes' do + page.within('.note') do + expect(page).to have_selector('.js-award-action-btn') + end + end + + it 'should not show award bar on note if no awards given' do + page.within('.note') do + expect(find('.js-awards-block', visible: false)).not_to be_visible + end + end + + it 'should be able to show award menu when clicking add award button in note' do + show_note_award_menu + end + + it 'should only have one menu on the page' do + first('.js-add-award').click + expect(page).to have_selector('.emoji-menu') + + page.within('.note') do + find('.js-add-award').click + expect(page).to have_selector('.emoji-menu', count: 1) + end + end + + it 'should add award to note' do + show_note_award_menu + award_on_note + + page.within('.note') do + expect(find('.js-awards-block')).to be_visible + expect(find('.js-awards-block')).to have_selector('.active') + end + end + + it 'should remove award from note' do + show_note_award_menu + award_on_note + + page.within('.note') do + expect(find('.js-awards-block')).to be_visible + expect(find('.js-awards-block')).to have_selector('.active') + end + + remove_award_on_note + sleep 0.5 + + page.within('.note') do + expect(find('.js-awards-block', visible: false)).not_to be_visible + expect(find('.js-awards-block', visible: false)).not_to have_selector('.active') + end + end + + it 'should not hide award bar on notes with more than 1 award' do + show_note_award_menu + award_on_note + + show_note_award_menu + award_on_note(2) + + page.within('.note') do + expect(find('.js-awards-block')).to be_visible + expect(find('.js-awards-block')).to have_selector('.active') + end + + remove_award_on_note + + page.within('.note') do + expect(find('.js-awards-block')).to be_visible + end + end + end + + describe 'logged out' do + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + + it 'should not see award menu button in note' do + page.within('.note') do + expect(page).not_to have_selector('.js-award-action-btn') + end + end + end + + def show_note_award_menu + page.within('.note') do + find('.js-add-award').click + expect(page).to have_selector('.emoji-menu') + end + end + + def award_on_note(index = 1) + page.within('.note') do + page.within('.emoji-menu') do + buttons = all('.js-emoji-btn') + buttons[index].click + end + end + end + + def remove_award_on_note + page.within('.note') do + page.within('.js-awards-block') do + first('.js-emoji-btn').click + end + end + end +end From 783a5ae98c17ffd1d93e2e5390e543e5f2f92bcc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 1 Apr 2016 18:18:45 +0100 Subject: [PATCH 047/507] Adds the emoji menu to the body and then re-positions it depending on which button clicked This spots bugs where the menu could be in a div that has overflow hidden on ie. diff comments --- app/assets/javascripts/awards_handler.coffee | 52 +++++++++++++++----- app/assets/stylesheets/pages/awards.scss | 9 ++-- app/assets/stylesheets/pages/notes.scss | 8 --- app/views/projects/notes/_note.html.haml | 2 +- spec/features/issues/award_spec.rb | 13 +++-- spec/features/merge_requests/award_spec.rb | 13 +++-- 6 files changed, 59 insertions(+), 38 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 96f271be4d..ff12cce485 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -13,6 +13,7 @@ class @AwardsHandler $("html").on 'click', (event) -> if !$(event.target).closest(".emoji-menu").length if $(".emoji-menu").is(":visible") + $('.js-add-award.is-active').removeClass 'is-active' $(".emoji-menu").removeClass "is-visible" $(document) @@ -22,10 +23,13 @@ class @AwardsHandler handleClick: (e) => e.preventDefault() $emojiBtn = $(e.currentTarget) - $votesBlock = $($emojiBtn.closest('.js-award-holder').data('target')) + $addAwardBtn = $('.js-add-award.is-active') + $votesBlock = $($addAwardBtn.closest('.js-award-holder').data('target')) - if $votesBlock.length is 0 + if $addAwardBtn.length is 0 $votesBlock = $emojiBtn.closest('.js-awards-block') + else if $votesBlock.length is 0 + $votesBlock = $addAwardBtn.closest('.js-awards-block') $votesBlock.addClass 'js-awards-block-current' awardUrl = $votesBlock.data 'award-url' @@ -35,32 +39,56 @@ class @AwardsHandler @addAward awardUrl, emoji showEmojiMenu: ($addBtn) -> - if $(".emoji-menu").length + $menu = $('.emoji-menu') + + if $menu.length $holder = $addBtn.closest('.js-award-holder') - if $holder.find('.emoji-menu').length is 0 - $(".emoji-menu").detach().appendTo $holder - - if $(".emoji-menu").is ".is-visible" - $(".emoji-menu").removeClass "is-visible" + if $menu.is ".is-visible" + $addBtn.removeClass "is-active" + $menu.removeClass "is-visible" $("#emoji_search").blur() else - $(".emoji-menu").addClass "is-visible" + $addBtn.addClass "is-active" + @positionMenu($menu, $addBtn) + + $menu.addClass "is-visible" $("#emoji_search").focus() else - $addBtn.addClass "is-loading" + $addBtn.addClass "is-loading is-active" $.get $addBtn.data('award-menu-url'), (response) => $addBtn.removeClass "is-loading" - $addBtn.closest('.js-award-holder').append response + $('body').append response + + $menu = $(".emoji-menu") + + @positionMenu($menu, $addBtn) @renderFrequentlyUsedBlock() setTimeout => - $(".emoji-menu").addClass "is-visible" + $menu.addClass "is-visible" $("#emoji_search").focus() @setupSearch() , 200 + positionMenu: ($menu, $addBtn) -> + position = $addBtn.data('position') + + # The menu could potentially be off-screen or in a hidden overflow element + # So we position the element absolute in the body + css = + top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px" + + if position? and position is 'right' + css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px" + $menu.addClass "is-aligned-right" + else + css.left = "#{$addBtn.offset().left}px" + $menu.removeClass "is-aligned-right" + + $menu.css(css) + addAward: (awardUrl, emoji) -> emoji = @normilizeEmojiName(emoji) @postEmoji awardUrl, emoji, => diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index ef73e2f0a5..07d40f4055 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -7,8 +7,6 @@ .emoji-menu { position: absolute; - top: 100%; - left: 0; margin-top: 3px; z-index: 1000; min-width: 160px; @@ -21,7 +19,12 @@ opacity: 0; transform: scale(.2); transform-origin: 0 -45px; - transition: all .3s cubic-bezier(.87,-.41,.19,1.44); + transition: .3s cubic-bezier(.87,-.41,.19,1.44); + transition-property: transform, opacity; + + &.is-aligned-right { + transform-origin: 100% -45px; + } &.is-visible { pointer-events: all; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 025897ca1f..feb42c36d3 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -280,14 +280,6 @@ ul.notes { } } -.note-action-award-holder { - .emoji-menu { - left: auto; - right: -15px; - transform-origin: 100% -45px; - } -} - .note-award-control { display: block; diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index bd478e315d..0b4a084b9a 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -12,7 +12,7 @@ .note-actions - if current_user .award-menu-holder.note-action-award-holder.js-award-holder.js-award-action-btn{ data: { target: "##{dom_id(note)} .js-awards-block" } } - = link_to '#', title: 'Award emoji', class: 'note-award-control js-add-award', data: { award_menu_url: emojis_path } do + = link_to '#', title: 'Award emoji', class: 'note-award-control js-add-award', data: { award_menu_url: emojis_path, position: "right" } do = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) - if note_editable?(note) diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb index 46e1ed5d8c..209d1cca17 100644 --- a/spec/features/issues/award_spec.rb +++ b/spec/features/issues/award_spec.rb @@ -46,8 +46,9 @@ feature 'Issue awards', js: true, feature: true do page.within('.note') do find('.js-add-award').click - expect(page).to have_selector('.emoji-menu', count: 1) end + + expect(page).to have_selector('.emoji-menu', count: 1) end it 'should add award to note' do @@ -117,16 +118,14 @@ feature 'Issue awards', js: true, feature: true do def show_note_award_menu page.within('.note') do find('.js-add-award').click - expect(page).to have_selector('.emoji-menu') end + expect(page).to have_selector('.emoji-menu') end def award_on_note(index = 1) - page.within('.note') do - page.within('.emoji-menu') do - buttons = all('.js-emoji-btn') - buttons[index].click - end + page.within('.emoji-menu') do + buttons = all('.js-emoji-btn') + buttons[index].click end end diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb index 91e76e83c6..0f268bab5d 100644 --- a/spec/features/merge_requests/award_spec.rb +++ b/spec/features/merge_requests/award_spec.rb @@ -46,8 +46,9 @@ feature 'Merge request awards', js: true, feature: true do page.within('.note') do find('.js-add-award').click - expect(page).to have_selector('.emoji-menu', count: 1) end + + expect(page).to have_selector('.emoji-menu', count: 1) end it 'should add award to note' do @@ -117,16 +118,14 @@ feature 'Merge request awards', js: true, feature: true do def show_note_award_menu page.within('.note') do find('.js-add-award').click - expect(page).to have_selector('.emoji-menu') end + expect(page).to have_selector('.emoji-menu') end def award_on_note(index = 1) - page.within('.note') do - page.within('.emoji-menu') do - buttons = all('.js-emoji-btn') - buttons[index].click - end + page.within('.emoji-menu') do + buttons = all('.js-emoji-btn') + buttons[index].click end end From b40afae62c6df741d4b458289c3d44af5a43ca4e Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 19 May 2016 14:18:33 -0500 Subject: [PATCH 048/507] Add Gitlab namespace AwardEmoji. --- app/assets/javascripts/lib/emoji_aliases.js.coffee.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb index 66f640a3cb..97be65116e 100644 --- a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb +++ b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb @@ -1,2 +1,2 @@ window.emojiAliases = -> - JSON.parse('<%= AwardEmoji.aliases.to_json %>') + JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') From 4d6c51e2282c6416a8766fdf580a19fd08670609 Mon Sep 17 00:00:00 2001 From: ZJ van de Weg Date: Thu, 19 May 2016 15:00:50 -0500 Subject: [PATCH 049/507] Fix backend merge mistakes [ci skip] --- app/controllers/projects/merge_requests_controller.rb | 2 +- app/models/note.rb | 2 +- app/views/projects/notes/_note.html.haml | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 94eab37cb8..4a9ea4cbaa 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -303,7 +303,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.inc_author.fresh - @discussions = Note.discussions_from_notes(@notes) + @discussions = @notes.discussions @noteable = @merge_request # Get commits from repository diff --git a/app/models/note.rb b/app/models/note.rb index e9bef4fb9f..2b1a3f9069 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -193,7 +193,7 @@ class Note < ActiveRecord::Base end def award_emoji_supported? - noteable.is_a?(Awardable) && !for_diff_line? + noteable.is_a?(Awardable) && !line_code.present? end def contains_emoji_only? diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 0b4a084b9a..13d3f1ecea 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -38,8 +38,6 @@ = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) - if note_editable = render 'projects/notes/edit_form', note: note - .note-awards - = render 'emoji_awards/awards_block', awardable: note, inline: false = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note.attachment.url From 6d19d6a2fd51a68c150eb7bb8b1112cfadf0ef5d Mon Sep 17 00:00:00 2001 From: ZJ van de Weg Date: Fri, 20 May 2016 20:17:49 -0500 Subject: [PATCH 050/507] minor js fixes --- app/assets/javascripts/dispatcher.js.coffee | 4 +- app/assets/javascripts/notes.js.coffee | 2 +- app/assets/stylesheets/pages/notes.scss | 257 +++++++++++--------- app/views/projects/notes/_note.html.haml | 20 +- 4 files changed, 151 insertions(+), 132 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index ff8bcf8991..8ee00fb972 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -23,7 +23,7 @@ class Dispatcher new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() - window.awards_handler = new AwardsHandler() + window.awardsHandler = new AwardsHandler() when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() when 'dashboard:todos:index' @@ -54,7 +54,7 @@ class Dispatcher new Diff() shortcut_handler = new ShortcutsIssuable(true) new ZenMode() - window.awards_handler = new AwardsHandler() + window.awardsHandler = new AwardsHandler() when "projects:merge_requests:diffs" new Diff() new ZenMode() diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 6d9d6528f4..b8359c31df 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -167,7 +167,7 @@ class @Notes return if note.award - awardsHandler.addAwardToEmojiBar(note.note) + awardsHandler.addAwardToEmojiBar(note.name) awardsHandler.scrollToAwards() # render note if it not present in loaded list diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index feb42c36d3..a3e1ac13a4 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -20,9 +20,15 @@ ul.notes { .timeline-content { margin-left: 55px; + + &.timeline-content-form { + @media (max-width: $screen-sm-max) { + margin-left: 0; + } + } } - .note_created_ago, .note-updated-at { + .note-created-ago, .note-updated-at { white-space: nowrap; } @@ -39,53 +45,6 @@ ul.notes { } } - .discussion-header, - .note-header { - @extend .cgray; - - a:hover { - text-decoration: none; - } - - .avatar { - float: left; - margin-right: 10px; - } - - .discussion-last-update, - .note-last-update { - &:before { - content: "\00b7"; - } - - a { - color: $gl-gray; - - &:hover { - text-decoration: underline; - } - } - } - .author { - color: #4c4e54; - margin-right: 3px; - - &:hover { - color: $gl-link-color; - } - } - .author-username { - } - - .note-role { - float: right; - margin-top: 1px; - border: 1px solid #bbb; - background-color: transparent; - color: $gl-gray; - } - } - .discussion-body { padding-top: 15px; } @@ -99,12 +58,12 @@ ul.notes { .note { display: block; position: relative; + border-bottom: 1px solid $table-border-gray; &.is-editting { .note-header, .note-text, - .edited-text, - .note-awards { + .edited-text { display: none; } @@ -114,16 +73,16 @@ ul.notes { } .note-body { + overflow: auto; + .note-text { overflow: auto; word-wrap: break-word; @include md-typography; // On diffs code should wrap nicely and not overflow - pre { - code { - white-space: pre-wrap; - } + code { + white-space: pre-wrap; } // Reset ul style types since we're nested inside a ul already @@ -150,6 +109,10 @@ ul.notes { border-color: darken(#f5f5f5, 8%); margin: 10px 0; } + + code { + word-break: keep-all; + } } } @@ -157,9 +120,6 @@ ul.notes { padding-bottom: 3px; } - &:last-child { - border-bottom: 1px solid $border-color; - } } } @@ -177,60 +137,149 @@ ul.notes { font-family: $regular_font; td { - border: 1px solid #ddd; + border: 1px solid $table-border-gray; border-left: none; &.notes_line { vertical-align: middle; text-align: center; padding: 10px 0; - background: #fff; + background: $background-color; color: $text-color; } + &.notes_line2 { text-align: center; padding: 10px 0; border-left: 1px solid #ddd !important; } + &.notes_content { - background-color: #fff; + background-color: $background-color; border-width: 1px 0; - padding-top: 0; + padding: 0; vertical-align: top; - &.parallel{ + white-space: normal; + + &.parallel { border-width: 1px; } + + .notes { + background-color: $white-light; + } + + a code { + top: 0; + margin-right: 0; + } } } } +.discussion-header, +.note-header { + a { + color: inherit; + + &:hover { + color: $gl-link-color; + text-decoration: none; + } + } + + .author_link { + color: $gl-gray; + } +} + +.note-headline-light, +.discussion-headline-light { + color: $notes-light-color; +} + +.discussion-headline-light { + a { + color: $gl-link-color; + } +} + /** * Actions for Discussions/Notes */ -.discussion, -.note { - .discussion-actions, - .note-actions { - float: right; - margin-left: 10px; +.discussion-actions, +.note-actions { + float: right; + margin-left: 10px; + color: $notes-action-color; +} - a { - margin-left: 5px; - color: $gl-gray; +.discussion-actions { + @media (max-width: $screen-md-max) { + float: none; + margin-left: 0; - i.fa { - font-size: 16px; - line-height: 16px; - } + .note-action-button { + margin-left: 0; + } + } +} +.note-action-button { + display: inline-block; + margin-left: 10px; + line-height: 24px; + + .fa { + color: $notes-action-color; + position: relative; + top: 1px; + font-size: 17px; + } + + &.js-note-delete { + i { &:hover { - @extend .cgray; - &.danger { @extend .cred; } + color: $gl-text-red; + } + } + } + + &.js-note-edit { + i { + &:hover { + color: $gl-link-color; } } } } + +.discussion-toggle-button { + line-height: 20px; + font-size: 13px; + + .fa { + margin-right: 3px; + font-size: 10px; + line-height: 18px; + vertical-align: top; + } +} + +.note-role { + position: relative; + top: -2px; + display: inline-block; + padding-left: 4px; + padding-right: 4px; + color: $notes-role-color; + font-size: 12px; + line-height: 20px; + border: 1px solid $notes-role-border-color; + border-radius: $border-radius-base; +} + .diff-file .note .note-actions { right: 0; top: 0; @@ -243,8 +292,7 @@ ul.notes { .diff-file tr.line_holder { @mixin show-add-diff-note { - filter: alpha(opacity=100); - opacity: 1.0; + display: inline-block; } .add-diff-note { @@ -254,17 +302,12 @@ ul.notes { padding: 4px; font-size: 16px; color: $gl-link-color; - margin-left: -60px; + margin-left: -56px; position: absolute; z-index: 10; width: 32px; - - transition: all 0.2s ease; - // "hide" it by default - opacity: 0.0; - filter: alpha(opacity=0); - + display: none; &:hover { background: $gl-info; color: #fff; @@ -280,38 +323,20 @@ ul.notes { } } -.note-award-control { - display: block; +.disabled-comment { + margin-left: -$gl-padding-top; + margin-right: -$gl-padding-top; + background-color: $gray-light; + border-radius: $border-radius-base; + border: 1px solid $border-gray-normal; + color: $note-disabled-comment-color; + line-height: 200px; - &:hover, - &:focus { - text-decoration: none; + .disabled-comment-text { + line-height: normal; } - .award-control-icon-loading { - display: none; - } - - &.is-loading { - .award-control-icon-normal { - display: none; - } - - .award-control-icon-loading { - display: block; - } - } -} - -.note-awards { - .awards { - padding-top: 10px; - } - - .award-control { - padding-top: 2px; - padding-bottom: 2px; - color: #8f8f8f; - font-size: 13px; + a { + color: $gl-link-color; } } diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 13d3f1ecea..9fbc9a4554 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -9,20 +9,14 @@ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header + = link_to_member(note.project, note.author, avatar: false) + .inline.note-headline-light + = 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') .note-actions - - if current_user - .award-menu-holder.note-action-award-holder.js-award-holder.js-award-action-btn{ data: { target: "##{dom_id(note)} .js-awards-block" } } - = link_to '#', title: 'Award emoji', class: 'note-award-control js-add-award', data: { award_menu_url: emojis_path, position: "right" } do - = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) - = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) - - if note_editable?(note) - = link_to '#', title: 'Edit comment', class: 'js-note-edit' do - = icon('pencil-square-o') - - = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do - = icon('trash-o') - - - unless note.system - access = note.project.team.human_max_access(note.author.id) - if access %span.note-role From e8c8c9d7b275a9037d181e71ef77e29a5f649cf7 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 23 May 2016 21:25:45 +0300 Subject: [PATCH 051/507] Fix emoji counter issue. --- app/assets/javascripts/awards_handler.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index ff12cce485..64f93d21fd 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -108,7 +108,7 @@ class @AwardsHandler if @isActive($emojiBtn) @decrementCounter($emojiBtn, emoji) else - counter = $emojiBtn.siblings(".js-counter") + counter = $emojiBtn.find('.js-counter') counter.text(parseInt(counter.text()) + 1) $emojiBtn.addClass("active") @addMeToUserList(emoji) From 85e9eece7ecac4eaecd12c85de76ddf9c5acc241 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 23 May 2016 21:33:58 +0300 Subject: [PATCH 052/507] Fix displaying emoji only comments. To render emoji in the emoji bar of an issue or MR we actually need the emoji unicode and unicode info is stored in the emoji menu widget. That widget could only be visible if user clicks the "Add" button and there may not be a widget when posting emoji only comments. So this change will check existence of the widget and create it before posting the emoji if it's not exist to render it correctly. --- app/assets/javascripts/awards_handler.coffee | 66 +++++++++++++------- app/assets/javascripts/notes.js.coffee | 4 +- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 64f93d21fd..0ab3796118 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -20,7 +20,9 @@ class @AwardsHandler .off "click", ".js-emoji-btn" .on "click", ".js-emoji-btn", @handleClick + handleClick: (e) => + e.preventDefault() $emojiBtn = $(e.currentTarget) $addAwardBtn = $('.js-add-award.is-active') @@ -31,14 +33,14 @@ class @AwardsHandler else if $votesBlock.length is 0 $votesBlock = $addAwardBtn.closest('.js-awards-block') - $votesBlock.addClass 'js-awards-block-current' + $votesBlock.addClass 'js-awards-block' awardUrl = $votesBlock.data 'award-url' - emoji = $emojiBtn - .find(".icon") - .data "emoji" + emoji = $emojiBtn.find('.icon').data('emoji') @addAward awardUrl, emoji + showEmojiMenu: ($addBtn) -> + $menu = $('.emoji-menu') if $menu.length @@ -55,23 +57,29 @@ class @AwardsHandler $menu.addClass "is-visible" $("#emoji_search").focus() else - $addBtn.addClass "is-loading is-active" - $.get $addBtn.data('award-menu-url'), (response) => - $addBtn.removeClass "is-loading" - $('body').append response - - $menu = $(".emoji-menu") + $addBtn.addClass 'is-loading is-active' + url = $addBtn.data 'award-menu-url' + @createEmojiMenu url, => + $addBtn.removeClass 'is-loading' + $menu = $('.emoji-menu') @positionMenu($menu, $addBtn) - @renderFrequentlyUsedBlock() setTimeout => - $menu.addClass "is-visible" - $("#emoji_search").focus() + $menu.addClass 'is-visible' + $('#emoji_search').focus() @setupSearch() , 200 + + createEmojiMenu: (awardMenuUrl, callback) -> + + $.get awardMenuUrl, (response) => + $('body').append response + callback() + + positionMenu: ($menu, $addBtn) -> position = $addBtn.data('position') @@ -170,8 +178,9 @@ class @AwardsHandler ), 200 - createEmoji: (emoji) -> - emojiCssClass = @resolveNameToCssClass(emoji) + createEmoji_: (emoji) -> + + emojiCssClass = @resolveNameToCssClass emoji buttonHtml = "" emoji_node = $(buttonHtml) - .insertBefore(".js-awards-block-current .js-award-holder:not(.js-award-action-btn)") - .find(".emoji-icon") - .data("emoji", emoji) + .insertBefore '.js-awards-block .js-award-holder:not(.js-award-action-btn)' + .find '.emoji-icon' + .data 'emoji', emoji + $('.award-control').tooltip() - $currentBlock = $('.js-awards-block-current') - if $currentBlock.is('.hidden') + $currentBlock = $ '.js-awards-block' + + if $currentBlock.is '.hidden' $currentBlock.removeClass 'hidden' + + createEmoji: (emoji) -> + + return @createEmoji_ emoji if $('.emoji-menu').length + + awardMenuUrl = $('[data-award-menu-url]').data 'award-menu-url' + @createEmojiMenu awardMenuUrl, => @createEmoji emoji + + resolveNameToCssClass: (emoji) -> + emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") if emoji_icon.length > 0 @@ -197,7 +218,8 @@ class @AwardsHandler # Find by alias unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name") - "emoji-#{unicodeName}" + return "emoji-#{unicodeName}" + postEmoji: (awardUrl, emoji, callback) -> $.post awardUrl, { name: emoji }, (data) -> @@ -205,7 +227,7 @@ class @AwardsHandler callback.call() findEmojiIcon: (emoji) -> - $(".js-awards-block-current.awards > .js-emoji-btn [data-emoji='#{emoji}']") + $(".js-awards-block.awards > .js-emoji-btn [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 6d9d6528f4..687dc28551 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -167,8 +167,8 @@ class @Notes return if note.award - awardsHandler.addAwardToEmojiBar(note.note) - awardsHandler.scrollToAwards() + awards_handler.addAwardToEmojiBar(note.name) + awards_handler.scrollToAwards() # render note if it not present in loaded list # or skip if rendered From 5b046f8571ba360ec7b87d01db7b0933ab4388bf Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 23 May 2016 22:38:34 +0300 Subject: [PATCH 053/507] Fix variable name because of a wrong resolved merge conflict. --- app/assets/javascripts/notes.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 687dc28551..b8359c31df 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -167,8 +167,8 @@ class @Notes return if note.award - awards_handler.addAwardToEmojiBar(note.name) - awards_handler.scrollToAwards() + awardsHandler.addAwardToEmojiBar(note.name) + awardsHandler.scrollToAwards() # render note if it not present in loaded list # or skip if rendered From 1d7c3c79b1060e62ee1aa8a0e173334bb9180706 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Tue, 24 May 2016 03:10:38 +0300 Subject: [PATCH 054/507] Handle thumbsup and thumbsdown mutuality. --- app/assets/javascripts/awards_handler.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 0ab3796118..7235ec5785 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -24,6 +24,7 @@ class @AwardsHandler handleClick: (e) => e.preventDefault() + $emojiBtn = $(e.currentTarget) $addAwardBtn = $('.js-add-award.is-active') $votesBlock = $($addAwardBtn.closest('.js-award-holder').data('target')) @@ -36,6 +37,13 @@ class @AwardsHandler $votesBlock.addClass 'js-awards-block' awardUrl = $votesBlock.data 'award-url' emoji = $emojiBtn.find('.icon').data('emoji') + + if emoji in [ 'thumbsup', 'thumbsdown' ] + mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' + + isAlreadyVoted = $("[data-emoji=#{mutualVote}]").parent().hasClass 'active' + @addAward awardUrl, mutualVote if isAlreadyVoted + @addAward awardUrl, emoji From c697c9601d921b841fe791c4ea96a68aa6de0001 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Tue, 24 May 2016 03:44:19 +0300 Subject: [PATCH 055/507] Fix award tooltip after voting. --- app/assets/javascripts/awards_handler.coffee | 52 ++++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 7235ec5785..e535bd525b 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -156,26 +156,46 @@ class @AwardsHandler $emojiBtn.removeClass("active") + + getAwardTooltip: ($awardBlock) -> + + return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') + + removeMeFromUserList: ($emojiBtn, emoji) -> - award_block = $emojiBtn - authors = award_block - .attr("data-original-title") - .split(", ") - authors.splice(authors.indexOf("me"),1) - award_block - .closest(".js-emoji-btn") - .attr("data-original-title", authors.join(", ")) - @resetTooltip(award_block) + + awardBlock = $emojiBtn + originalTitle = @getAwardTooltip awardBlock + + authors = originalTitle.split ', ' + authors.splice authors.indexOf('me'), 1 + + newAuthors = authors.join ', ' + + awardBlock + .closest '.js-emoji-btn' + .removeData 'original-title' + .removeData 'title' + .attr 'data-original-title', newAuthors + .attr 'data-title', newAuthors + + @resetTooltip(awardBlock) + addMeToUserList: (emoji) -> - award_block = @findEmojiIcon(emoji).parent() - origTitle = award_block.attr("data-original-title").trim() - users = [] + + awardBlock = @findEmojiIcon(emoji).parent() + origTitle = @getAwardTooltip awardBlock + users = [] + if origTitle - users = origTitle.split(', ') - users.push("me") - award_block.attr("title", users.join(", ")) - @resetTooltip(award_block) + users = origTitle.trim().split(', ') + + users.push('me') + awardBlock.attr('title', users.join(", ")) + + @resetTooltip(awardBlock) + resetTooltip: (award) -> award.tooltip("destroy") From a29b45b061a0f2669818fde8fa324baac92b1d71 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 24 May 2016 16:57:26 +0100 Subject: [PATCH 056/507] Clicking search pill focuses field When clicking the pill in the search field, it now focus the field. Previously you would have to make sure you click in the field. Fixed an issue where clicking out of the field wouldn't remove the focus outline Also reduced some of the HTML to remove what isn't needed --- .../javascripts/search_autocomplete.js.coffee | 22 +++++++++---------- app/assets/stylesheets/pages/search.scss | 4 +++- app/views/layouts/_search.html.haml | 7 ++---- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 6a7b4ad1db..2122e80f57 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -20,8 +20,7 @@ class @SearchAutocomplete @dropdown = @wrap.find('.dropdown') @dropdownContent = @dropdown.find('.dropdown-content') - @locationBadgeEl = @getElement('.search-location-badge') - @locationText = @getElement('.location-text') + @locationBadgeEl = @getElement('.location-badge') @scopeInputEl = @getElement('#scope') @searchInput = @getElement('.search-input') @projectInputEl = @getElement('#search_project_id') @@ -133,7 +132,7 @@ class @SearchAutocomplete scope: @scopeInputEl.val() # Location badge - _location: @locationText.text() + _location: @locationBadgeEl.text() } bindEvents: -> @@ -143,12 +142,14 @@ class @SearchAutocomplete @searchInput.on 'click', @onSearchInputClick @searchInput.on 'focus', @onSearchInputFocus @clearInput.on 'click', @onClearInputClick + @locationBadgeEl.on 'click', => + @searchInput.focus() onDocumentClick: (e) => # If clicking outside the search box # And search input is not focused # And we are not clicking inside a suggestion - if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length + if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).closest('.search-form').length @onSearchInputBlur() enableAutocomplete: -> @@ -221,10 +222,8 @@ class @SearchAutocomplete category = if item.category? then "#{item.category}: " else '' value = if item.value? then item.value else '' - html = " - #{category}#{value} - " - @locationBadgeEl.html(html) + badgeText = "#{category}#{value}" + @locationBadgeEl.text(badgeText).show() @wrap.addClass('has-location-badge') restoreOriginalState: -> @@ -233,9 +232,8 @@ class @SearchAutocomplete for input in inputs @getElement("##{input}").val(@originalState[input]) - if @originalState._location is '' - @locationBadgeEl.empty() + @locationBadgeEl.hide() else @addLocationBadge( value: @originalState._location @@ -244,7 +242,7 @@ class @SearchAutocomplete @dropdown.removeClass 'open' badgePresent: -> - @locationBadgeEl.children().length + @locationBadgeEl.length resetSearchState: -> inputs = Object.keys @originalState @@ -257,7 +255,7 @@ class @SearchAutocomplete @getElement("##{input}").val('') removeLocationBadge: -> - @locationBadgeEl.empty() + @locationBadgeEl.hide() # Reset state @resetSearchState() diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 2bff70c8c6..037ad52054 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -28,6 +28,7 @@ } .search-input { + padding-right: 20px; border: none; font-size: 14px; outline: none; @@ -47,6 +48,7 @@ display: inline-block; background-color: $location-badge-bg; vertical-align: top; + cursor: default; } .search-input-container { @@ -55,7 +57,7 @@ position: relative; } - .search-location-badge, .search-input-wrap { + .search-input-wrap { // Fallback if flexbox is not supported display: inline-block; } diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 6b208c3d0b..b49207fc31 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -6,11 +6,8 @@ .search.search-form{class: "#{'has-location-badge' if label.present?}"} = form_tag search_path, method: :get, class: 'navbar-form' do |f| .search-input-container - .search-location-badge - - if label.present? - %span.location-badge - %i.location-text - = label + - if label.present? + .location-badge= label .search-input-wrap .dropdown{ data: {url: search_autocomplete_path } } = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } From b0082bec7305f9bf4322d8d532807e5607f5655a Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Tue, 24 May 2016 19:34:08 +0300 Subject: [PATCH 057/507] Get award menu url from gl object. --- app/assets/javascripts/awards_handler.coffee | 2 +- app/views/award_emoji/_awards_block.html.haml | 3 +++ app/views/emoji_awards/_awards_block.html.haml | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index e535bd525b..b4a341675b 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -232,7 +232,7 @@ class @AwardsHandler return @createEmoji_ emoji if $('.emoji-menu').length - awardMenuUrl = $('[data-award-menu-url]').data 'award-menu-url' + awardMenuUrl = gl.awardMenuUrl or '/emojis' @createEmojiMenu awardMenuUrl, => @createEmoji emoji diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 86931c70f3..19d1275a92 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -7,6 +7,9 @@ = awards.count - if current_user + :javascript + gl.awardMenuUrl = "#{emojis_path}" + .award-menu-holder.js-award-holder %button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) diff --git a/app/views/emoji_awards/_awards_block.html.haml b/app/views/emoji_awards/_awards_block.html.haml index 1b840d8875..e9b286b7c3 100644 --- a/app/views/emoji_awards/_awards_block.html.haml +++ b/app/views/emoji_awards/_awards_block.html.haml @@ -7,6 +7,9 @@ = awards.count - if current_user + :javascript + gl.awardMenuUrl = emojis_path + .award-menu-holder.js-award-holder %button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) From 20e6e0dbe609f7b0fdd480281223a1a98d645f34 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Tue, 24 May 2016 20:47:10 +0300 Subject: [PATCH 058/507] Handle posting emoji only vote +1 and -1 comments. And remove mutual votes. --- app/assets/javascripts/awards_handler.coffee | 54 +++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index b4a341675b..67f04dcac7 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -25,26 +25,9 @@ class @AwardsHandler e.preventDefault() - $emojiBtn = $(e.currentTarget) - $addAwardBtn = $('.js-add-award.is-active') - $votesBlock = $($addAwardBtn.closest('.js-award-holder').data('target')) - - if $addAwardBtn.length is 0 - $votesBlock = $emojiBtn.closest('.js-awards-block') - else if $votesBlock.length is 0 - $votesBlock = $addAwardBtn.closest('.js-awards-block') - - $votesBlock.addClass 'js-awards-block' - awardUrl = $votesBlock.data 'award-url' - emoji = $emojiBtn.find('.icon').data('emoji') - - if emoji in [ 'thumbsup', 'thumbsdown' ] - mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' - - isAlreadyVoted = $("[data-emoji=#{mutualVote}]").parent().hasClass 'active' - @addAward awardUrl, mutualVote if isAlreadyVoted - - @addAward awardUrl, emoji + emoji = $(e.currentTarget).find('.icon').data 'emoji' + @getVotesBlock().addClass 'js-awards-block' + @addAward @getAwardUrl(), emoji showEmojiMenu: ($addBtn) -> @@ -105,16 +88,21 @@ class @AwardsHandler $menu.css(css) - addAward: (awardUrl, emoji) -> + + addAward: (awardUrl, emoji, checkMutuality = yes) -> + emoji = @normilizeEmojiName(emoji) @postEmoji awardUrl, emoji, => - @addAwardToEmojiBar(emoji) + @addAwardToEmojiBar(emoji, checkMutuality) $('.js-awards-block-current').removeClass 'js-awards-block-current' - $(".emoji-menu").removeClass "is-visible" + $('.emoji-menu').removeClass 'is-visible' - addAwardToEmojiBar: (emoji) -> + + addAwardToEmojiBar: (emoji, checkForMutuality = yes) -> + + @checkMutuality emoji if checkForMutuality @addEmojiToFrequentlyUsedList(emoji) emoji = @normilizeEmojiName(emoji) @@ -131,6 +119,24 @@ class @AwardsHandler else @createEmoji(emoji) + + getVotesBlock: -> return $ '.awards.js-awards-block' + + + getAwardUrl: -> @getVotesBlock().data 'award-url' + + + checkMutuality: (emoji) -> + + awardUrl = @getAwardUrl() + + if emoji in [ 'thumbsup', 'thumbsdown' ] + mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' + + isAlreadyVoted = $("[data-emoji=#{mutualVote}]").parent().hasClass 'active' + @addAward awardUrl, mutualVote, no if isAlreadyVoted + + isActive: ($emojiBtn) -> $emojiBtn.hasClass("active") From 53866711d4c4f728f86d061984437bdd65f15bed Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 17 May 2016 17:12:23 +0100 Subject: [PATCH 059/507] Show loading indicator for autocomplete Closes #15435 --- .../javascripts/gfm_auto_complete.js.coffee | 78 +++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 41dba34210..b13a431a52 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -22,6 +22,24 @@ GitLab.GfmAutoComplete = Milestones: template: '
                              • ${title}
                              • ' + Loading: + template: '
                              • Loading...
                              • ' + + DefaultOptions: + sorter: (query, items, searchKey) -> + return items if items[0].name? and items[0].name is 'loading' + + $.fn.atwho.default.callbacks.sorter(query, items, searchKey) + filter: (query, data, searchKey) -> + return data if data[0] is 'loading' + + $.fn.atwho.default.callbacks.filter(query, data, searchKey) + beforeInsert: (value) -> + if value.indexOf('undefined') + @at + else + value + # Add GFM auto-completion to all input fields, that accept GFM input. setup: (wrap) -> @input = $('.js-gfm-input') @@ -53,18 +71,37 @@ GitLab.GfmAutoComplete = # Emoji @input.atwho at: ':' - displayTpl: @Emoji.template + displayTpl: (value) => + if value.path? + @Emoji.template + else + @Loading.template insertTpl: ':${name}:' + data: ['loading'] + callbacks: + sorter: @DefaultOptions.sorter + filter: @DefaultOptions.filter + beforeInsert: @DefaultOptions.beforeInsert # Team Members @input.atwho at: '@' - displayTpl: @Members.template + displayTpl: (value) => + if value.username? + @Members.template + else + @Loading.template insertTpl: '${atwho-at}${username}' searchKey: 'search' + data: ['loading'] callbacks: + sorter: @DefaultOptions.sorter + filter: @DefaultOptions.filter + beforeInsert: @DefaultOptions.beforeInsert beforeSave: (members) -> $.map members, (m) -> + return m if not m.username? + title = m.name title += " (#{m.count})" if m.count @@ -76,11 +113,21 @@ GitLab.GfmAutoComplete = at: '#' alias: 'issues' searchKey: 'search' - displayTpl: @Issues.template + displayTpl: (value) => + if value.title? + @Issues.template + else + @Loading.template + data: ['loading'] insertTpl: '${atwho-at}${id}' callbacks: + sorter: @DefaultOptions.sorter + filter: @DefaultOptions.filter + beforeInsert: @DefaultOptions.beforeInsert beforeSave: (issues) -> $.map issues, (i) -> + return i if not i.title? + id: i.iid title: sanitize(i.title) search: "#{i.iid} #{i.title}" @@ -89,11 +136,18 @@ GitLab.GfmAutoComplete = at: '%' alias: 'milestones' searchKey: 'search' - displayTpl: @Milestones.template + displayTpl: (value) => + if value.title? + @Milestones.template + else + @Loading.template insertTpl: '${atwho-at}"${title}"' + data: ['loading'] callbacks: beforeSave: (milestones) -> $.map milestones, (m) -> + return m if not m.title? + id: m.iid title: sanitize(m.title) search: "#{m.title}" @@ -102,11 +156,21 @@ GitLab.GfmAutoComplete = at: '!' alias: 'mergerequests' searchKey: 'search' - displayTpl: @Issues.template + displayTpl: (value) => + if value.title? + @Issues.template + else + @Loading.template + data: ['loading'] insertTpl: '${atwho-at}${id}' callbacks: + sorter: @DefaultOptions.sorter + filter: @DefaultOptions.filter + beforeInsert: @DefaultOptions.beforeInsert beforeSave: (merges) -> $.map merges, (m) -> + return m if not m.title? + id: m.iid title: sanitize(m.title) search: "#{m.iid} #{m.title}" @@ -128,3 +192,7 @@ GitLab.GfmAutoComplete = @input.atwho 'load', 'mergerequests', data.mergerequests # load emojis @input.atwho 'load', ':', data.emojis + + # This trigger at.js again + # otherwise we would be stuck with loading until the user types + $(':focus').trigger('keyup') From c2bd71fda65d5e2eae5f4c36bb8548a7a8c1937e Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 25 May 2016 16:05:56 +0300 Subject: [PATCH 060/507] " :arrow_right: '. --- app/assets/javascripts/awards_handler.coffee | 85 ++++++++++---------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 67f04dcac7..ffe060e8c2 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,24 +1,26 @@ class @AwardsHandler + constructor: -> + @aliases = emojiAliases() $(document) - .off "click", ".js-add-award" - .on "click", ".js-add-award", (event) => + .off 'click', '.js-add-award' + .on 'click', '.js-add-award', (event) => event.stopPropagation() event.preventDefault() @showEmojiMenu $(event.currentTarget) - $("html").on 'click', (event) -> - if !$(event.target).closest(".emoji-menu").length - if $(".emoji-menu").is(":visible") + $('html').on 'click', (event) -> + unless $(event.target).closest('.emoji-menu').length + if $('.emoji-menu').is(':visible') $('.js-add-award.is-active').removeClass 'is-active' - $(".emoji-menu").removeClass "is-visible" + $('.emoji-menu').removeClass 'is-visible' $(document) - .off "click", ".js-emoji-btn" - .on "click", ".js-emoji-btn", @handleClick + .off 'click', '.js-emoji-btn' + .on 'click', '.js-emoji-btn', @handleClick handleClick: (e) => @@ -37,16 +39,16 @@ class @AwardsHandler if $menu.length $holder = $addBtn.closest('.js-award-holder') - if $menu.is ".is-visible" - $addBtn.removeClass "is-active" - $menu.removeClass "is-visible" - $("#emoji_search").blur() + if $menu.is '.is-visible' + $addBtn.removeClass 'is-active' + $menu.removeClass 'is-visible' + $('#emoji_search').blur() else - $addBtn.addClass "is-active" + $addBtn.addClass 'is-active' @positionMenu($menu, $addBtn) - $menu.addClass "is-visible" - $("#emoji_search").focus() + $menu.addClass 'is-visible' + $('#emoji_search').focus() else $addBtn.addClass 'is-loading is-active' url = $addBtn.data 'award-menu-url' @@ -81,10 +83,10 @@ class @AwardsHandler if position? and position is 'right' css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px" - $menu.addClass "is-aligned-right" + $menu.addClass 'is-aligned-right' else css.left = "#{$addBtn.offset().left}px" - $menu.removeClass "is-aligned-right" + $menu.removeClass 'is-aligned-right' $menu.css(css) @@ -114,7 +116,7 @@ class @AwardsHandler else counter = $emojiBtn.find('.js-counter') counter.text(parseInt(counter.text()) + 1) - $emojiBtn.addClass("active") + $emojiBtn.addClass('active') @addMeToUserList(emoji) else @createEmoji(emoji) @@ -137,8 +139,8 @@ class @AwardsHandler @addAward awardUrl, mutualVote, no if isAlreadyVoted - isActive: ($emojiBtn) -> - $emojiBtn.hasClass("active") + isActive: ($emojiBtn) -> $emojiBtn.hasClass 'active' + decrementCounter: ($emojiBtn, emoji) -> isntNoteBody = $emojiBtn.closest('.note-body').length is 0 @@ -152,15 +154,15 @@ class @AwardsHandler if counterNumber > 1 counter.text(counterNumber - 1) @removeMeFromUserList($emojiBtn, emoji) - else if (emoji == "thumbsup" || emoji == "thumbsdown") && isntNoteBody - $emojiBtn.tooltip("destroy") + else if (emoji == 'thumbsup' || emoji == 'thumbsdown') && isntNoteBody + $emojiBtn.tooltip('destroy') counter.text('0') @removeMeFromUserList($emojiBtn, emoji) else - $emojiBtn.tooltip("destroy") + $emojiBtn.tooltip('destroy') $emojiBtn.remove() - $emojiBtn.removeClass("active") + $emojiBtn.removeClass('active') getAwardTooltip: ($awardBlock) -> @@ -194,19 +196,20 @@ class @AwardsHandler origTitle = @getAwardTooltip awardBlock users = [] + if origTitle if origTitle users = origTitle.trim().split(', ') users.push('me') - awardBlock.attr('title', users.join(", ")) + awardBlock.attr('title', users.join(', ')) @resetTooltip(awardBlock) resetTooltip: (award) -> - award.tooltip("destroy") + award.tooltip('destroy') - # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. + # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. setTimeout (-> award.tooltip() ), 200 @@ -247,10 +250,10 @@ class @AwardsHandler emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") if emoji_icon.length > 0 - unicodeName = emoji_icon.data("unicode-name") + unicodeName = emoji_icon.data('unicode-name') else # Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name") + unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') return "emoji-#{unicodeName}" @@ -274,10 +277,10 @@ class @AwardsHandler addEmojiToFrequentlyUsedList: (emoji) -> frequently_used_emojis = @getFrequentlyUsedEmojis() frequently_used_emojis.push(emoji) - $.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 }) + $.cookie('frequently_used_emojis', frequently_used_emojis.join(','), { expires: 365 }) getFrequentlyUsedEmojis: -> - frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",") + frequently_used_emojis = ($.cookie('frequently_used_emojis') || '').split(',') _.compact(_.uniq(frequently_used_emojis)) renderFrequentlyUsedBlock: -> @@ -287,26 +290,26 @@ class @AwardsHandler ul = $("
                                  ") for emoji in frequently_used_emojis - $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) + $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) - $("input.emoji-search").after(ul).after($("
                                  ").text("Frequently used")) + $('input.emoji-search').after(ul).after($('
                                  ').text('Frequently used')) setupSearch: -> - $("input.emoji-search").on 'keyup', (ev) => + $('input.emoji-search').on 'keyup', (ev) => term = $(ev.target).val() # Clean previous search results - $("ul.emoji-menu-search, h5.emoji-search").remove() + $('ul.emoji-menu-search, h5.emoji-search').remove() if term # Generate a search result block - h5 = $("
                                  ").text("Search results").addClass("emoji-search") + h5 = $('
                                  ').text('Search results').addClass('emoji-search') found_emojis = @searchEmojis(term).show() - ul = $("
                                    ").addClass("emoji-menu-list emoji-menu-search").append(found_emojis) - $(".emoji-menu-content ul, .emoji-menu-content h5").hide() - $(".emoji-menu-content").append(h5).append(ul) + ul = $('
                                      ').addClass('emoji-menu-list emoji-menu-search').append(found_emojis) + $('.emoji-menu-content ul, .emoji-menu-content h5').hide() + $('.emoji-menu-content').append(h5).append(ul) else - $(".emoji-menu-content").children().show() + $('.emoji-menu-content').children().show() searchEmojis: (term)-> - $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() + $(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone() From 9777a3d39de8c483ab1a3a6dc325b0a7f32eff68 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 25 May 2016 16:34:39 +0300 Subject: [PATCH 061/507] Remove accidentally duplicated line. :facepalm: --- app/assets/javascripts/awards_handler.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index ffe060e8c2..766c653111 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -196,7 +196,6 @@ class @AwardsHandler origTitle = @getAwardTooltip awardBlock users = [] - if origTitle if origTitle users = origTitle.trim().split(', ') From 6523002267e63e301693c141f6de2c6bbf6e2e73 Mon Sep 17 00:00:00 2001 From: ZJ van de Weg Date: Wed, 25 May 2016 15:43:07 +0200 Subject: [PATCH 062/507] Remove old tests, and use right factories --- features/steps/project/issues/issues.rb | 10 ++-- features/steps/project/merge_requests.rb | 8 ++-- spec/models/note_spec.rb | 60 ------------------------ 3 files changed, 9 insertions(+), 69 deletions(-) diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 78ddaee877..530ba60430 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -192,14 +192,14 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do awardable = Issue.find_by(title: 'Release 0.4') - create_list(:upvote, 2, project: project, awardable: awardable) - create(:downvote, project: project, awardable: awardable) + create_list(:award_emoji, 2, awardable: awardable) + create(:award_emoji, :downvote, awardable: awardable) end step 'issue "Tweet control" have 1 upvote and 2 downvotes' do - issue = Issue.find_by(title: 'Tweet control') - create(:upvote, project: project, noteable: issue) - create_list(:downvote, 2, project: project, noteable: issue) + awardable = Issue.find_by(title: 'Tweet control') + create(:award_emoji, :upvote, awardable: awardable) + create_list(:award_emoji, 2, awardable: awardable, name: 'thumbsdown') end step 'The list should be sorted by "Least popular"' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index bdb126c6ed..692a01c7f7 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do merge_request = MergeRequest.find_by(title: 'Bug NS-04') - create_list(:upvote, 2, project: project, awardable: merge_request) - create(:downvote, project: project, awardable: merge_request) + create_list(:award_emoji, 2, awardable: merge_request) + create(:award_emoji, :downvote, awardable: merge_request) end step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do awardable = MergeRequest.find_by(title: 'Bug NS-06') - create(:upvote, project: project, awardable: awardable) - create_list(:downvote, 2, project: project, awardable: awardable) + create(:award_emoji, awardable: awardable) + create_list(:award_emoji, 2, awardable: awardable, name: "thumbsdown") end step 'The list should be sorted by "Least popular"' do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 4448eefad0..a3ab1b796b 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -113,66 +113,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) From f99b38c9d5dab11677b2680a671c1a66fb69283b Mon Sep 17 00:00:00 2001 From: ZJ van de Weg Date: Wed, 25 May 2016 21:27:36 +0200 Subject: [PATCH 063/507] Remove tests specific for awards on notes --- spec/features/issues/award_spec.rb | 67 ---------------------- spec/features/merge_requests/award_spec.rb | 67 ---------------------- 2 files changed, 134 deletions(-) diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb index 209d1cca17..eafd517c84 100644 --- a/spec/features/issues/award_spec.rb +++ b/spec/features/issues/award_spec.rb @@ -24,79 +24,12 @@ feature 'Issue awards', js: true, feature: true do expect(first('.js-emoji-btn')).to have_content '0' end - it 'should show award menu button in notes' do - page.within('.note') do - expect(page).to have_selector('.js-award-action-btn') - end - end - - it 'should not show award bar on note if no awards given' do - page.within('.note') do - expect(find('.js-awards-block', visible: false)).not_to be_visible - end - end - - it 'should be able to show award menu when clicking add award button in note' do - show_note_award_menu - end - it 'should only have one menu on the page' do first('.js-add-award').click expect(page).to have_selector('.emoji-menu') - page.within('.note') do - find('.js-add-award').click - end - expect(page).to have_selector('.emoji-menu', count: 1) end - - it 'should add award to note' do - show_note_award_menu - award_on_note - - page.within('.note') do - expect(find('.js-awards-block')).to be_visible - expect(find('.js-awards-block')).to have_selector('.active') - end - end - - it 'should remove award from note' do - show_note_award_menu - award_on_note - - page.within('.note') do - expect(find('.js-awards-block')).to be_visible - expect(find('.js-awards-block')).to have_selector('.active') - end - - remove_award_on_note - sleep 0.5 - - page.within('.note') do - expect(find('.js-awards-block', visible: false)).not_to be_visible - expect(find('.js-awards-block', visible: false)).not_to have_selector('.active') - end - end - - it 'should not hide award bar on notes with more than 1 award' do - show_note_award_menu - award_on_note - - show_note_award_menu - award_on_note(2) - - page.within('.note') do - expect(find('.js-awards-block')).to be_visible - expect(find('.js-awards-block')).to have_selector('.active') - end - - remove_award_on_note - - page.within('.note') do - expect(find('.js-awards-block')).to be_visible - end - end end describe 'logged out' do diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb index 0f268bab5d..4d3e8173eb 100644 --- a/spec/features/merge_requests/award_spec.rb +++ b/spec/features/merge_requests/award_spec.rb @@ -24,79 +24,12 @@ feature 'Merge request awards', js: true, feature: true do expect(first('.js-emoji-btn')).to have_content '0' end - it 'should show award menu button in notes' do - page.within('.note') do - expect(page).to have_selector('.js-award-action-btn') - end - end - - it 'should not show award bar on note if no awards given' do - page.within('.note') do - expect(find('.js-awards-block', visible: false)).not_to be_visible - end - end - - it 'should be able to show award menu when clicking add award button in note' do - show_note_award_menu - end - it 'should only have one menu on the page' do first('.js-add-award').click expect(page).to have_selector('.emoji-menu') - page.within('.note') do - find('.js-add-award').click - end - expect(page).to have_selector('.emoji-menu', count: 1) end - - it 'should add award to note' do - show_note_award_menu - award_on_note - - page.within('.note') do - expect(find('.js-awards-block')).to be_visible - expect(find('.js-awards-block')).to have_selector('.active') - end - end - - it 'should remove award from note' do - show_note_award_menu - award_on_note - - page.within('.note') do - expect(find('.js-awards-block')).to be_visible - expect(find('.js-awards-block')).to have_selector('.active') - end - - remove_award_on_note - sleep 0.5 - - page.within('.note') do - expect(find('.js-awards-block', visible: false)).not_to be_visible - expect(find('.js-awards-block', visible: false)).not_to have_selector('.active') - end - end - - it 'should not hide award bar on notes with more than 1 award' do - show_note_award_menu - award_on_note - - show_note_award_menu - award_on_note(2) - - page.within('.note') do - expect(find('.js-awards-block')).to be_visible - expect(find('.js-awards-block')).to have_selector('.active') - end - - remove_award_on_note - - page.within('.note') do - expect(find('.js-awards-block')).to be_visible - end - end end describe 'logged out' do From 97aecdeadeb85383a793135f92677daf99c6183e Mon Sep 17 00:00:00 2001 From: Daniel Beyer Date: Fri, 27 May 2016 13:35:12 +0200 Subject: [PATCH 064/507] Fix bug with SQL syntax error during backup restoration closes #15259 --- lib/tasks/gitlab/db.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 86f5d65f12..60234c872e 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -34,7 +34,7 @@ namespace :gitlab do # PG: http://www.postgresql.org/docs/current/static/ddl-depend.html # MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html # Add `IF EXISTS` because cascade could have already deleted a table. - tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{t} CASCADE") } + tables.each { |t| connection.execute("DROP TABLE IF EXISTS `#{t}` CASCADE") } end desc 'Configures the database by running migrate, or by loading the schema and seeding if needed' From ccb20c1622b30eeec5951bbe6431a3df53dfe7ed Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 30 May 2016 18:53:07 -0600 Subject: [PATCH 065/507] Remove unnecessary vendor prefixes for browsers we no longer support. --- app/assets/stylesheets/framework/mixins.scss | 8 -------- app/assets/stylesheets/pages/builds.scss | 7 +------ app/assets/stylesheets/pages/search.scss | 2 -- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 250d630929..828e722423 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -2,18 +2,10 @@ * Generic mixins */ @mixin box-shadow($shadow) { - -webkit-box-shadow: $shadow; - -moz-box-shadow: $shadow; - -ms-box-shadow: $shadow; - -o-box-shadow: $shadow; box-shadow: $shadow; } @mixin border-radius($radius) { - -webkit-border-radius: $radius; - -moz-border-radius: $radius; - -ms-border-radius: $radius; - -o-border-radius: $radius; border-radius: $radius; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index aa41565f81..44222e8e8a 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -3,12 +3,7 @@ background: #111; color: #fff; font-family: $monospace_font; - white-space: pre; - white-space: pre-wrap; /* css-3 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ + white-space: pre-wrap; overflow: auto; overflow-y: hidden; font-size: 12px; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 2bff70c8c6..ed8fcf84f5 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -156,13 +156,11 @@ .search-holder { @media (min-width: $screen-sm-min) { display: -webkit-flex; - display: -ms-flexbox; display: flex; } .search-field-holder { -webkit-flex: 1 0 auto; - -ms-flex: 1 0 auto; flex: 1 0 auto; position: relative; margin-right: 0; From 7edbce65b0cd8c420707904a6733c84fadf5d224 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 30 May 2016 23:05:02 -0400 Subject: [PATCH 066/507] Add tooltips to todo target links --- app/helpers/todos_helper.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index b9d7edb418..b4923fbb13 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -17,7 +17,9 @@ module TodosHelper def todo_target_link(todo) target = todo.target_type.titleize.downcase - link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title } + link_to "#{target} #{todo.target_reference}", todo_target_path(todo), + class: 'has-tooltip', + title: todo.target.title end def todo_target_path(todo) From 91a7b9333b660abc866e52e1a614151cb529413d Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 1 Jun 2016 11:23:09 +0200 Subject: [PATCH 067/507] Incorportate feedback --- app/models/concerns/issuable.rb | 4 ++ app/models/legacy_diff_note.rb | 4 ++ app/models/note.rb | 16 +------- app/services/notes/post_process_service.rb | 2 +- app/services/notification_service.rb | 2 +- app/views/award_emoji/_awards_block.html.haml | 8 ++-- .../emoji_awards/_awards_block.html.haml | 18 --------- config/routes.rb | 1 - db/migrate/20160416180807_add_award_emoji.rb | 3 +- ...82152_convert_award_note_to_emoji_award.rb | 2 + db/schema.rb | 3 +- features/steps/project/merge_requests.rb | 2 +- lib/api/entities.rb | 2 + lib/gitlab/award_emoji.rb | 18 +++++---- spec/controllers/groups_controller_spec.rb | 4 +- .../projects/issues_controller_spec.rb | 6 +-- spec/factories/award_emoji.rb | 6 --- spec/features/issues/award_spec.rb | 35 +++--------------- spec/features/merge_requests/award_spec.rb | 37 ++++--------------- spec/features/notes_on_merge_requests_spec.rb | 25 ------------- spec/models/award_emoji_spec.rb | 3 +- 21 files changed, 52 insertions(+), 149 deletions(-) delete mode 100644 app/views/emoji_awards/_awards_block.html.haml diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 57ded0f91a..1fc0e002f4 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -133,6 +133,10 @@ module Issuable opened? || reopened? end + def user_notes_count + notes.user.count + end + def subscribed_without_subscriptions?(user) participants(user).include?(user) end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index bbefc911b2..95fd510eb3 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -110,6 +110,10 @@ class LegacyDiffNote < Note @active end + def award_emoji_supported? + false + end + private def find_diff diff --git a/app/models/note.rb b/app/models/note.rb index bbe5545dc8..f99d327a5b 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -21,10 +21,8 @@ class Note < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true delegate :title, to: :noteable, allow_nil: true - before_validation :clear_blank_line_code! - validates :note, :project, presence: true - 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 } @@ -173,10 +171,6 @@ class Note < ActiveRecord::Base Event.reset_event_cache_for(self) end - def system? - read_attribute(:system) - end - def editable? !system? end @@ -193,14 +187,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 award_emoji_supported? - noteable.is_a?(Awardable) && !line_code.present? + noteable.is_a?(Awardable) end def contains_emoji_only? diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index c1bf46bdfb..534c48aeff 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -8,7 +8,7 @@ module Notes def execute # Skip system notes, like status changes and cross-references and awards - unless @note.system + unless @note.system? EventCreateService.new.leave_note(@note, @note.author) @note.create_cross_references! execute_note_hooks diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 703636658b..91ca82ed3b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -130,7 +130,7 @@ class NotificationService # ignore gitlab service messages return true if note.note.start_with?('Status changed to closed') - return true if note.cross_reference? && note.system == true + return true if note.cross_reference? && note.system? target = note.noteable diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 19d1275a92..f1f93c1375 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,7 +1,7 @@ - grouped_emojis = awardable.grouped_awards(with_thumbs: inline) -.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.size == 0), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } +.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } - awards_sort(grouped_emojis).each do |emoji, awards| - %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)),data: { placement: "bottom", title: award_user_list(awards, current_user) } } + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count @@ -12,7 +12,7 @@ .award-menu-holder.js-award-holder %button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } - = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) - = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) + = icon('smile-o', class: "award-control-icon award-control-icon-normal") + = icon('spinner spin', class: "award-control-icon award-control-icon-loading") %span.award-control-text Add diff --git a/app/views/emoji_awards/_awards_block.html.haml b/app/views/emoji_awards/_awards_block.html.haml deleted file mode 100644 index e9b286b7c3..0000000000 --- a/app/views/emoji_awards/_awards_block.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- grouped_emojis = awardable.grouped_awards(inline) -.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.size == 0), data: { award_url: url_for([:toggle_emoji_award, @project.namespace.becomes(Namespace), @project, awardable]) } } - - awards_sort(grouped_emojis).each do |emoji, awards| - %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), title: award_user_list(awards, current_user), data: { placement: "bottom" } } - = emoji_icon(emoji) - %span.award-control-text.js-counter - = awards.count - - - if current_user - :javascript - gl.awardMenuUrl = emojis_path - - .award-menu-holder.js-award-holder - %button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } - = icon('smile-o', {class: "award-control-icon award-control-icon-normal"}) - = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) - %span.award-control-text - Add diff --git a/config/routes.rb b/config/routes.rb index 85760409c1..cd28c421fe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -754,7 +754,6 @@ Rails.application.routes.draw do resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do member do delete :delete_attachment - post :toggle_award_emoji end end diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb index 3177b86a13..2ead181921 100644 --- a/db/migrate/20160416180807_add_award_emoji.rb +++ b/db/migrate/20160416180807_add_award_emoji.rb @@ -9,7 +9,6 @@ class AddAwardEmoji < ActiveRecord::Migration end add_index :award_emoji, :user_id - add_index :award_emoji, :awardable_type - add_index :award_emoji, :awardable_id + add_index :award_emoji, [:awardable_type, :awardable_id] end end diff --git a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb index d2efbd0abe..073bbc0fc2 100644 --- a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb +++ b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb @@ -2,6 +2,8 @@ class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration def change def up execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)" + + execute "DELETE FROM notes WHERE is_award = true" end end end diff --git a/db/schema.rb b/db/schema.rb index 9ef68efc4e..e784f24bb8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -108,8 +108,7 @@ ActiveRecord::Schema.define(version: 20160528043124) do t.datetime "updated_at" end - add_index "award_emoji", ["awardable_id"], name: "index_award_emoji_on_awardable_id", using: :btree - add_index "award_emoji", ["awardable_type"], name: "index_award_emoji_on_awardable_type", using: :btree + add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree create_table "broadcast_messages", force: :cascade do |t| diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index f6fafe8119..11978aa65a 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -186,7 +186,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do awardable = MergeRequest.find_by(title: 'Bug NS-06') create(:award_emoji, awardable: awardable) - create_list(:award_emoji, 2, awardable: awardable, name: "thumbsdown") + create_list(:award_emoji, 2, :downvote, awardable: awardable) end step 'The list should be sorted by "Least popular"' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 43e0ba388d..a5582490a3 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -175,6 +175,7 @@ module API expose :subscribed do |issue, options| issue.subscribed?(options[:current_user]) end + expose :user_notes_count expose :upvotes, :downvotes end @@ -192,6 +193,7 @@ module API expose :subscribed do |merge_request, options| merge_request.subscribed?(options[:current_user]) end + expose :user_notes_count end class MergeRequestChanges < MergeRequest diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb index 0ae220a86b..51b1df9ecb 100644 --- a/lib/gitlab/award_emoji.rb +++ b/lib/gitlab/award_emoji.rb @@ -47,17 +47,19 @@ module Gitlab end def self.emojis - @emojis ||= begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) - JSON.parse(File.read(json_path)) - end + @emojis ||= + begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) + JSON.parse(File.read(json_path)) + end end def self.aliases - @aliases ||= begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) - JSON.parse(File.read(json_path)) - end + @aliases ||= + begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) + JSON.parse(File.read(json_path)) + end end # Returns an Array of Emoji names and their asset URLs. diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 82b2570217..cd98fecd0c 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -33,7 +33,7 @@ describe GroupsController do before do create_list(:award_emoji, 3, awardable: issue_2) create_list(:award_emoji, 2, awardable: issue_1) - create_list(:award_emoji, 2, awardable: issue_2, name: "thumbsdown") + create_list(:award_emoji, 2, :downvote, awardable: issue_2,) sign_in(user) end @@ -58,7 +58,7 @@ describe GroupsController do before do create_list(:award_emoji, 3, awardable: merge_request_2) create_list(:award_emoji, 2, awardable: merge_request_1) - create_list(:award_emoji, 2, awardable: merge_request_2, name: "thumbsdown") + create_list(:award_emoji, 2, :downvote, awardable: merge_request_2) sign_in(user) end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 849816b9ca..78be7e3dc3 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -258,10 +258,10 @@ describe Projects::IssuesController do end it "toggles the award emoji" do - expect do + expect do post(:toggle_award_emoji, namespace_id: project.namespace.path, - project_id: project.path, id: issue.iid, name: "thumbsup") - end.to change { AwardEmoji.count }.by(1) + project_id: project.path, id: issue.iid, name: "thumbsup") + end.to change { issue.award_emoji.count }.by(1) expect(response.status).to eq(200) end diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb index b09f8b0bc7..4b858df52c 100644 --- a/spec/factories/award_emoji.rb +++ b/spec/factories/award_emoji.rb @@ -4,13 +4,7 @@ FactoryGirl.define do user awardable factory: :issue - trait :thumbs_up trait :upvote - - trait :thumbs_down do - name "thumbsdown" - end - trait :downvote do name "thumbsdown" end diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb index eafd517c84..63efecf878 100644 --- a/spec/features/issues/award_spec.rb +++ b/spec/features/issues/award_spec.rb @@ -4,7 +4,6 @@ feature 'Issue awards', js: true, feature: true do let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project) } - let!(:note) { create(:note_on_issue, project: project, noteable: issue, note: 'Looks good!') } describe 'logged in' do before do @@ -16,12 +15,18 @@ feature 'Issue awards', js: true, feature: true do first('.js-emoji-btn').click expect(page).to have_selector('.js-emoji-btn.active') expect(first('.js-emoji-btn')).to have_content '1' + + visit namespace_project_issue_path(project.namespace, project, issue) + expect(first('.js-emoji-btn')).to have_content '1' end it 'should remove award from issue' do first('.js-emoji-btn').click find('.js-emoji-btn.active').click expect(first('.js-emoji-btn')).to have_content '0' + + visit namespace_project_issue_path(project.namespace, project, issue) + expect(first('.js-emoji-btn')).to have_content '0' end it 'should only have one menu on the page' do @@ -40,33 +45,5 @@ feature 'Issue awards', js: true, feature: true do it 'should not see award menu button' do expect(page).not_to have_selector('.js-award-holder') end - - it 'should not see award menu button in note' do - page.within('.note') do - expect(page).not_to have_selector('.js-award-action-btn') - end - end - end - - def show_note_award_menu - page.within('.note') do - find('.js-add-award').click - end - expect(page).to have_selector('.emoji-menu') - end - - def award_on_note(index = 1) - page.within('.emoji-menu') do - buttons = all('.js-emoji-btn') - buttons[index].click - end - end - - def remove_award_on_note - page.within('.note') do - page.within('.js-awards-block') do - first('.js-emoji-btn').click - end - end end end diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb index 4d3e8173eb..007f67d608 100644 --- a/spec/features/merge_requests/award_spec.rb +++ b/spec/features/merge_requests/award_spec.rb @@ -3,8 +3,7 @@ require 'rails_helper' feature 'Merge request awards', js: true, feature: true do let(:user) { create(:user) } let(:project) { create(:project, :public) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: project) } - let!(:note) { create(:note_on_merge_request, project: project, noteable: merge_request, note: 'Looks good!') } + let(:merge_request) { create(:merge_request, source_project: project) } describe 'logged in' do before do @@ -16,12 +15,18 @@ feature 'Merge request awards', js: true, feature: true do first('.js-emoji-btn').click expect(page).to have_selector('.js-emoji-btn.active') expect(first('.js-emoji-btn')).to have_content '1' + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + expect(first('.js-emoji-btn')).to have_content '1' end it 'should remove award from merge request' do first('.js-emoji-btn').click find('.js-emoji-btn.active').click expect(first('.js-emoji-btn')).to have_content '0' + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + expect(first('.js-emoji-btn')).to have_content '0' end it 'should only have one menu on the page' do @@ -40,33 +45,5 @@ feature 'Merge request awards', js: true, feature: true do it 'should not see award menu button' do expect(page).not_to have_selector('.js-award-holder') end - - it 'should not see award menu button in note' do - page.within('.note') do - expect(page).not_to have_selector('.js-award-action-btn') - end - end - end - - def show_note_award_menu - page.within('.note') do - find('.js-add-award').click - end - expect(page).to have_selector('.emoji-menu') - end - - def award_on_note(index = 1) - page.within('.emoji-menu') do - buttons = all('.js-emoji-btn') - buttons[index].click - end - end - - def remove_award_on_note - page.within('.note') do - page.within('.js-awards-block') do - first('.js-emoji-btn').click - end - end end end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index de52460f15..737efcef45 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -4,20 +4,6 @@ describe 'Comments', feature: true do include RepoHelpers include WaitForAjax - describe 'On merge requests page', feature: true do - it 'excludes award_emoji from comment count' do - merge_request = create(:merge_request) - project = merge_request.source_project - create(:award_emoji, awardable: merge_request) - - login_as :admin - visit namespace_project_merge_requests_path(project.namespace, project) - - expect(merge_request.mr_and_commit_notes.count).to eq 0 - expect(page.all('.merge-request-no-comments').first.text).to eq "0" - end - end - describe 'On a merge request', js: true, feature: true do let!(:project) { create(:project) } let!(:merge_request) do @@ -147,17 +133,6 @@ describe 'Comments', feature: true do end end end - - describe 'comment info' do - it 'excludes award_emoji from comment count' do - create(:award_emoji, awardable: merge_request) - - visit namespace_project_merge_request_path(project.namespace, project, merge_request) - - expect(merge_request.mr_and_commit_notes.count).to eq 1 - expect(find('.notes-tab span.badge').text).to eq "1" - end - end end describe 'On a merge request diff', js: true, feature: true do diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index fd3712b7d4..cb3c592f8c 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -14,7 +14,6 @@ describe AwardEmoji, models: true do it { is_expected.to validate_presence_of(:awardable) } it { is_expected.to validate_presence_of(:user) } it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_presence_of(:awardable) } # To circumvent a bug in the shoulda matchers describe "scoped uniqueness validation" do @@ -22,7 +21,7 @@ describe AwardEmoji, models: true do user = create(:user) issue = create(:issue) create(:award_emoji, user: user, awardable: issue) - new_award = AwardEmoji.new(user: user, awardable: issue, name: "thumbsup") + new_award = build(:award_emoji, user: user, awardable: issue) expect(new_award).not_to be_valid end From fab695461afbc4d03fbbf8cfbf9c5d90760ce752 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 1 Jun 2016 18:33:49 +0200 Subject: [PATCH 068/507] Move awardables too when issue is moved --- app/services/issues/move_service.rb | 9 +++++++++ spec/services/issues/move_service_spec.rb | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index e61628086f..138730ca35 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -24,6 +24,7 @@ module Issues @new_issue = create_new_issue rewrite_notes + rewrite_award_emoji add_note_moved_from # Old issue tasks @@ -72,6 +73,14 @@ module Issues end end + def rewrite_award_emoji + @old_issue.award_emoji.each do |award| + new_award = award.dup + new_award.awardable = @new_issue + new_award.save + end + end + def rewrite_content(content) return unless content diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 95fe6c2400..93bf0f6496 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do let!(:milestone2) do create(:milestone, project_id: new_project.id, title: 'v9.0') end + let!(:award_emoji) { create(:award_emoji, awardable: old_issue) } let!(:new_issue) { move_service.execute(old_issue, new_project) } end @@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do it 'preserves create time' do expect(old_issue.created_at).to eq new_issue.created_at end + + it 'moves the award emoji' do + expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name + end end context 'issue with notes' do From 5b43eeee97f8e06a4ada4b173cb972c20d58d8ed Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 1 Jun 2016 21:13:29 +0300 Subject: [PATCH 069/507] Blur button tags when clicked. Fixes #17748. http://stackoverflow.com/questions/23443579/how-to-stop-buttons-from-staying-depressed-with-bootstrap-3 --- app/assets/javascripts/application.js.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 7c547ac843..8f275510ba 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -219,6 +219,10 @@ $ -> form = btn.closest("form") new ConfirmDangerModal(form, text) + + $(document).on 'click', 'button', -> + $(this).blur() + $('input[type="search"]').each -> $this = $(this) $this.attr 'value', $this.val() From ec1191a110166bb95e7aea1020ad7f3fb5ccf3d8 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 31 May 2016 11:57:34 -0600 Subject: [PATCH 070/507] Disable 2FA field autocomplete, resolves #18021. --- app/views/devise/sessions/two_factor.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 8c6a1552a5..fd5937a45c 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -5,7 +5,7 @@ .login-body = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| = f.hidden_field :remember_me, value: params[resource_name][:remember_me] - = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true + = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true, autocomplete: 'off' %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. .prepend-top-20 = f.submit "Verify code", class: "btn btn-save" From e5bb417cdef7d78d128a03295a34b9752ee73d35 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 31 May 2016 23:58:27 -0700 Subject: [PATCH 071/507] Reduce number of fog gem dependencies Closes #15352 --- CHANGELOG | 1 + Gemfile | 8 +++- Gemfile.lock | 114 ++++++++------------------------------------------- 3 files changed, 24 insertions(+), 99 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 848aaa8506..cd499ad187 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.9.0 (unreleased) - Fix issue todo not remove when leave project !4150 (Long Nguyen) - Allow forking projects with restricted visibility level - Improve note validation to prevent errors when creating invalid note via API + - Reduce number of fog gem dependencies - Remove project notification settings associated with deleted projects - Fix 404 page when viewing TODOs that contain milestones or labels in different projects - Redesign navigation for project pages diff --git a/Gemfile b/Gemfile index 540710712f..0e8c42452d 100644 --- a/Gemfile +++ b/Gemfile @@ -84,8 +84,14 @@ gem "carrierwave", '~> 0.10.0' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' +# for backups +gem 'fog-aws', '~> 0.9' +gem 'fog-core', '~> 1.40' +gem 'fog-local', '~> 0.3' +gem 'fog-google', '~> 0.3' +gem 'fog-openstack', '~> 0.1' + # for aws storage -gem "fog", "~> 1.36.0" gem "unf", '~> 0.1.4' # Authorization diff --git a/Gemfile.lock b/Gemfile.lock index 146e95167b..bdb42c6180 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,6 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (4.0.2) actionmailer (4.2.6) @@ -186,7 +185,7 @@ GEM erubis (2.7.0) escape_utils (1.1.1) eventmachine (1.0.8) - excon (0.45.4) + excon (0.49.0) execjs (2.6.0) expression_parser (0.9.0) factory_girl (4.5.0) @@ -203,8 +202,6 @@ GEM multi_json ffaker (2.0.0) ffi (1.9.10) - fission (0.5.0) - CFPropertyList (~> 2.2) flay (2.6.1) ruby_parser (~> 3.0) sexp_processor (~> 4.0) @@ -214,109 +211,28 @@ GEM flowdock (0.7.1) httparty (~> 0.7) multi_json - fog (1.36.0) - fog-aliyun (>= 0.1.0) - fog-atmos - fog-aws (>= 0.6.0) - fog-brightbox (~> 0.4) - fog-core (~> 1.32) - fog-dynect (~> 0.0.2) - fog-ecloud (~> 0.1) - fog-google (<= 0.1.0) - fog-json - fog-local - fog-powerdns (>= 0.1.1) - fog-profitbricks - fog-radosgw (>= 0.0.2) - fog-riakcs - fog-sakuracloud (>= 0.0.4) - fog-serverlove - fog-softlayer - fog-storm_on_demand - fog-terremark - fog-vmfusion - fog-voxel - fog-xenserver - fog-xml (~> 0.1.1) - ipaddress (~> 0.5) - nokogiri (~> 1.5, >= 1.5.11) - fog-aliyun (0.1.0) - fog-core (~> 1.27) - fog-json (~> 1.0) - ipaddress (~> 0.8) - xml-simple (~> 1.1) - fog-atmos (0.1.0) - fog-core - fog-xml - fog-aws (0.8.1) + fog-aws (0.9.2) fog-core (~> 1.27) fog-json (~> 1.0) fog-xml (~> 0.1) ipaddress (~> 0.8) - fog-brightbox (0.10.1) - fog-core (~> 1.22) - fog-json - inflecto (~> 0.0.2) - fog-core (1.35.0) + fog-core (1.40.0) builder - excon (~> 0.45) + excon (~> 0.49) formatador (~> 0.2) - fog-dynect (0.0.2) - fog-core - fog-json - fog-xml - fog-ecloud (0.3.0) - fog-core - fog-xml - fog-google (0.1.0) + fog-google (0.3.2) fog-core fog-json fog-xml fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) - fog-local (0.2.1) + fog-local (0.3.0) fog-core (~> 1.27) - fog-powerdns (0.1.1) - fog-core (~> 1.27) - fog-json (~> 1.0) - fog-xml (~> 0.1) - fog-profitbricks (0.0.5) - fog-core - fog-xml - nokogiri - fog-radosgw (0.0.5) - fog-core (>= 1.21.0) - fog-json - fog-xml (>= 0.0.1) - fog-riakcs (0.1.0) - fog-core - fog-json - fog-xml - fog-sakuracloud (1.7.5) - fog-core - fog-json - fog-serverlove (0.1.2) - fog-core - fog-json - fog-softlayer (1.0.3) - fog-core - fog-json - fog-storm_on_demand (0.1.1) - fog-core - fog-json - fog-terremark (0.1.0) - fog-core - fog-xml - fog-vmfusion (0.1.0) - fission - fog-core - fog-voxel (0.1.0) - fog-core - fog-xml - fog-xenserver (0.2.2) - fog-core - fog-xml + fog-openstack (0.1.6) + fog-core (>= 1.39) + fog-json (>= 1.0) + ipaddress (>= 0.8) fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) @@ -425,11 +341,10 @@ GEM httpclient (2.7.0.1) i18n (0.7.0) ice_nine (0.11.1) - inflecto (0.0.2) influxdb (0.2.3) cause json - ipaddress (0.8.2) + ipaddress (0.8.3) jquery-atwho-rails (1.3.2) jquery-rails (4.1.1) rails-dom-testing (>= 1, < 3) @@ -876,7 +791,6 @@ GEM builder expression_parser rinku - xml-simple (1.1.5) xpath (2.0.0) nokogiri (~> 1.3) @@ -931,7 +845,11 @@ DEPENDENCIES ffaker (~> 2.0.0) flay flog - fog (~> 1.36.0) + fog-aws (~> 0.9) + fog-core (~> 1.40) + fog-google (~> 0.3) + fog-local (~> 0.3) + fog-openstack (~> 0.1) font-awesome-rails (~> 4.2) foreman fuubar (~> 2.0.0) From 6e96035e265dd9072e32865c2973d77b4b72343f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 1 Jun 2016 10:29:44 -0600 Subject: [PATCH 072/507] Change color of canceled ci text to gray --- app/assets/stylesheets/pages/merge_requests.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 4f8a8748d3..c0ae60add6 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -79,11 +79,14 @@ } &.ci-failed, - &.ci-canceled, &.ci-error { color: $gl-danger; } + &.ci-canceled { + color: $gl-gray; + } + a.monospace { color: inherit; } From d863d86aeb1993c2032da0610b3662e61960eb38 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 1 Jun 2016 10:21:22 +0100 Subject: [PATCH 073/507] Add `sha` parameter to MR accept API The `sha` parameter is optional, and when present, must match the current HEAD SHA of the source branch. Otherwise, the API call fails with a 409 Conflict and a message containing the current HEAD for the source branch. Also tidy up some doc wording. --- CHANGELOG | 1 + doc/api/merge_requests.md | 11 +++++++---- lib/api/merge_requests.rb | 5 +++++ spec/requests/api/merge_requests_spec.rb | 13 +++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 848aaa8506..eb037e1ab8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.9.0 (unreleased) - Fix groups API to list only user's accessible projects - Redesign account and email confirmation emails - Use gitlab-shell v3.0.0 + - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged - Add DB index on users.state - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Changed the Slack build message to use the singular duration if necessary (Aran Koning) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 8217e30fe2..16b892dc3b 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -413,11 +413,13 @@ curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.c Merge changes submitted with MR using this API. -If merge success you get `200 OK`. +If the merge succeeds you'll get a `200 OK`. -If it has some conflicts and can not be merged - you get 405 and error message 'Branch cannot be merged' +If it has some conflicts and can not be merged - you'll get a 405 and the error message 'Branch cannot be merged' -If merge request is already merged or closed - you get 405 and error message 'Method Not Allowed' +If merge request is already merged or closed - you'll get a 406 and the error message 'Method Not Allowed' + +If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a 409 and the error message 'SHA does not match HEAD of source branch' If you don't have permissions to accept this merge request - you'll get a 401 @@ -431,7 +433,8 @@ Parameters: - `merge_request_id` (required) - ID of MR - `merge_commit_message` (optional) - Custom merge commit message - `should_remove_source_branch` (optional) - if `true` removes the source branch -- `merged_when_build_succeeds` (optional) - if `true` the MR is merge when the build succeeds +- `merged_when_build_succeeds` (optional) - if `true` the MR is merged when the build succeeds +- `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail ```json { diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4e7de8867b..50baf4c09a 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -218,6 +218,7 @@ module API # merge_commit_message (optional) - Custom merge commit message # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds + # sha (optional) - When present, must have the HEAD SHA of the source branch # Example: # PUT /projects/:id/merge_requests/:merge_request_id/merge # @@ -233,6 +234,10 @@ module API render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? + if params[:sha] && merge_request.source_sha != params[:sha] + render_api_error!("SHA does not match HEAD of source branch: #{merge_request.target_sha}", 409) + end + merge_params = { commit_message: params[:merge_commit_message], should_remove_source_branch: params[:should_remove_source_branch] diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 4b0111df14..5aa98ec401 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -428,6 +428,19 @@ describe API::API, api: true do expect(json_response['message']).to eq('401 Unauthorized') end + it "returns 409 if the SHA parameter doesn't match" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha.succ + + expect(response.status).to eq(409) + expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') + end + + it "succeeds if the SHA parameter matches" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha + + expect(response.status).to eq(200) + end + it "enables merge when build succeeds if the ci is active" do allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) allow(ci_commit).to receive(:active?).and_return(true) From f680eca912f854373fc538ae1e9d0dcb60fcd310 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 1 Jun 2016 13:25:44 +0100 Subject: [PATCH 074/507] Don't allow merges with new commits Set a `sha` parameter on the MR form. If this doesn't match the HEAD of the source branch when the form is submitted, show a warning (like with a merge conflict) and don't merge the branch. --- CHANGELOG | 1 + .../projects/merge_requests_controller.rb | 5 ++ .../projects/merge_requests/merge.js.haml | 3 + .../widget/open/_accept.html.haml | 1 + .../open/_merge_when_build_succeeds.html.haml | 2 +- .../widget/open/_sha_mismatch.html.haml | 6 ++ .../merge_requests_controller_spec.rb | 86 +++++++++++++++++++ 7 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml diff --git a/CHANGELOG b/CHANGELOG index eb037e1ab8..b998713e2f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.9.0 (unreleased) - Redesign account and email confirmation emails - Use gitlab-shell v3.0.0 - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged + - Don't allow MRs to be merged when commits were added since the last review / page load - Add DB index on users.state - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Changed the Slack build message to use the singular duration if necessary (Aran Koning) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d54284d7b2..3142fe5c76 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -190,6 +190,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController return end + if params[:sha] != @merge_request.source_sha + @status = :sha_mismatch + return + end + TodoService.new.merge_merge_request(merge_request, current_user) @merge_request.update(merge_error: nil) diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index 92ce479d46..84b6c9ebc5 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -5,6 +5,9 @@ - when :merge_when_build_succeeds :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}"); +- when :sha_mismatch + :plain + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}"); - else :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index cfdf4edac3..0d49b6471a 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -2,6 +2,7 @@ = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token + = hidden_field_tag :sha, @merge_request.source_sha .accept-merge-holder.clearfix.js-toggle-container .clearfix .accept-action diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index b83ddcab3a..ad898ff153 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -16,7 +16,7 @@ - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.source_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged diff --git a/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml new file mode 100644 index 0000000000..a78583b7c6 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml @@ -0,0 +1,6 @@ +%h4 + = icon("exclamation-triangle") + This merge request has new commits since the page was loaded. + +%p + Please review the new commits before merging. diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 4f621a43d7..8499bf07e9 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -185,6 +185,92 @@ describe Projects::MergeRequestsController do end end + describe 'POST #merge' do + let(:base_params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + format: 'raw' + } + end + + context 'when the user does not have access' do + before do + project.team.truncate + project.team << [user, :reporter] + post :merge, base_params + end + + it 'returns not found' do + expect(response).to be_not_found + end + end + + context 'when the merge request is not mergeable' do + before do + merge_request.update_attributes(title: "WIP: #{merge_request.title}") + + post :merge, base_params + end + + it 'returns :failed' do + expect(assigns(:status)).to eq(:failed) + end + end + + context 'when the sha parameter does not match the source SHA' do + before { post :merge, base_params.merge(sha: 'foo') } + + it 'returns :sha_mismatch' do + expect(assigns(:status)).to eq(:sha_mismatch) + end + end + + context 'when the sha parameter matches the source SHA' do + def merge_with_sha + post :merge, base_params.merge(sha: merge_request.source_sha) + end + + it 'returns :success' do + merge_with_sha + + expect(assigns(:status)).to eq(:success) + end + + it 'starts the merge immediately' do + expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything) + + merge_with_sha + end + + context 'when merge_when_build_succeeds is passed' do + def merge_when_build_succeeds + post :merge, base_params.merge(sha: merge_request.source_sha, merge_when_build_succeeds: '1') + end + + before do + create(:ci_empty_commit, project: project, sha: merge_request.source_sha, ref: merge_request.source_branch) + end + + it 'returns :merge_when_build_succeeds' do + merge_when_build_succeeds + + expect(assigns(:status)).to eq(:merge_when_build_succeeds) + end + + it 'sets the MR to merge when the build succeeds' do + service = double(:merge_when_build_succeeds_service) + + expect(MergeRequests::MergeWhenBuildSucceedsService).to receive(:new).with(project, anything, anything).and_return(service) + expect(service).to receive(:execute).with(merge_request) + + merge_when_build_succeeds + end + end + end + end + describe "DELETE #destroy" do it "denies access to users unless they're admin or project owner" do delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid From 30856b1eeaf2990297e4eb2323e5bdf9ee975a5c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 1 Jun 2016 11:59:45 -0600 Subject: [PATCH 075/507] Add flexbox to project header --- app/assets/stylesheets/pages/projects.scss | 10 +++++++++- app/views/projects/_home_panel.html.haml | 8 ++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index edef336481..b9cb0f81e3 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -32,6 +32,15 @@ .container-fluid { position: relative; + + @media (min-width: $screen-md-max) { + .row { + display: flex; + -ms-flex-align: center; + -webkit-align-items: center; + -webkit-box-align: center; + } + } } .cover-controls { @@ -57,7 +66,6 @@ max-width: 86px; min-width: 86px; padding-right: 0; - margin: 11px 0; @media (max-width: $screen-md-max) { padding-left: 0; diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 57c3d1b0a6..f0e04a0235 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -29,10 +29,10 @@ .project-clone-holder = render "shared/clone_panel" - .project-repo-buttons.btn-group.project-right-buttons - = render "projects/buttons/download" - = render 'projects/buttons/dropdown' - = render 'projects/buttons/notifications' + .project-repo-buttons.btn-group.project-right-buttons + = render "projects/buttons/download" + = render 'projects/buttons/dropdown' + = render 'projects/buttons/notifications' :javascript new Star(); From ad3d0585aa2d36e57b53781a5bd6e3dbe96cb71d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 31 May 2016 19:35:33 -0400 Subject: [PATCH 076/507] Fix serious performance bug with rendering Markdown with InlineDiffFilter Nokogiri's `node.replace` was being unnecessarily called for every text node in the document due to a comparison bug. The code previously was comparing the HTML representation of the full document against the text node, which would always fail. Fix the comparison to just compare the modified text. Closes #18011 --- CHANGELOG | 1 + lib/banzai/filter/inline_diff_filter.rb | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d1cde40c1c..9a5d134112 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -30,6 +30,7 @@ v 8.9.0 (unreleased) v 8.8.3 - Fix incorrect links on pipeline page when merge request created from fork - Fix gitlab importer failing to import new projects due to missing credentials + - Fix serious performance bug with rendering Markdown with InlineDiffFilter - Fix import URL migration not rescuing with the correct Error - In search results, only show notes on confidential issues that the user has access to - Fix health check access token changing due to old application settings being used diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb index 9e75edd4d4..beb21b19ab 100644 --- a/lib/banzai/filter/inline_diff_filter.rb +++ b/lib/banzai/filter/inline_diff_filter.rb @@ -8,15 +8,19 @@ module Banzai next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) content = node.to_html - content = content.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '\1\2') - content = content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '\1\2') + html_content = inline_diff_filter(content) - next if html == content + next if content == html_content - node.replace(content) + node.replace(html_content) end doc end + + def inline_diff_filter(text) + html_content = text.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '\1\2') + html_content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '\1\2') + end end end end From c102b03e768fab984958a6d7e48ffbe4ef946576 Mon Sep 17 00:00:00 2001 From: chujinjin <10746161@qq.com> Date: Wed, 1 Jun 2016 14:11:20 +0000 Subject: [PATCH 077/507] Fix wiki project clone address error --- CHANGELOG | 1 + app/helpers/button_helper.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d1cde40c1c..a0df839666 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,6 +33,7 @@ v 8.8.3 - Fix import URL migration not rescuing with the correct Error - In search results, only show notes on confidential issues that the user has access to - Fix health check access token changing due to old application settings being used + - Fix wiki project clone address error (chujinjin) v 8.8.2 - Added remove due date button. !4209 diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index a9047ede8c..f742922d92 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -30,7 +30,7 @@ module ButtonHelper content_tag :a, protocol, class: klass, - href: @project.http_url_to_repo, + href: project.http_url_to_repo, data: { html: true, placement: 'right', From b856bd640695b615e765ea09057fc1bc2e4a9870 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 2 Jun 2016 10:53:39 +0300 Subject: [PATCH 078/507] Shorter name for Container Registry tab Signed-off-by: Dmitriy Zaporozhets --- app/views/layouts/nav/_project.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2c9b900666..c5d064ed48 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -58,7 +58,7 @@ = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do = icon('hdd-o fw') %span - Container Registry + Registry - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do From 855ff6423b78bafa06ce62a2bb724e58f1a0042b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 1 Jun 2016 12:33:32 +0100 Subject: [PATCH 079/507] Shows the edit comment button on mobile Closes #17214 --- app/assets/stylesheets/framework/mobile.scss | 4 --- app/assets/stylesheets/pages/notes.scss | 26 ++++++++++++++++++-- app/views/projects/notes/_note.html.haml | 5 ++-- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index bd531f8376..d4e5cc819a 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -66,10 +66,6 @@ display: none; } - %ul.notes .note-role, .note-actions { - display: none; - } - .nav-links, .nav-links { li a { font-size: 14px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index a3e1ac13a4..0e82c45723 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -118,6 +118,11 @@ ul.notes { .note-header { padding-bottom: 3px; + padding-right: 20px; + + @media (min-width: $screen-sm-min) { + padding-right: 0; + } } } @@ -179,6 +184,8 @@ ul.notes { .discussion-header, .note-header { + position: relative; + a { color: inherit; @@ -215,6 +222,16 @@ ul.notes { color: $notes-action-color; } +.note-actions { + position: absolute; + right: 0; + top: 0; + + @media (min-width: $screen-sm-min) { + position: relative; + } +} + .discussion-actions { @media (max-width: $screen-md-max) { float: none; @@ -228,8 +245,13 @@ ul.notes { .note-action-button { display: inline-block; - margin-left: 10px; - line-height: 24px; + margin-left: 0; + line-height: 20px; + + @media (min-width: $screen-sm-min) { + margin-left: 10px; + line-height: 24px; + } .fa { color: $notes-action-color; diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index f1045bbd8c..f6b7463ff0 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -19,12 +19,11 @@ .note-actions - access = note.project.team.human_max_access(note.author.id) - if access - %span.note-role - = access + %span.note-role.hidden-xs= access - if note_editable = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil') - = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do = icon('trash-o') .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text From 6aa9ea7d02aeec500107ea06c774bae5a8efbd50 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 1 Jun 2016 12:11:16 +0100 Subject: [PATCH 080/507] Fixed issue with activity links not being consistent Closes #17621 --- app/views/events/event/_common.html.haml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index c7f29f2fc0..2e2403347c 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,10 +1,14 @@ .event-title %span.author_name= link_to_author event %span.event_label{class: event.action_name} - = event_action_name(event) - - if event.target - %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title + = event.action_name + %strong + = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title do + = event.target_type.titleize.downcase + = event.target.reference_link_text + - else + = event_action_name(event) = event_preposition(event) From 1abb0ed97d48b603f7488fe2543aeef110067908 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 2 Jun 2016 11:05:54 +0200 Subject: [PATCH 081/507] Move feature specs for shortcuts to valid directory --- spec/features/{project => projects}/shortcuts_spec.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/features/{project => projects}/shortcuts_spec.rb (100%) diff --git a/spec/features/project/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb similarity index 100% rename from spec/features/project/shortcuts_spec.rb rename to spec/features/projects/shortcuts_spec.rb From 1521dc51e9f0c37434c8290d67d775bc9fde188d Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 31 May 2016 13:02:12 +0100 Subject: [PATCH 082/507] Fix link to blank group icon When the group is the default blank icon, this needs to use the `image_path` helper; otherwise, the link won't work if assets are precompiled. This still works fine for uploaded icons in either case. --- CHANGELOG | 1 + app/helpers/groups_helper.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d1cde40c1c..74ea27a2e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ v 8.9.0 (unreleased) - Fix issues filter when ordering by milestone - Todos will display target state if issuable target is 'Closed' or 'Merged' - Fix bug when sorting issues by milestone due date and filtering by two or more labels + - Link to blank group icon doesn't throw a 404 anymore - Remove 'main language' feature - Pipelines can be canceled only when there are running builds - Use downcased path to container repository as this is expected path by Docker diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index b1f0a765bb..4cac69c679 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -31,7 +31,7 @@ module GroupsHelper if group && group.avatar.present? group.avatar.url else - 'no_group_avatar.png' + image_path('no_group_avatar.png') end end From 31112fd0cb0ce930cc8fbc068536310424d6dd06 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 2 Jun 2016 11:24:53 +0100 Subject: [PATCH 083/507] Edit form background color on highlighted note Added a white background to the edit form on highlighted notes Closes #18101 --- app/assets/stylesheets/pages/note_form.scss | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 7fa13e66b4..d714257f17 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -96,17 +96,8 @@ display: none; font-size: 15px; - .form-actions { - padding-left: 20px; - - .btn-save { - float: left; - } - - .note-form-option { - float: left; - padding: 2px 0 0 25px; - } + .md-area { + background-color: #fff; } } From 2fbfb85492401b2a8ac81f22b319b304afecf6c3 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 30 May 2016 15:55:11 +0200 Subject: [PATCH 084/507] Returning enums in ReferenceFilter#each_node This changes ReferenceFilter#each_node so that when it's called without a block an Enumerator is returned. --- lib/banzai/filter/reference_filter.rb | 2 ++ .../banzai/filter/reference_filter_spec.rb | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 spec/lib/banzai/filter/reference_filter_spec.rb diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 41ae0e1f9c..5eace574ab 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -68,6 +68,8 @@ module Banzai # by `ignore_ancestor_query`. Link tags are not processed if they have a # "gfm" class or the "href" attribute is empty. def each_node + return to_enum(__method__) unless block_given? + query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})] | descendant-or-self::a[ not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "") diff --git a/spec/lib/banzai/filter/reference_filter_spec.rb b/spec/lib/banzai/filter/reference_filter_spec.rb new file mode 100644 index 0000000000..d3e7af0671 --- /dev/null +++ b/spec/lib/banzai/filter/reference_filter_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Banzai::Filter::ReferenceFilter, lib: true do + let(:project) { build(:project) } + + describe '#each_node' do + it 'iterates over the nodes in a document' do + document = Nokogiri::HTML.fragment('foo') + filter = described_class.new(document, project: project) + + expect { |b| filter.each_node(&b) }. + to yield_with_args(an_instance_of(Nokogiri::XML::Element)) + end + + it 'returns an Enumerator when no block is given' do + document = Nokogiri::HTML.fragment('foo') + filter = described_class.new(document, project: project) + + expect(filter.each_node).to be_an_instance_of(Enumerator) + end + + it 'skips links with a "gfm" class' do + document = Nokogiri::HTML.fragment('foo') + filter = described_class.new(document, project: project) + + expect { |b| filter.each_node(&b) }.not_to yield_control + end + + it 'skips text nodes in pre elements' do + document = Nokogiri::HTML.fragment('
                                      foo
                                      ') + filter = described_class.new(document, project: project) + + expect { |b| filter.each_node(&b) }.not_to yield_control + end + end +end From 8a6c3f27e9dfea2c151657045e17fe66ad81b5e5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 30 May 2016 15:56:05 +0200 Subject: [PATCH 085/507] Added ReferenceFilter#nodes This method returns an Array of the HTML nodes as yielded by ReferenceFilter#each_node. The method's return value is memoized to allow multiple calls without having to re-query the input document. --- lib/banzai/filter/reference_filter.rb | 5 +++++ spec/lib/banzai/filter/reference_filter_spec.rb | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 5eace574ab..2d6f34c9cd 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -80,6 +80,11 @@ module Banzai end end + # Returns an Array containing all HTML nodes. + def nodes + @nodes ||= each_node.to_a + end + # Yields the link's URL and text whenever the node is a valid tag. def yield_valid_link(node) link = CGI.unescape(node.attr('href').to_s) diff --git a/spec/lib/banzai/filter/reference_filter_spec.rb b/spec/lib/banzai/filter/reference_filter_spec.rb index d3e7af0671..55e681f6fa 100644 --- a/spec/lib/banzai/filter/reference_filter_spec.rb +++ b/spec/lib/banzai/filter/reference_filter_spec.rb @@ -33,4 +33,13 @@ describe Banzai::Filter::ReferenceFilter, lib: true do expect { |b| filter.each_node(&b) }.not_to yield_control end end + + describe '#nodes' do + it 'returns an Array of the HTML nodes' do + document = Nokogiri::HTML.fragment('foo') + filter = described_class.new(document, project: project) + + expect(filter.nodes).to eq([document.children[0]]) + end + end end From fea591e5c5796235d28eeec4d27759f87fa9d8e2 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 2 Jun 2016 13:42:18 +0200 Subject: [PATCH 086/507] Rename finder to find_in_gitlab_or_ldap --- config/initializers/doorkeeper.rb | 2 +- lib/api/session.rb | 2 +- lib/gitlab/auth.rb | 4 ++-- lib/gitlab/backend/grack_auth.rb | 2 +- spec/lib/gitlab/auth_spec.rb | 16 ++++++++-------- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index aae2ee3193..8dc8e270af 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -12,7 +12,7 @@ Doorkeeper.configure do end resource_owner_from_credentials do |routes| - Gitlab::Auth.find_by_master_or_ldap(params[:username], params[:password]) + Gitlab::Auth.find_in_gitlab_or_ldap(params[:username], params[:password]) end # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. diff --git a/lib/api/session.rb b/lib/api/session.rb index 1156aab8cc..56e69b2366 100644 --- a/lib/api/session.rb +++ b/lib/api/session.rb @@ -11,7 +11,7 @@ module API # Example Request: # POST /session post "/session" do - user = Gitlab::Auth.find_by_master_or_ldap(params[:email] || params[:login], params[:password]) + user = Gitlab::Auth.find_in_gitlab_or_ldap(params[:email] || params[:login], params[:password]) return unauthorized! unless user present user, with: Entities::UserLogin diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 0479006f99..d156fa2978 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -9,7 +9,7 @@ module Gitlab if valid_ci_request?(login, password, project) type = :ci - elsif user = find_by_master_or_ldap(login, password) + elsif user = find_in_gitlab_or_ldap(login, password) type = :master_or_ldap elsif user = oauth_access_token_check(login, password) type = :oauth @@ -19,7 +19,7 @@ module Gitlab [user, type] end - def find_by_master_or_ldap(login, password) + def find_in_gitlab_or_ldap(login, password) user = User.by_login(login) # If no user is found, or it's an LDAP server, try LDAP. diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 3462c2dcfb..492ffb138a 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -95,7 +95,7 @@ module Grack end def authenticate_user(login, password) - user = Gitlab::Auth.new.find_by_master_or_ldap(login, password) + user = Gitlab::Auth.new.find_in_gitlab_or_ldap(login, password) unless user user = oauth_access_token_check(login, password) diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 16083f90bb..3c41c4b068 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -41,7 +41,7 @@ describe Gitlab::Auth, lib: true do end end - describe 'find_by_master_or_ldap' do + describe 'find_in_gitlab_or_ldap' do let!(:user) do create(:user, username: username, @@ -52,25 +52,25 @@ describe Gitlab::Auth, lib: true do let(:password) { 'my-secret' } it "should find user by valid login/password" do - expect( gl_auth.find_by_master_or_ldap(username, password) ).to eql user + expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).to eql user end it 'should find user by valid email/password with case-insensitive email' do - expect(gl_auth.find_by_master_or_ldap(user.email.upcase, password)).to eql user + expect(gl_auth.find_in_gitlab_or_ldap(user.email.upcase, password)).to eql user end it 'should find user by valid username/password with case-insensitive username' do - expect(gl_auth.find_by_master_or_ldap(username.upcase, password)).to eql user + expect(gl_auth.find_in_gitlab_or_ldap(username.upcase, password)).to eql user end it "should not find user with invalid password" do password = 'wrong' - expect( gl_auth.find_by_master_or_ldap(username, password) ).not_to eql user + expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).not_to eql user end it "should not find user with invalid login" do user = 'wrong' - expect( gl_auth.find_by_master_or_ldap(username, password) ).not_to eql user + expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).not_to eql user end context "with ldap enabled" do @@ -81,13 +81,13 @@ describe Gitlab::Auth, lib: true do it "tries to autheticate with db before ldap" do expect(Gitlab::LDAP::Authentication).not_to receive(:login) - gl_auth.find_by_master_or_ldap(username, password) + gl_auth.find_in_gitlab_or_ldap(username, password) end it "uses ldap as fallback to for authentication" do expect(Gitlab::LDAP::Authentication).to receive(:login) - gl_auth.find_by_master_or_ldap('ldap_user', 'password') + gl_auth.find_in_gitlab_or_ldap('ldap_user', 'password') end end end From 01575e9966805fa4c12a7a56361f511b3b61e309 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 30 May 2016 15:56:50 +0200 Subject: [PATCH 087/507] Reduce Namespace queries in UserReferenceFilter This changes UserReferenceFilter so it operates using the following steps: 1. Grab all username references from the input document. 2. Query the corresponding Namespace objects using a single query. 3. Iterate over all nodes to build links while re-using the objects queried in step 2. The impact of these changes is that a comment mentioning 5 different usernames no longer runs 5 different queries (1 for every username), instead it only runs a single query. --- CHANGELOG | 1 + lib/banzai/filter/user_reference_filter.rb | 29 +++++++++++++++++-- .../filter/user_reference_filter_spec.rb | 19 ++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d1cde40c1c..7e22406022 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.9.0 (unreleased) - Measure queue duration between gitlab-workhorse and Rails - Make authentication service for Container Registry to be compatible with < Docker 1.11 - Add Application Setting to configure Container Registry token expire delay (default 5min) + - Reduce number of SQL queries when rendering user references v 8.8.3 - Fix incorrect links on pipeline page when merge request created from fork diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 331d800725..5b0a6d8541 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -29,7 +29,7 @@ module Banzai ref_pattern = User.reference_pattern ref_pattern_start = /\A#{ref_pattern}\z/ - each_node do |node| + nodes.each do |node| if text_node?(node) replace_text_when_pattern_matches(node, ref_pattern) do |content| user_link_filter(content) @@ -59,7 +59,7 @@ module Banzai self.class.references_in(text) do |match, username| if username == 'all' link_to_all(link_text: link_text) - elsif namespace = Namespace.find_by(path: username) + elsif namespace = namespaces[username] link_to_namespace(namespace, link_text: link_text) || match else match @@ -67,6 +67,31 @@ module Banzai end end + # Returns a Hash containing all Namespace objects for the username + # references in the current document. + # + # The keys of this Hash are the namespace paths, the values the + # corresponding Namespace objects. + def namespaces + @namespaces ||= + Namespace.where(path: usernames).each_with_object({}) do |row, hash| + hash[row.path] = row + end + end + + # Returns all usernames referenced in the current document. + def usernames + refs = Set.new + + nodes.each do |node| + node.to_html.scan(User.reference_pattern) do + refs << $~[:user] + end + end + + refs.to_a + end + private def urls diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index d7dfd6699e..108b36a97c 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -136,4 +136,23 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s end end + + describe '#namespaces' do + it 'returns a Hash containing all Namespaces' do + document = Nokogiri::HTML.fragment("

                                      #{user.to_reference}

                                      ") + filter = described_class.new(document, project: project) + ns = user.namespace + + expect(filter.namespaces).to eq({ ns.path => ns }) + end + end + + describe '#usernames' do + it 'returns the usernames mentioned in a document' do + document = Nokogiri::HTML.fragment("

                                      #{user.to_reference}

                                      ") + filter = described_class.new(document, project: project) + + expect(filter.usernames).to eq([user.username]) + end + end end From bffe0d6325710e89e405390f4da297fb12fdc314 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Tue, 31 May 2016 08:58:11 -0400 Subject: [PATCH 088/507] Cache assigned merge request count. Closes #18036 --- app/models/user.rb | 6 ++++++ app/views/layouts/nav/_dashboard.html.haml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 15b6cbc225..8dde01bf35 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -776,6 +776,12 @@ class User < ActiveRecord::Base notification_settings.find_or_initialize_by(source: source) end + def assigned_open_merge_request_count + Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], expires_in: 60) do + assigned_merge_requests.opened.count + end + end + private def projects_union diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 43532b0c15..e14ae850fc 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -36,7 +36,7 @@ = icon('tasks fw') %span Merge Requests - %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) + %span.count= number_with_delimiter(current_user.assigned_open_merge_request_count) = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') From bfccea370310d6f7e5fe16c846ccd073d487a97f Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Tue, 31 May 2016 09:02:47 -0400 Subject: [PATCH 089/507] Cache assigned open issue count. Closes #18035 --- app/models/user.rb | 10 ++++++++-- app/views/layouts/nav/_dashboard.html.haml | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 8dde01bf35..55f8e14962 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -778,8 +778,14 @@ class User < ActiveRecord::Base def assigned_open_merge_request_count Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], expires_in: 60) do - assigned_merge_requests.opened.count - end + assigned_merge_requests.opened.count + end + end + + def assigned_open_issues_count + Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], expires_in: 60) do + assigned_issues.opened.count + end end private diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index e14ae850fc..306ebd5fcf 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -30,7 +30,7 @@ = icon('exclamation-circle fw') %span Issues - %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) + %span.count= number_with_delimiter(current_user.assigned_open_issues_count) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = icon('tasks fw') From 8835b7889a6265aba3c6d7ee241abf80a1cf07f3 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Wed, 1 Jun 2016 19:27:21 -0400 Subject: [PATCH 090/507] Flush cache in callback. Add tests --- app/models/concerns/issuable.rb | 8 ++++++++ app/models/user.rb | 13 +++++++++---- spec/features/issues_spec.rb | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 2326a395cb..46cde46095 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -68,6 +68,14 @@ module Issuable strip_attributes :title acts_as_paranoid + + after_save :update_assignee_cache_counts, if: :assignee_id_changed? + + def update_assignee_cache_counts + # make sure we flush the cache for both the old *and* new assignee + User.find(assignee_id_was).update_cache_counts if assignee_id_was + assignee.update_cache_counts if assignee_id + end end module ClassMethods diff --git a/app/models/user.rb b/app/models/user.rb index 55f8e14962..172845c9d2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -776,18 +776,23 @@ class User < ActiveRecord::Base notification_settings.find_or_initialize_by(source: source) end - def assigned_open_merge_request_count - Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], expires_in: 60) do + def assigned_open_merge_request_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do assigned_merge_requests.opened.count end end - def assigned_open_issues_count - Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], expires_in: 60) do + def assigned_open_issues_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do assigned_issues.opened.count end end + def update_cache_counts + assigned_open_merge_request_count(force: true) + assigned_open_issues_count(force: true) + end + private def projects_union diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 9271964166..fa0d8e1a0c 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -62,6 +62,21 @@ describe 'Issues', feature: true do expect(issue.reload.assignee).to be_nil end + + it 'updates assigned cache count on change', js: true do + visit edit_namespace_project_issue_path(project.namespace, project, issue) + + expect(page).to have_content "Assignee #{@user.name}" + expect(@user.assigned_open_issues_count).to eq @user.assigned_issues.opened.count + + first('#s2id_issue_assignee_id').click + sleep 2 # wait for ajax stuff to complete + first('.user-result').click + + click_button 'Save changes' + + expect(@user.assigned_open_issues_count).to eq @user.assigned_issues.opened.count + end end describe 'due date', js: true do From 0f3df62e1a42982ffb635dc5a9b201ed2520b0f4 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Thu, 2 Jun 2016 09:25:40 -0400 Subject: [PATCH 091/507] Update specs. Add CHANGELOG entry --- CHANGELOG | 1 + app/models/concerns/issuable.rb | 2 +- spec/features/issues_spec.rb | 15 --------------- spec/models/issue_spec.rb | 17 +++++++++++++++++ spec/models/merge_request_spec.rb | 17 +++++++++++++++++ 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6f91dfcfb6..de77625dce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 8.9.0 (unreleased) - Measure queue duration between gitlab-workhorse and Rails - Make authentication service for Container Registry to be compatible with < Docker 1.11 - Add Application Setting to configure Container Registry token expire delay (default 5min) + - Cache assigned issue and merge request counts in sidebar nav v 8.8.3 - Fix incorrect links on pipeline page when merge request created from fork diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 46cde46095..50f5b749e3 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -74,7 +74,7 @@ module Issuable def update_assignee_cache_counts # make sure we flush the cache for both the old *and* new assignee User.find(assignee_id_was).update_cache_counts if assignee_id_was - assignee.update_cache_counts if assignee_id + assignee.update_cache_counts if assignee end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index fa0d8e1a0c..9271964166 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -62,21 +62,6 @@ describe 'Issues', feature: true do expect(issue.reload.assignee).to be_nil end - - it 'updates assigned cache count on change', js: true do - visit edit_namespace_project_issue_path(project.namespace, project, issue) - - expect(page).to have_content "Assignee #{@user.name}" - expect(@user.assigned_open_issues_count).to eq @user.assigned_issues.opened.count - - first('#s2id_issue_assignee_id').click - sleep 2 # wait for ajax stuff to complete - first('.user-result').click - - click_button 'Save changes' - - expect(@user.assigned_open_issues_count).to eq @user.assigned_issues.opened.count - end end describe 'due date', js: true do diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 87b3d8d650..b87d68283e 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -269,4 +269,21 @@ describe Issue, models: true do end end end + + describe 'cached counts' do + it 'updates when assignees change' do + user1 = create(:user) + user2 = create(:user) + issue = create(:issue, assignee: user1) + + expect(user1.assigned_open_issues_count).to eq(1) + expect(user2.assigned_open_issues_count).to eq(0) + + issue.assignee = user2 + issue.save + + expect(user1.assigned_open_issues_count).to eq(0) + expect(user2.assigned_open_issues_count).to eq(1) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 118e1e22a7..a4c55cc2fd 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -438,4 +438,21 @@ describe MergeRequest, models: true do expect(mr.participants).to include(note1.author, note2.author) end end + + describe 'cached counts' do + it 'updates when assignees change' do + user1 = create(:user) + user2 = create(:user) + mr = create(:merge_request, assignee: user1) + + expect(user1.assigned_open_merge_request_count).to eq(1) + expect(user2.assigned_open_merge_request_count).to eq(0) + + mr.assignee = user2 + mr.save + + expect(user1.assigned_open_merge_request_count).to eq(0) + expect(user2.assigned_open_merge_request_count).to eq(1) + end + end end From c966d55e6a293b153b329612b0f78d282a83abe6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 2 Jun 2016 09:36:09 -0500 Subject: [PATCH 092/507] Fixes missing number on generated ordered list --- app/assets/stylesheets/pages/detail_page.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 5e61e61d85..1b389d8352 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -29,8 +29,6 @@ margin-top: 6px; p { - overflow-x: auto; - &:last-child { margin-bottom: 0; } From 021d3810c300d1e0514f21ccb6f1439f59e20565 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 2 Jun 2016 16:19:18 +0200 Subject: [PATCH 093/507] Rename Ci::Commit to Ci::Pipeline and rename some of the ci_commit to pipeline --- app/controllers/projects/builds_controller.rb | 4 ++-- app/controllers/projects/commit_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 22 +++++++++---------- .../projects/pipelines_controller.rb | 6 ++--- app/models/ci/build.rb | 12 +++++----- app/models/ci/{commit.rb => pipeline.rb} | 4 ++-- app/models/ci/trigger_request.rb | 2 +- app/models/commit.rb | 6 ++--- app/models/commit_status.rb | 8 +++---- app/models/merge_request.rb | 4 ++-- app/models/project.rb | 10 ++++----- app/services/ci/create_pipeline_service.rb | 4 ++-- .../ci/create_trigger_request_service.rb | 2 +- app/services/ci/image_for_build_service.rb | 2 +- app/services/create_commit_builds_service.rb | 2 +- app/services/merge_requests/base_service.rb | 8 +++---- features/steps/project/commits/commits.rb | 2 +- lib/api/commit_statuses.rb | 2 +- lib/api/merge_requests.rb | 2 +- lib/ci/charts.rb | 2 +- spec/factories/ci/commits.rb | 2 +- spec/features/pipelines_spec.rb | 2 +- spec/helpers/ci_status_helper_spec.rb | 4 ++-- spec/helpers/merge_requests_helper_spec.rb | 2 +- spec/models/ci/commit_spec.rb | 2 +- spec/models/merge_request_spec.rb | 4 ++-- spec/models/project_spec.rb | 2 +- spec/requests/api/commit_statuses_spec.rb | 4 ++-- spec/requests/api/merge_requests_spec.rb | 4 ++-- spec/requests/api/triggers_spec.rb | 2 +- spec/requests/ci/api/triggers_spec.rb | 2 +- .../create_commit_builds_service_spec.rb | 14 ++++++------ .../merge_when_build_succeeds_service_spec.rb | 2 +- spec/support/stub_gitlab_calls.rb | 2 +- spec/workers/post_receive_spec.rb | 8 +++---- 35 files changed, 81 insertions(+), 81 deletions(-) rename app/models/ci/{commit.rb => pipeline.rb} (98%) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index bb1f6c5e98..de8abf8639 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -26,9 +26,9 @@ class Projects::BuildsController < Projects::ApplicationController end def show - @builds = @project.ci_commits.find_by_sha(@build.sha).builds.order('id DESC') + @builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC') @builds = @builds.where("id not in (?)", @build.id) - @commit = @build.commit + @pipeline = @build.pipeline respond_to do |format| format.html diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 10b5932aff..287388652e 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -100,7 +100,7 @@ class Projects::CommitController < Projects::ApplicationController end def ci_commits - @ci_commits ||= project.ci_commits.where(sha: commit.sha) + @ci_commits ||= project.pipelines.where(sha: commit.sha) end def ci_builds diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d54284d7b2..21a70fd69a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -119,8 +119,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @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 + @pipeline = @merge_request.pipeline + @statuses = @pipeline.statuses if @pipeline @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count @@ -194,7 +194,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.update(merge_error: nil) - if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active? + if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active? MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) .execute(@merge_request) @status = :merge_when_build_succeeds @@ -225,10 +225,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def ci_status - ci_commit = @merge_request.ci_commit - if ci_commit - status = ci_commit.status - coverage = ci_commit.try(:coverage) + pipeline = @merge_request.pipeline + if pipeline + status = pipeline.status + coverage = pipeline.try(:coverage) status ||= "preparing" else @@ -310,8 +310,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff - @ci_commit = @merge_request.ci_commit - @statuses = @ci_commit.statuses if @ci_commit + @pipeline = @merge_request.pipeline + @statuses = @ci_commit.statuses if @pipeline if @merge_request.locked_long_ago? @merge_request.unlock_mr @@ -320,8 +320,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_widget_vars - @ci_commit = @merge_request.ci_commit - @ci_commits = [@ci_commit].compact + @pipeline = @merge_request.pipeline + @pipelines = [@pipeline].compact closes_issues end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index b36081205d..cac440ae53 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -7,7 +7,7 @@ class Projects::PipelinesController < Projects::ApplicationController def index @scope = params[:scope] - all_pipelines = project.ci_commits + all_pipelines = project.pipelines @pipelines_count = all_pipelines.count @running_or_pending_count = all_pipelines.running_or_pending.count @pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope) @@ -15,7 +15,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def new - @pipeline = project.ci_commits.new(ref: @project.default_branch) + @pipeline = project.pipelines.new(ref: @project.default_branch) end def create @@ -50,7 +50,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def pipeline - @pipeline ||= project.ci_commits.find_by!(id: params[:id]) + @pipeline ||= project.pipelines.find_by!(id: params[:id]) end def commit diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5e77fda70b..74441eb97d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -45,8 +45,8 @@ module Ci new_build.options = build.options new_build.commands = build.commands new_build.tag_list = build.tag_list - new_build.gl_project_id = build.gl_project_id - new_build.commit_id = build.commit_id + new_build.project = build.project + new_build.pipeline = build.pipeline new_build.name = build.name new_build.allow_failure = build.allow_failure new_build.stage = build.stage @@ -66,7 +66,7 @@ module Ci # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed around_transition any => [:success, :failed, :canceled] do |build, block| block.call - build.commit.create_next_builds(build) if build.commit + build.pipeline.create_next_builds(build) if build.pipeline end after_transition any => [:success, :failed, :canceled] do |build| @@ -80,7 +80,7 @@ module Ci end def retried? - !self.commit.statuses.latest.include?(self) + !self.pipeline.statuses.latest.include?(self) end def retry @@ -89,7 +89,7 @@ module Ci def depends_on_builds # Get builds of the same type - latest_builds = self.commit.builds.latest + latest_builds = self.pipeline.builds.latest # Return builds from previous stages latest_builds.where('stage_idx < ?', stage_idx) @@ -114,7 +114,7 @@ module Ci def merge_request merge_requests = MergeRequest.includes(:merge_request_diff) - .where(source_branch: ref, source_project_id: commit.gl_project_id) + .where(source_branch: ref, source_project_id: pipeline.gl_project_id) .reorder(iid: :asc) merge_requests.find do |merge_request| diff --git a/app/models/ci/commit.rb b/app/models/ci/pipeline.rb similarity index 98% rename from app/models/ci/commit.rb rename to app/models/ci/pipeline.rb index f22b573a94..74347cf142 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/pipeline.rb @@ -1,5 +1,5 @@ module Ci - class Commit < ActiveRecord::Base + class Pipeline < ActiveRecord::Base extend Ci::Model include Statuseable @@ -47,7 +47,7 @@ module Ci end def short_sha - Ci::Commit.truncate_sha(sha) + Ci::Pipeline.truncate_sha(sha) end def commit_data diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 872d5fb31d..47632c4b40 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -3,7 +3,7 @@ module Ci extend Ci::Model belongs_to :trigger, class_name: 'Ci::Trigger' - belongs_to :commit, class_name: 'Ci::Commit' + belongs_to :commit, class_name: 'Ci::Pipeline' has_many :builds, class_name: 'Ci::Build' serialize :variables diff --git a/app/models/commit.rb b/app/models/commit.rb index f96c7cb34d..b5637bc4fb 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -214,13 +214,13 @@ class Commit @raw.short_id(7) end - def ci_commits - @ci_commits ||= project.ci_commits.where(sha: sha) + def pipelines + @pipeline ||= project.pipelines.where(sha: sha) end def status return @status if defined?(@status) - @status ||= ci_commits.status + @status ||= pipelines.status end def revert_branch_name diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index f774b6e0ef..3752bf09f1 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -4,10 +4,10 @@ class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - belongs_to :commit, class_name: 'Ci::Commit', touch: true + belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true belongs_to :user - validates :commit, presence: true + validates :pipeline, presence: true validates_presence_of :name @@ -44,11 +44,11 @@ class CommitStatus < ActiveRecord::Base end after_transition [:pending, :running] => :success do |commit_status| - MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status) + MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) end after_transition any => :failed do |commit_status| - MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.commit.project, nil).execute(commit_status) + MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 722c258244..6c7668778b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -579,8 +579,8 @@ class MergeRequest < ActiveRecord::Base diverged_commits_count > 0 end - def ci_commit - @ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project + def pipeline + @pipeline ||= source_project.pipeline(last_commit.id, source_branch) if last_commit && source_project end def diff_refs diff --git a/app/models/project.rb b/app/models/project.rb index 525a82c753..e0ea1026b9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -119,7 +119,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id - has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id + has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' @@ -930,12 +930,12 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def ci_commit(sha, ref) - ci_commits.order(id: :desc).find_by(sha: sha, ref: ref) + def pipeline(sha, ref) + pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_ci_commit(sha, ref) - ci_commit(sha, ref) || ci_commits.create(sha: sha, ref: ref) + def ensure_pipeline(sha, ref) + pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref) end def enable_ci diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 5bc0c31cb4..a7751b8eff 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -1,7 +1,7 @@ module Ci class CreatePipelineService < BaseService def execute - pipeline = project.ci_commits.new(params) + pipeline = project.pipelines.new(params) unless ref_names.include?(params[:ref]) pipeline.errors.add(:base, 'Reference not found') @@ -19,7 +19,7 @@ module Ci end begin - Ci::Commit.transaction do + Ci::Pipeline.transaction do pipeline.sha = commit.id unless pipeline.config_processor diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 993acf11db..cd8a2b2510 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -7,7 +7,7 @@ module Ci # check if ref is tag tag = project.repository.find_tag(ref).present? - ci_commit = project.ci_commits.create(sha: commit.sha, ref: ref, tag: tag) + ci_commit = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag) trigger_request = trigger.trigger_requests.create!( variables: variables, diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 3018f27ec0..90eb3e365f 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -3,7 +3,7 @@ module Ci def execute(project, opts) sha = opts[:sha] || ref_sha(project, opts[:ref]) - ci_commits = project.ci_commits.where(sha: sha) + ci_commits = project.pipelines.where(sha: sha) ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref] image_name = image_for_status(ci_commits.status) diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 5b6fefe669..70a7d4bef4 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -18,7 +18,7 @@ class CreateCommitBuildsService return false end - commit = Ci::Commit.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) + commit = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) # Skip creating ci_commit when no gitlab-ci.yml is found unless commit.ci_yaml_file diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 9d7fca6882..bc93ba2552 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -55,12 +55,12 @@ module MergeRequests def each_merge_request(commit_status) merge_request_from(commit_status).each do |merge_request| - ci_commit = merge_request.ci_commit + pipeline = merge_request.pipeline - next unless ci_commit - next unless ci_commit.sha == commit_status.sha + next unless pipeline + next unless pipeline.sha == commit_status.sha - yield merge_request, ci_commit + yield merge_request, pipeline end end end diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index e1b29f1e57..bf01a78cb3 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -169,7 +169,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'repository contains ".gitlab-ci.yml" file' do - allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file).and_return(String.new) + allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(String.new) end step 'I see commit ci info' do diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 9bcd33ff19..0c02b5fd57 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -50,7 +50,7 @@ module API commit = @project.commit(params[:sha]) not_found! 'Commit' unless commit - # Since the CommitStatus is attached to Ci::Commit (in the future Pipeline) + # Since the CommitStatus is attached to Ci::Pipeline (in the future Pipeline) # We need to always have the pipeline object # To have a valid pipeline object that can be attached to specific MR # Other CI service needs to send `ref` diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4e7de8867b..d129c510d6 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -238,7 +238,7 @@ module API should_remove_source_branch: params[:should_remove_source_branch] } - if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active? + if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.pipeline && merge_request.pipeline.active? ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). execute(merge_request) else diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index e163663693..5270108ef0 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -60,7 +60,7 @@ module Ci class BuildTime < Chart def collect - commits = project.ci_commits.last(30) + commits = project.pipelines.last(30) commits.each do |commit| @labels << commit.short_sha diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index 645cd7ae76..d1082f0132 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -17,7 +17,7 @@ # FactoryGirl.define do - factory :ci_empty_commit, class: Ci::Commit do + factory :ci_empty_commit, class: Ci::Pipeline do sha '97de212e80737a608d939f648d959671fb0a0142' project factory: :empty_project diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index acd6fb3538..093f92ffc6 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -167,7 +167,7 @@ describe "Pipelines" do context 'with gitlab-ci.yml' do before { stub_ci_commit_to_return_yaml_file } - it { expect{ click_on 'Create pipeline' }.to change{ Ci::Commit.count }.by(1) } + it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) } end context 'without gitlab-ci.yml' do diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index f942695b6f..45199d0f09 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe CiStatusHelper do include IconsHelper - let(:success_commit) { double("Ci::Commit", status: 'success') } - let(:failed_commit) { double("Ci::Commit", status: 'failed') } + let(:success_commit) { double("Ci::Pipeline", status: 'success') } + let(:failed_commit) { double("Ci::Pipeline", status: 'failed') } describe 'ci_icon_for_status' do it { expect(helper.ci_icon_for_status(success_commit.status)).to include('fa-check') } diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 8e7ed42e88..a3336c8717 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -5,7 +5,7 @@ describe MergeRequestsHelper do let(:project) { create :project } let(:merge_request) { MergeRequest.new } let(:ci_service) { CiService.new } - let(:last_commit) { Ci::Commit.new({}) } + let(:last_commit) { Ci::Pipeline.new({}) } before do allow(merge_request).to receive(:source_project).and_return(project) diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 22f8639e5a..2c6e6db682 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::Commit, models: true do +describe Ci::Pipeline, models: true do let(:project) { FactoryGirl.create :empty_project } let(:commit) { FactoryGirl.create :ci_commit, project: project } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 118e1e22a7..23d09331e5 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -402,7 +402,7 @@ describe MergeRequest, models: true do with('123abc', 'master'). and_return(ci_commit) - expect(subject.ci_commit).to eq(ci_commit) + expect(subject.pipeline).to eq(ci_commit) end end @@ -410,7 +410,7 @@ describe MergeRequest, models: true do it 'returns nil' do allow(subject).to receive(:source_project).and_return(nil) - expect(subject.ci_commit).to be_nil + expect(subject.pipeline).to be_nil end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 338a4c3d3f..44debdbdc1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -403,7 +403,7 @@ describe Project, models: true do let(:project) { create :project } let(:commit) { create :ci_commit, project: project, ref: 'master' } - subject { project.ci_commit(commit.sha, 'master') } + subject { project.pipeline(commit.sha, 'master') } it { is_expected.to eq(commit) } diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 633927c8c3..00d426e979 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -16,8 +16,8 @@ describe API::CommitStatuses, api: true do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } context 'ci commit exists' do - let!(:master) { project.ci_commits.create(sha: commit.id, ref: 'master') } - let!(:develop) { project.ci_commits.create(sha: commit.id, ref: 'develop') } + let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') } + let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') } it_behaves_like 'a paginated resources' do let(:request) { get api(get_url, reporter) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 4b0111df14..d8569d88ef 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -388,7 +388,7 @@ describe API::API, api: true do end describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do - let(:ci_commit) { create(:ci_commit_without_jobs) } + let(:pipeline) { create(:ci_commit_without_jobs) } it "should return merge_request in case of success" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) @@ -429,7 +429,7 @@ describe API::API, api: true do end it "enables merge when build succeeds if the ci is active" do - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) allow(ci_commit).to receive(:active?).and_return(true) put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 0510b77a39..1a0da8d6ba 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -44,7 +44,7 @@ describe API::API do end context 'Have a commit' do - let(:commit) { project.ci_commits.last } + let(:commit) { project.pipelines.last } it 'should create builds' do post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index 0ef03f9371..441f8d613e 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -36,7 +36,7 @@ describe Ci::API::API do end context 'Have a commit' do - let(:commit) { project.ci_commits.last } + let(:commit) { project.pipelines.last } it 'should create builds' do post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 9ae8f31b37..3785723a03 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -20,10 +20,10 @@ describe CreateCommitBuildsService, services: true do ) end - it { expect(commit).to be_kind_of(Ci::Commit) } + it { expect(commit).to be_kind_of(Ci::Pipeline) } it { expect(commit).to be_valid } it { expect(commit).to be_persisted } - it { expect(commit).to eq(project.ci_commits.last) } + it { expect(commit).to eq(project.pipelines.last) } it { expect(commit.builds.first).to be_kind_of(Ci::Build) } end @@ -61,12 +61,12 @@ describe CreateCommitBuildsService, services: true do commits: [{ message: 'Message' }] ) expect(result).to be_falsey - expect(Ci::Commit.count).to eq(0) + expect(Ci::Pipeline.count).to eq(0) end it 'fails commits if yaml is invalid' do message = 'message' - allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } stub_ci_commit_yaml_file('invalid: file: file') commits = [{ message: message }] commit = service.execute(project, user, @@ -85,7 +85,7 @@ describe CreateCommitBuildsService, services: true do let(:message) { "some message[ci skip]" } before do - allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } end it "skips builds creation if there is [ci skip] tag in commit message" do @@ -102,7 +102,7 @@ describe CreateCommitBuildsService, services: true do end it "does not skips builds creation if there is no [ci skip] tag in commit message" do - allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" } + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } commits = [{ message: "some message" }] commit = service.execute(project, user, @@ -133,7 +133,7 @@ describe CreateCommitBuildsService, services: true do end it "skips build creation if there are already builds" do - allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml } + allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml } commits = [{ message: "message" }] commit = service.execute(project, user, diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 0861d74aed..7f8ea8d7c2 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -116,7 +116,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do before do # This behavior of MergeRequest: we instantiate a new object allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_wrap_original do - Ci::Commit.find(ci_commit.id) + Ci::Pipeline.find(ci_commit.id) end # We create test after the build diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index f73416a3d0..49660a48a8 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -18,7 +18,7 @@ module StubGitlabCalls end def stub_ci_commit_yaml_file(ci_yaml) - allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { ci_yaml } + allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml } end def stub_ci_builds_disabled diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 20d3dfb42b..7d0cfed362 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -52,16 +52,16 @@ describe PostReceive do context "gitlab-ci.yml" do subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) } - context "creates a Ci::Commit for every change" do + context "creates a Ci::Pipeline for every change" do before { stub_ci_commit_to_return_yaml_file } - it { expect{ subject }.to change{ Ci::Commit.count }.by(2) } + it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) } end - context "does not create a Ci::Commit" do + context "does not create a Ci::Pipeline" do before { stub_ci_commit_yaml_file(nil) } - it { expect{ subject }.not_to change{ Ci::Commit.count } } + it { expect{ subject }.not_to change{ Ci::Pipeline.count } } end end end From 2d05de7af8de9a11f5bdfec0dd3f294a0148d023 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Tue, 31 May 2016 08:51:13 -0400 Subject: [PATCH 094/507] Cache project build count. Closes #18032 --- .vagrant_enabled | 0 app/models/project.rb | 6 ++++++ app/views/layouts/nav/_project.html.haml | 9 +++++++++ 3 files changed, 15 insertions(+) create mode 100644 .vagrant_enabled diff --git a/.vagrant_enabled b/.vagrant_enabled new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/models/project.rb b/app/models/project.rb index 525a82c753..1375dab8c3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1011,4 +1011,10 @@ class Project < ActiveRecord::Base update_attribute(:pending_delete, true) end + + def running_or_pending_build_count + Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], expires_in: 60) do + builds.running_or_pending.count(:all) + end + end end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2c9b900666..86d2ba9ba2 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -52,6 +52,15 @@ = icon('ship fw') %span Pipelines + %span.badge.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) + + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + = icon('cubes fw') + %span + Builds + %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) - if project_nav_tab? :container_registry = nav_link(controller: %w(container_registry)) do From 4d9622e7d3e927ad158d26c780fee64c6d8183bc Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Tue, 31 May 2016 16:01:16 -0400 Subject: [PATCH 095/507] Invalidate cache on build change --- app/models/ci/build.rb | 1 + app/models/project.rb | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5e77fda70b..f597f920a3 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -313,6 +313,7 @@ module Ci build_data = Gitlab::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) + project.expire_running_or_pending_build_count end def artifacts? diff --git a/app/models/project.rb b/app/models/project.rb index 1375dab8c3..3b5ca05dc3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1013,8 +1013,12 @@ class Project < ActiveRecord::Base end def running_or_pending_build_count - Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], expires_in: 60) do + Rails.cache.fetch(['projects', id, 'running_or_pending_build_count']) do builds.running_or_pending.count(:all) end end + + def expire_running_or_pending_build_count + Rails.cache.delete(['projects', id, 'running_or_pending_build_count']) + end end From 2605a0a844f187daeeff1f16920db445f53e2793 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Wed, 1 Jun 2016 10:50:32 -0400 Subject: [PATCH 096/507] Refactor. Add tests. --- CHANGELOG | 1 + app/models/ci/build.rb | 2 +- app/models/project.rb | 8 ++------ app/views/layouts/nav/_project.html.haml | 9 --------- app/views/projects/pipelines/_head.html.haml | 2 +- features/project/builds/summary.feature | 1 + features/steps/project/builds/summary.rb | 4 ++++ 7 files changed, 10 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dedaf9090a..daffb91606 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 8.9.0 (unreleased) - Make authentication service for Container Registry to be compatible with < Docker 1.11 - Add Application Setting to configure Container Registry token expire delay (default 5min) - Cache assigned issue and merge request counts in sidebar nav + - Cache project build count in sidebar nav v 8.8.3 - Fix incorrect links on pipeline page when merge request created from fork diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index f597f920a3..64723ab6b4 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -313,7 +313,7 @@ module Ci build_data = Gitlab::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) - project.expire_running_or_pending_build_count + project.running_or_pending_build_count(force: true) end def artifacts? diff --git a/app/models/project.rb b/app/models/project.rb index 3b5ca05dc3..9ccf6a97df 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1012,13 +1012,9 @@ class Project < ActiveRecord::Base update_attribute(:pending_delete, true) end - def running_or_pending_build_count - Rails.cache.fetch(['projects', id, 'running_or_pending_build_count']) do + def running_or_pending_build_count(force: false) + Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do builds.running_or_pending.count(:all) end end - - def expire_running_or_pending_build_count - Rails.cache.delete(['projects', id, 'running_or_pending_build_count']) - end end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 86d2ba9ba2..2c9b900666 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -52,15 +52,6 @@ = icon('ship fw') %span Pipelines - %span.badge.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) - - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - = icon('cubes fw') - %span - Builds - %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) - if project_nav_tab? :container_registry = nav_link(controller: %w(container_registry)) do diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index 2c8ae625e6..6e757df541 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -11,4 +11,4 @@ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do %span Builds - %span.badge.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) + %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature index 3c029a973d..550ebccf0d 100644 --- a/features/project/builds/summary.feature +++ b/features/project/builds/summary.feature @@ -24,3 +24,4 @@ Feature: Project Builds Summary Then recent build has been erased And recent build summary does not have artifacts widget And recent build summary contains information saying that build has been erased + And the build count cache is updated diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index e9e2359146..374eb0b0e0 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -36,4 +36,8 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps expect(page).to have_content 'Build has been erased' end end + + step 'the build count cache is updated' do + expect(@build.project.running_or_pending_build_count).to eq @build.project.builds.running_or_pending.count(:all) + end end From 04fdf4b9a926129d63d0483ad8cd7e748f3a4e07 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 2 Jun 2016 17:00:16 +0100 Subject: [PATCH 097/507] fixup! Add `sha` parameter to MR accept API --- lib/api/merge_requests.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 50baf4c09a..db304abe1c 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -235,7 +235,7 @@ module API render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? if params[:sha] && merge_request.source_sha != params[:sha] - render_api_error!("SHA does not match HEAD of source branch: #{merge_request.target_sha}", 409) + render_api_error!("SHA does not match HEAD of source branch: #{merge_request.source_sha}", 409) end merge_params = { From 4f726683cb59da54f47302880d5c0c447638402a Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 2 Jun 2016 17:00:42 +0100 Subject: [PATCH 098/507] fixup! Don't allow merges with new commits --- .../merge_requests/widget/open/_sha_mismatch.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml index a78583b7c6..499624f8dd 100644 --- a/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml +++ b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml @@ -1,6 +1,6 @@ %h4 = icon("exclamation-triangle") - This merge request has new commits since the page was loaded. + This merge request has received new commits since the page was loaded. %p - Please review the new commits before merging. + Please reload the page to review the new commits before merging. From bc351d5ac6e782a078c9435abd92146a4f3eecab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 2 Jun 2016 18:10:16 +0200 Subject: [PATCH 099/507] Fix link to current design reference in the CONTRIBUTING guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a15f8c4fec..02d2b77f5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the [free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design (the PNG is 1:1). -The current designs can be found in the [`gitlab1.atype` file]. +The current designs can be found in the [`gitlab8.atype` file]. ### UI development kit @@ -530,4 +530,4 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 -[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/ +[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/ From 905e8b6b545dfb6bc408824889b66d47aa02eda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 2 Jun 2016 18:14:33 +0200 Subject: [PATCH 100/507] Remove unused Issuable#is_assigned? method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/models/concerns/issuable.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 50f5b749e3..37124379c1 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -171,10 +171,6 @@ module Issuable today? && created_at == updated_at end - def is_assigned? - !!assignee_id - end - def is_being_reassigned? assignee_id_changed? end From 34007aa0dc61df80514cc0ff125eef8fcf57e35a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 2 Jun 2016 18:25:44 +0200 Subject: [PATCH 101/507] Fix deprecation warnings in spec/services/issues/bulk_update_service_spec.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/services/issues/bulk_update_service_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 96f050bbd9..454d584949 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -18,7 +18,7 @@ describe Issues::BulkUpdateService, services: true do @issues = create_list(:issue, 5, project: @project) @params = { state_event: 'close', - issues_ids: @issues.map(&:id) + issues_ids: @issues.map(&:id).join(",") } end @@ -38,7 +38,7 @@ describe Issues::BulkUpdateService, services: true do @issues = create_list(:closed_issue, 5, project: @project) @params = { state_event: 'reopen', - issues_ids: @issues.map(&:id) + issues_ids: @issues.map(&:id).join(",") } end @@ -58,7 +58,7 @@ describe Issues::BulkUpdateService, services: true do before do @new_assignee = create :user @params = { - issues_ids: [issue.id], + issues_ids: issue.id.to_s, assignee_id: @new_assignee.id } end @@ -97,7 +97,7 @@ describe Issues::BulkUpdateService, services: true do before do @milestone = create(:milestone, project: @project) @params = { - issues_ids: [issue.id], + issues_ids: issue.id.to_s, milestone_id: @milestone.id } end From ab75b21c83a80d144716e63aa8731092af3eb0cc Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 2 Jun 2016 13:59:34 -0400 Subject: [PATCH 102/507] Update CHANGELOG for 8.8.3 [ci skip] --- CHANGELOG | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6a87e09194..27f60c7de0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,13 +32,23 @@ v 8.9.0 (unreleased) - Cache project build count in sidebar nav v 8.8.3 - - Fix incorrect links on pipeline page when merge request created from fork - - Fix gitlab importer failing to import new projects due to missing credentials - - Fix serious performance bug with rendering Markdown with InlineDiffFilter - - Fix import URL migration not rescuing with the correct Error - - In search results, only show notes on confidential issues that the user has access to - - Fix health check access token changing due to old application settings being used - - Fix wiki project clone address error (chujinjin) + - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 + - Fixed JS error when trying to remove discussion form. !4303 + - Fixed issue with button color when no CI enabled. !4287 + - Fixed potential issue with 2 CI status polling events happening. !3869 + - Improve design of Pipeline view. !4230 + - Fix gitlab importer failing to import new projects due to missing credentials. !4301 + - Fix import URL migration not rescuing with the correct Error. !4321 + - Fix health check access token changing due to old application settings being used. !4332 + - Make authentication service for Container Registry to be compatible with Docker versions before 1.11. !4363 + - Add Application Setting to configure Container Registry token expire delay (default 5 min). !4364 + - Pass the "Remember me" value to the 2FA token form. !4369 + - Fix incorrect links on pipeline page when merge request created from fork. !4376 + - Use downcased path to container repository as this is expected path by Docker. !4420 + - Fix wiki project clone address error (chujinjin). !4429 + - Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392 + - Fix missing number on generated ordered list element. !4437 + - Prevent disclosure of notes on confidential issues in search results. v 8.8.2 - Added remove due date button. !4209 From c3e923c496b7d1c344a5fa68cef4a80ce23c90d0 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 25 May 2016 18:52:10 -0700 Subject: [PATCH 103/507] Ensure we don't show TODOS for projects pending delete By joining the Todos on the project table. --- app/finders/todos_finder.rb | 2 +- spec/features/todos/todos_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 4bd46a7608..f638b5bf91 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -23,7 +23,7 @@ class TodosFinder end def execute - items = current_user.todos + items = current_user.todos.joins(:project).where(projects: { pending_delete: false }) items = by_action_id(items) items = by_author(items) items = by_project(items) diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 4e627753cc..c8c86a3ff4 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -98,5 +98,18 @@ describe 'Dashboard Todos', feature: true do end end end + + context 'User has a Todo in a project pending deletion' do + before do + deleted_project = create(:project, pending_delete: true) + create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author) + login_as(user) + visit dashboard_todos_path + end + + it 'shows "All done" message' do + expect(page).to have_content "You're all done!" + end + end end end From 4ecc10fade61a1b45cd45ea4189e95a2acbea353 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 25 May 2016 21:31:36 -0700 Subject: [PATCH 104/507] Reduce the filters on the todos joins project query by being explicit about the join --- app/finders/todos_finder.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index f638b5bf91..3243d62fc9 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -23,7 +23,13 @@ class TodosFinder end def execute - items = current_user.todos.joins(:project).where(projects: { pending_delete: false }) + items = current_user.todos + + # Filter out todos linked to project pending deletion + items = items.joins( + 'INNER JOIN projects ON projects.id = todos.project_id AND projects.pending_delete = false' + ) + items = by_action_id(items) items = by_author(items) items = by_project(items) From 4280575fc0888632196cf4483dcd777618c13390 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Mon, 30 May 2016 10:59:14 -0700 Subject: [PATCH 105/507] Move filtering todos by projects not pending deletion into a scope on the todo model --- app/finders/todos_finder.rb | 8 +------- app/models/todo.rb | 1 + 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 3243d62fc9..5d7d55180e 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -23,13 +23,7 @@ class TodosFinder end def execute - items = current_user.todos - - # Filter out todos linked to project pending deletion - items = items.joins( - 'INNER JOIN projects ON projects.id = todos.project_id AND projects.pending_delete = false' - ) - + items = current_user.todos.not_pending_delete items = by_action_id(items) items = by_author(items) items = by_project(items) diff --git a/app/models/todo.rb b/app/models/todo.rb index 3a09137332..f66755436e 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -19,6 +19,7 @@ class Todo < ActiveRecord::Base scope :pending, -> { with_state(:pending) } scope :done, -> { with_state(:done) } + scope :not_pending_delete, -> { joins('INNER JOIN projects ON projects.id = todos.project_id AND projects.pending_delete = false') } state_machine :state, initial: :pending do event :done do From b173ea2bd4bbc65529b827f9afa5999f6f04579e Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 1 Jun 2016 16:44:35 -0700 Subject: [PATCH 106/507] Use the project finder in the todos finder to limit todos to just ones within projects you have access to. --- app/finders/todos_finder.rb | 14 +++++++++++++- app/models/todo.rb | 1 - spec/features/todos/todos_spec.rb | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 5d7d55180e..6fbe68a720 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -23,7 +23,7 @@ class TodosFinder end def execute - items = current_user.todos.not_pending_delete + items = current_user.todos items = by_action_id(items) items = by_author(items) items = by_project(items) @@ -78,6 +78,16 @@ class TodosFinder @project end + def projects + return @projects if defined?(@projects) + + if project? + @projects = project + else + @projects = ProjectsFinder.new.execute(current_user).reorder(nil) + end + end + def type? type.present? && ['Issue', 'MergeRequest'].include?(type) end @@ -105,6 +115,8 @@ class TodosFinder def by_project(items) if project? items = items.where(project: project) + elsif projects + items = items.merge(projects).joins(:project) end items diff --git a/app/models/todo.rb b/app/models/todo.rb index f66755436e..3a09137332 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -19,7 +19,6 @@ class Todo < ActiveRecord::Base scope :pending, -> { with_state(:pending) } scope :done, -> { with_state(:done) } - scope :not_pending_delete, -> { joins('INNER JOIN projects ON projects.id = todos.project_id AND projects.pending_delete = false') } state_machine :state, initial: :pending do event :done do diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index c8c86a3ff4..c0a1cd64f3 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Dashboard Todos', feature: true do let(:user) { create(:user) } let(:author) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:issue) { create(:issue) } describe 'GET /dashboard/todos' do From 14a9b0d7dda78e88852644bd9a6c05922b5e367d Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 2 Jun 2016 12:06:24 -0700 Subject: [PATCH 107/507] Update target todo test to use a public project --- CHANGELOG | 3 +++ spec/features/todos/target_state_spec.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 27f60c7de0..ef5b4aa79c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,9 @@ v 8.9.0 (unreleased) - Cache assigned issue and merge request counts in sidebar nav - Cache project build count in sidebar nav +v 8.8.4 + - Fix todos page throwing errors when you have a project pending deletion + v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 - Fixed JS error when trying to remove discussion form. !4303 diff --git a/spec/features/todos/target_state_spec.rb b/spec/features/todos/target_state_spec.rb index 72491ac7e6..32fa88a2b2 100644 --- a/spec/features/todos/target_state_spec.rb +++ b/spec/features/todos/target_state_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' feature 'Todo target states', feature: true do let(:user) { create(:user) } let(:author) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } before do login_as user From 078ba8c090dd30cb4cad7ef6dc5380e2036e2e6a Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Thu, 2 Jun 2016 13:17:54 +0200 Subject: [PATCH 108/507] issuable#labels_array explicitly load the labels This will be useful when you want to ask for the number of items and later iterate over them, without needing to ask if the association is load or not. So you avoid extra database queries --- CHANGELOG | 1 + app/models/concerns/issuable.rb | 4 ++++ app/views/shared/issuable/_sidebar.html.haml | 10 +++++----- spec/models/concerns/issuable_spec.rb | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6a87e09194..84e755e4ea 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -30,6 +30,7 @@ v 8.9.0 (unreleased) - Add Application Setting to configure Container Registry token expire delay (default 5min) - Cache assigned issue and merge request counts in sidebar nav - Cache project build count in sidebar nav + - Reduce number of queries needed to render issue labels in the sidebar v 8.8.3 - Fix incorrect links on pipeline page when merge request created from fork diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 50f5b749e3..e86d5236ab 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -213,6 +213,10 @@ module Issuable hook_data end + def labels_array + labels.to_a + end + def label_names labels.order('title ASC').pluck(:title) end diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c1eec45019..d6552ae7f1 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -114,20 +114,20 @@ .sidebar-collapsed-icon = icon('tags') %span - = issuable.labels.count + = issuable.labels_array.size .title.hide-collapsed Labels = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } - - if issuable.labels.any? - - issuable.labels.each do |label| + .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } + - if issuable.labels_array.any? + - issuable.labels_array.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else .light None .selectbox.hide-collapsed - - issuable.labels.each do |label| + - issuable.labels_array.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index fb20578d8d..e9f827e9f5 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -227,6 +227,20 @@ describe Issue, "Issuable" do end end + describe '#labels_array' do + let(:project) { create(:project) } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:issue) { create(:issue, project: project) } + + before(:each) do + issue.labels << bug + end + + it 'loads the association and returns it as an array' do + expect(issue.reload.labels_array).to eq([bug]) + end + end + describe "votes" do let(:project) { issue.project } From 33c499441a91c068c4b3e73748b50a2369450a5a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 2 Jun 2016 22:43:02 +0300 Subject: [PATCH 109/507] Add icons and buttons to UI guide Signed-off-by: Dmitriy Zaporozhets --- doc/development/ui_guide.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index b4dcb74835..ecaa306756 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -33,4 +33,24 @@ be under 'Wiki' tab and so on and so forth. We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide part of the UI for smaller resolutions in favor of a better user experience. However core functionality like browsing files, creating issues, writing comments, should -be available on all resolutions. \ No newline at end of file +be available on all resolutions. + +## Icons + +* `trash` icon for button or link that does destructive action like removing +information from database or file system +* `x` icon for closing/hiding UI element. For example close modal window +* `pencil` icon for edit button or link +* `eye` icon for subscribe action +* `rss` for rss/atom feed +* `plus` for link or dropdown that lead to page wher eyou create new object (For example new issue page) + + +## Buttons + +* Button should contain icon or text. Exceptions should be approved by UX designer. +* Use gray button on white background or white button on gray background. +* Use red button for destructive actions (not revertable). For example removing issue. +* Use green or blue button for primary action. Primary button should be only one. +Do not use both green and blue button in one form. + From 814b26cfc3829a683ce565cbbfc8525dd768299d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 2 Jun 2016 23:04:08 +0300 Subject: [PATCH 110/507] Fix typo Signed-off-by: Dmitriy Zaporozhets --- doc/development/ui_guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index ecaa306756..23760a14b3 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -43,7 +43,7 @@ information from database or file system * `pencil` icon for edit button or link * `eye` icon for subscribe action * `rss` for rss/atom feed -* `plus` for link or dropdown that lead to page wher eyou create new object (For example new issue page) +* `plus` for link or dropdown that lead to page where you create new object (For example new issue page) ## Buttons From 91937d5b312a37d2b602623f57dad3c0ae8af813 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 3 Jun 2016 00:00:48 +0300 Subject: [PATCH 111/507] Added a new common util called $.timefor. It will use $.timeago behind the scene and it does the opposite of what $.timeago does. $.timefor("Thu Jun 05 2016 23:40:39 GMT+0300 (EEST)") will return "3 days remaining". --- .../javascripts/lib/common_utils.js.coffee | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/assets/javascripts/lib/common_utils.js.coffee diff --git a/app/assets/javascripts/lib/common_utils.js.coffee b/app/assets/javascripts/lib/common_utils.js.coffee new file mode 100644 index 0000000000..a7cc07608a --- /dev/null +++ b/app/assets/javascripts/lib/common_utils.js.coffee @@ -0,0 +1,24 @@ +((w) -> + + jQuery.timefor = (time, suffix, expiredLabel) -> + + return '' unless time + + suffix or= 'remaining' + expiredLabel or= 'expired' + + jQuery.timeago.settings.allowFuture = yes + + { suffixFromNow } = jQuery.timeago.settings.strings + jQuery.timeago.settings.strings.suffixFromNow = suffix + + timefor = $.timeago time + + if timefor.indexOf('ago') > -1 + timefor = expiredLabel + + jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow + + return timefor + +) window From 5cae36d16ca730c057badb0f41ee25a9830e5abd Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 3 Jun 2016 00:01:11 +0300 Subject: [PATCH 112/507] Show milestone remaining tooltip in right sidebar. --- app/assets/javascripts/milestone_select.js.coffee | 15 +++++++++++++-- app/helpers/milestones_helper.rb | 15 ++++++++++----- app/views/shared/issuable/_sidebar.html.haml | 6 ++++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 345a0e447a..e8d489dce9 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -24,11 +24,21 @@ class @MilestoneSelect if issueUpdateURL milestoneLinkTemplate = _.template( - '<%= _.escape(title) %>' + ' + + <%= _.escape(title) %> + + ' ) milestoneLinkNoneTemplate = '
                                      None
                                      ' + collapsedSidebarLabelTemplate = _.template( + ' + <%= _.escape(title) %> + ' + ) + $dropdown.glDropdown( data: (term, callback) -> $.ajax( @@ -122,8 +132,9 @@ class @MilestoneSelect if data.milestone? data.milestone.namespace = _this.currentProject.namespace data.milestone.path = _this.currentProject.path + data.milestone.remaining = $.timefor data.milestone.due_date $value.html(milestoneLinkTemplate(data.milestone)) - $sidebarCollapsedValue.find('span').text(data.milestone.title) + $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)) else $value.html(milestoneLinkNoneTemplate) $sidebarCollapsedValue.find('span').text('No') diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 87fc2db690..2abb0e9b17 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -54,13 +54,18 @@ module MilestonesHelper end end - def milestone_remaining_days(milestone) + def milestone_remaining_days(milestone, withContentTag = true) if milestone.expired? - content_tag(:strong, 'expired') + if withContentTag then content_tag(:strong, 'expired') else 'expired' end elsif milestone.due_date - days = milestone.remaining_days - content = content_tag(:strong, days) - content << " #{'day'.pluralize(days)} remaining" + days = milestone.remaining_days + + if withContentTag + content = content_tag(:strong, days) + content << " #{'day'.pluralize(days)} remaining" + else + "#{days} #{'day'.pluralize(days)} remaining" + end end end end diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c1eec45019..be038cab94 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -56,7 +56,8 @@ = icon('clock-o') %span - if issuable.milestone - = issuable.milestone.title + %span.has-tooltip{"data-container" => "body", "data-placement" => "left", "data-original-title" => milestone_remaining_days(issuable.milestone, false)} + = issuable.milestone.title - else None .title.hide-collapsed @@ -67,7 +68,8 @@ .value.bold.hide-collapsed - if issuable.milestone = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do - = issuable.milestone.title + %span.has-tooltip{"data-container" => "body", "data-original-title" => milestone_remaining_days(issuable.milestone, false)} + = issuable.milestone.title - else .light None From f0ca487cd513d50c807e4226a1ee459586f16b08 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 2 Jun 2016 14:17:46 -0700 Subject: [PATCH 113/507] Reorder the todos because the use of the project finder attempts to order them differently --- app/finders/todos_finder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 6fbe68a720..1d88116d7d 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -30,7 +30,7 @@ class TodosFinder items = by_state(items) items = by_type(items) - items + items.reorder(id: :desc) end private @@ -84,7 +84,7 @@ class TodosFinder if project? @projects = project else - @projects = ProjectsFinder.new.execute(current_user).reorder(nil) + @projects = ProjectsFinder.new.execute(current_user) end end From 7cb9c7d8ea97a17029a7b24b5bcc870f1063e025 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 3 Jun 2016 00:23:18 +0300 Subject: [PATCH 114/507] Update CHANGELOG. Add milestone expire date to the right sidebar. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 27f60c7de0..d9beae0a06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -30,6 +30,7 @@ v 8.9.0 (unreleased) - Add Application Setting to configure Container Registry token expire delay (default 5min) - Cache assigned issue and merge request counts in sidebar nav - Cache project build count in sidebar nav + - Add milestone expire date to the right sidebar v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 From 86675194aa44236c3e9acc4aa7ede143f718685e Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 2 Jun 2016 15:30:13 -0700 Subject: [PATCH 115/507] Fix failing todo tests --- spec/features/todos/todos_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index c0a1cd64f3..8e1833a069 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -49,7 +49,7 @@ describe 'Dashboard Todos', feature: true do note1 = create(:note_on_issue, note: "Hello #{label1.to_reference(format: :name)}", noteable_id: issue.id, noteable_type: 'Issue', project: issue.project) create(:todo, :mentioned, project: project, target: issue, user: user, note_id: note1.id) - project2 = create(:project) + project2 = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) label2 = create(:label, project: project2) issue2 = create(:issue, project: project2) note2 = create(:note_on_issue, note: "Test #{label2.to_reference(format: :name)}", noteable_id: issue2.id, noteable_type: 'Issue', project: project2) @@ -101,7 +101,7 @@ describe 'Dashboard Todos', feature: true do context 'User has a Todo in a project pending deletion' do before do - deleted_project = create(:project, pending_delete: true) + deleted_project = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC, pending_delete: true) create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author) login_as(user) visit dashboard_todos_path From 3b4f03de8fb0de0b882d499c9259f053fa69d9e6 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 2 Jun 2016 19:48:11 -0300 Subject: [PATCH 116/507] Ensure branch cleanup regardless of whether the import process succeeds --- CHANGELOG | 3 +++ lib/gitlab/github_import/importer.rb | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 68ea866968..6ead604b72 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,9 @@ v 8.9.0 (unreleased) - Cache project build count in sidebar nav - Reduce number of queries needed to render issue labels in the sidebar +v 8.8.4 (unreleased) + - Ensure branch cleanup regardless of whether the GitHub import process succeeds + v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 - Fixed JS error when trying to remove discussion form. !4303 diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 408d9b7963..9d077e79c3 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -89,11 +89,11 @@ module Gitlab end end - delete_refs(branches_removed) - true rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message + ensure + delete_refs(branches_removed) end def create_refs(branches) From b2b2b2f9de1a7d96decbd1e54336ba805a059d9e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 30 May 2016 18:02:54 +0200 Subject: [PATCH 117/507] fix create service error handling - missing setting import status to failed --- app/services/projects/create_service.rb | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 6728fabea1..b73389f048 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -56,14 +56,14 @@ module Projects after_create_actions if @project.persisted? - @project.add_import_job if @project.import? - + if @project.errors.empty? + @project.add_import_job if @project.import? + else + fail(error: @project.errors.join(', ')) + end @project rescue => e - message = "Unable to save project: #{e.message}" - Rails.logger.error(message) - @project.errors.add(:base, message) if @project - @project + fail(error: e.message) end protected @@ -103,5 +103,19 @@ module Projects end end end + + def fail(error:) + message = "Unable to save project. Error: #{error}" + message << "Project ID: #{@project.id}" if @project && @project.id + + Rails.logger.error(message) + + if @project && @project.import? + @project.errors.add(:base, message) + @project.mark_import_as_failed(message) + end + + @project + end end end From b2b3e0e6ee8e428322ae6aff172b59e040750ae0 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 31 May 2016 10:53:50 +0200 Subject: [PATCH 118/507] fix empty message on shell error --- app/services/projects/import_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index ef15ef6a47..c4838d31f2 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -39,7 +39,7 @@ module Projects begin gitlab_shell.import_repository(project.path_with_namespace, project.import_url) rescue Gitlab::Shell::Error => e - raise Error, e.message + raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" end end From 64c3905523808942ac62b7ebfbce3634b6e9dc17 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 31 May 2016 11:00:59 +0200 Subject: [PATCH 119/507] some refactoring and fixing spec --- app/models/project.rb | 5 +++++ app/workers/repository_fork_worker.rb | 6 ++---- app/workers/repository_import_worker.rb | 3 +-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 9ccf6a97df..03429fb7a4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1017,4 +1017,9 @@ class Project < ActiveRecord::Base builds.running_or_pending.count(:all) end end + + def mark_import_as_failed(error_message) + import_fail + update_column(:import_error, Gitlab::UrlSanitizer.sanitize(error_message)) + end end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index f9e3233798..d947f10551 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -15,8 +15,7 @@ class RepositoryForkWorker result = gitlab_shell.fork_repository(source_path, target_path) unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") - project.update(import_error: "The project could not be forked.") - project.import_fail + project.mark_import_as_failed('The project could not be forked.') return end @@ -24,8 +23,7 @@ class RepositoryForkWorker unless project.valid_repo? logger.error("Project #{project_id} had an invalid repository after fork") - project.update(import_error: "The forked repository is invalid.") - project.import_fail + project.mark_import_as_failed('The forked repository is invalid.') return end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index fbc7ed63c6..56411bca57 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -13,8 +13,7 @@ class RepositoryImportWorker result = Projects::ImportService.new(project, current_user).execute if result[:status] == :error - project.update(import_error: Gitlab::UrlSanitizer.sanitize(result[:message])) - project.import_fail + project.mark_import_as_failed(Gitlab::UrlSanitizer.sanitize(result[:message])) return end From 097eafc8c74f97912a52399d422fb24be84e734d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 31 May 2016 14:53:33 +0200 Subject: [PATCH 120/507] fix some issues and improved error output for forking --- app/models/project.rb | 9 ++++++++- app/services/projects/create_service.rb | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 03429fb7a4..e4a9d17a20 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1019,7 +1019,14 @@ class Project < ActiveRecord::Base end def mark_import_as_failed(error_message) + original_errors = errors.dup + sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message) + import_fail - update_column(:import_error, Gitlab::UrlSanitizer.sanitize(error_message)) + update_column(:import_error, sanitized_message) + rescue ActiveRecord::ActiveRecordError => e + Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}") + ensure + @errors = original_errors end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index b73389f048..61cac5419a 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -59,7 +59,7 @@ module Projects if @project.errors.empty? @project.add_import_job if @project.import? else - fail(error: @project.errors.join(', ')) + fail(error: @project.errors.full_messages.join(', ')) end @project rescue => e From f2ec6341b35c3f0ec797d01420416893c780e15d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 31 May 2016 15:12:08 +0200 Subject: [PATCH 121/507] added changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 68ea866968..62870aee08 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ v 8.9.0 (unreleased) - Cache assigned issue and merge request counts in sidebar nav - Cache project build count in sidebar nav - Reduce number of queries needed to render issue labels in the sidebar + - Improve error handling importing projects v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 From d9df05e14312f38ee3d8f97a068efd0c4890c9a4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 31 May 2016 15:47:51 +0200 Subject: [PATCH 122/507] fix import service spec --- spec/services/projects/import_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 7f2dcdab96..9d90bfceb7 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -49,7 +49,7 @@ describe Projects::ImportService, services: true do result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq 'Failed to import the repository' + expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository" end end From c9e8acd05846e981e26f940bd8c529839fcfc4a1 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 1 Jun 2016 14:55:14 +0000 Subject: [PATCH 123/507] Update repository_import_worker.rb --- app/workers/repository_import_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 56411bca57..7d819fe78f 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -13,7 +13,7 @@ class RepositoryImportWorker result = Projects::ImportService.new(project, current_user).execute if result[:status] == :error - project.mark_import_as_failed(Gitlab::UrlSanitizer.sanitize(result[:message])) + project.mark_import_as_failed(result[:message]) return end From 56a17a7701229a35af2cf710261cdeef15a6f8ad Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 2 Jun 2016 11:15:45 +0300 Subject: [PATCH 124/507] Put project Files and Commits tabs under Code tab Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + .../projects/branches_controller.rb | 2 +- app/views/layouts/nav/_project.html.haml | 19 +++++++++---------- app/views/projects/branches/destroy.js.haml | 1 - app/views/projects/commits/_head.html.haml | 8 ++++---- app/views/projects/tags/destroy.js.haml | 1 - app/views/projects/tree/show.html.haml | 1 + 7 files changed, 16 insertions(+), 17 deletions(-) delete mode 100644 app/views/projects/branches/destroy.js.haml diff --git a/CHANGELOG b/CHANGELOG index 62870aee08..ec026b8f39 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,7 @@ v 8.9.0 (unreleased) - Cache project build count in sidebar nav - Reduce number of queries needed to render issue labels in the sidebar - Improve error handling importing projects + - Put project Files and Commits tabs under Code tab v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d09e7375b6..8289f55849 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController redirect_to namespace_project_branches_path(@project.namespace, @project), status: 303 end - format.js { render status: status[:return_code] } + format.js { head :ok } end end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2c9b900666..9792c1c93b 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -33,18 +33,11 @@ %span Activity - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do - = icon('files-o fw') + = icon('code fw') %span - Files - - - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - = icon('history fw') - %span - Commits + Code - if project_nav_tab? :pipelines = nav_link(controller: :pipelines) do @@ -129,4 +122,10 @@ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do Builds + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits + .fade-right diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml deleted file mode 100644 index a21ddaf493..0000000000 --- a/app/views/projects/branches/destroy.js.haml +++ /dev/null @@ -1 +0,0 @@ -$('.js-totalbranch-count').html("#{@repository.branch_count}") diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index d1bd76ab52..1c136133ab 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,9 +1,11 @@ %ul.nav-links + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project) do + Files + = nav_link(controller: [:commit, :commits]) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits - %span.badge - = number_with_delimiter(@repository.commit_count) = nav_link(controller: %w(network)) do = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do @@ -16,9 +18,7 @@ = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do Branches - %span.badge.js-totalbranch-count= @repository.branch_count = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags - %span.badge.js-totaltags-count= @repository.tag_count diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml index ffeacb5a00..e4a78fadbe 100644 --- a/app/views/projects/tags/destroy.js.haml +++ b/app/views/projects/tags/destroy.js.haml @@ -1,3 +1,2 @@ -$('.js-totaltags-count').html("#{@repository.tags.size}"); - if @repository.tags.empty? $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 7e9ba09c72..59f60c4687 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -3,6 +3,7 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = render 'projects/last_push' += render "projects/commits/head" .tree-controls = render 'projects/find_file_link' From 7cfb445c15bdec0268f0a3800481e9cced973648 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 2 Jun 2016 19:35:46 +0300 Subject: [PATCH 125/507] Update tests to match new Code tab logic Signed-off-by: Dmitriy Zaporozhets --- .../projects/branches_controller.rb | 2 +- features/project/active_tab.feature | 37 ++++++++++--------- features/steps/project/active_tab.rb | 4 ++ features/steps/shared/project_tab.rb | 8 +--- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 8289f55849..dd9508da04 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController redirect_to namespace_project_branches_path(@project.namespace, @project), status: 303 end - format.js { head :ok } + format.js { render nothing: true, status: status[:return_code] } end end diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 5125a3e577..26e6750302 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -10,14 +10,9 @@ Feature: Project Active Tab Then the active main tab should be Home And no other main tabs should be active - Scenario: On Project Files + Scenario: On Project Code Given I visit my project's files page - Then the active main tab should be Files - And no other main tabs should be active - - Scenario: On Project Commits - Given I visit my project's commits page - Then the active main tab should be Commits + Then the active main tab should be Code And no other main tabs should be active Scenario: On Project Issues @@ -64,40 +59,46 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings - # Sub Tabs: Commits + # Sub Tabs: Code - Scenario: On Project Commits/Commits + Scenario: On Project Code/Files + Given I visit my project's files page + Then the active sub tab should be Files + And no other sub tabs should be active + And the active main tab should be Code + + Scenario: On Project Code/Commits Given I visit my project's commits page Then the active sub tab should be Commits And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Network + Scenario: On Project Code/Network Given I visit my project's network page Then the active sub tab should be Network And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Compare + Scenario: On Project Code/Compare Given I visit my project's commits page And I click the "Compare" tab Then the active sub tab should be Compare And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Branches + Scenario: On Project Code/Branches Given I visit my project's commits page And I click the "Branches" tab Then the active sub tab should be Branches And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Tags + Scenario: On Project Code/Tags Given I visit my project's commits page And I click the "Tags" tab Then the active sub tab should be Tags And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code Scenario: On Project Issues/Browse Given I visit my project's issues page diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 4a5a71e7e6..7db0d33719 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -63,6 +63,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Tags') end + step 'the active sub tab should be Files' do + ensure_active_sub_tab('Files') + end + step 'the active sub tab should be Commits' do ensure_active_sub_tab('Commits') end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index b209020c5a..988ce0d110 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -8,12 +8,8 @@ module SharedProjectTab ensure_active_main_tab('Project') end - step 'the active main tab should be Files' do - ensure_active_main_tab('Files') - end - - step 'the active main tab should be Commits' do - ensure_active_main_tab('Commits') + step 'the active main tab should be Code' do + ensure_active_main_tab('Code') end step 'the active main tab should be Graphs' do From db2109b086340a5bf5e049aa16f22a76c0a5aec7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 2 Jun 2016 22:32:22 +0300 Subject: [PATCH 126/507] Fix more tests with new Code tab Signed-off-by: Dmitriy Zaporozhets --- features/project/shortcuts.feature | 8 +++++--- features/steps/project/active_tab.rb | 8 -------- features/steps/project/project_find_file.rb | 4 ++-- features/steps/shared/project_tab.rb | 8 ++++++++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index 10e7c23461..c73d0b3233 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -8,19 +8,21 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to files tab Given I press "g" and "f" - Then the active main tab should be Files + Then the active main tab should be Code + Then the active sub tab should be Files @javascript Scenario: Navigate to commits tab Given I visit my project's files page Given I press "g" and "c" - Then the active main tab should be Commits + Then the active main tab should be Code + Then the active sub tab should be Commits @javascript Scenario: Navigate to network tab Given I press "g" and "n" Then the active sub tab should be Network - And the active main tab should be Commits + And the active main tab should be Code @javascript Scenario: Navigate to graphs tab diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 7db0d33719..745fd3471c 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -63,14 +63,6 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Tags') end - step 'the active sub tab should be Files' do - ensure_active_sub_tab('Files') - end - - step 'the active sub tab should be Commits' do - ensure_active_sub_tab('Commits') - end - step 'the active sub tab should be Compare' do ensure_active_sub_tab('Compare') end diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb index 8c1d09d6cc..47de4b91df 100644 --- a/features/steps/project/project_find_file.rb +++ b/features/steps/project/project_find_file.rb @@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps end step 'I should see "find file" page' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end step 'I fill in Find by path with "git"' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 988ce0d110..bfee879330 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -47,4 +47,12 @@ module SharedProjectTab step 'the active sub tab should be Network' do ensure_active_sub_tab('Network') end + + step 'the active sub tab should be Files' do + ensure_active_sub_tab('Files') + end + + step 'the active sub tab should be Commits' do + ensure_active_sub_tab('Commits') + end end From 334cb868213bfc08e83c6d12fbda04ca15fd67f0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 2 Jun 2016 23:48:04 +0300 Subject: [PATCH 127/507] Update test since branch removal does not render template anymore Signed-off-by: Dmitriy Zaporozhets --- spec/controllers/projects/branches_controller_spec.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 8ad7347211..c4b4a888b4 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -122,27 +122,23 @@ describe Projects::BranchesController do let(:branch) { "feature" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with unencoded slashes" do let(:branch) { "improve/awesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with encoded slashes" do let(:branch) { "improve%2Fawesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "invalid branch name, valid ref" do let(:branch) { "no-branch" } it { expect(response.status).to eq(404) } - it { expect(subject).to render_template('destroy') } end end end From a63ea487f7b949b43ab0c1790e9c640288a88649 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 1 Jun 2016 13:11:28 +0200 Subject: [PATCH 128/507] Extend specs for builds badge Related to #17549 --- spec/lib/gitlab/badge/build_spec.rb | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index b6f7a2e7ec..6b2b335d4f 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -42,9 +42,7 @@ describe Gitlab::Badge::Build do end context 'build exists' do - let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) } - let!(:build) { create(:ci_build, commit: ci_commit) } - + let!(:build) { create_build(project, sha, branch) } context 'build success' do before { build.success! } @@ -96,6 +94,28 @@ describe Gitlab::Badge::Build do end end + context 'when outdated pipeline for given ref exists' do + before do + build = create_build(project, sha, branch) + build.success! + + old_build = create_build(project, '11eeffdd', branch) + old_build.drop! + end + + it 'does not take outdated pipeline into account' do + expect(badge.to_s).to eq 'build-success' + end + end + + def create_build(project, sha, branch) + ci_commit = create(:ci_commit, project: project, + sha: sha, + ref: branch) + + create(:ci_build, commit: ci_commit) + end + def status_node(data, status) xml = Nokogiri::XML.parse(data) xml.at(%Q{text:contains("#{status}")}) From e052daa08adecf74d6ac85c7a8bf6cb7743b8d93 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 3 Jun 2016 10:34:20 +0200 Subject: [PATCH 129/507] Enable Style/EmptyLinesAroundAccessModifier rubocop cop See #17478 --- .rubocop.yml | 2 +- app/services/oauth2/access_token_validation_service.rb | 1 + lib/gitlab/ldap/config.rb | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 84a8015b41..3593ae29f2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -194,7 +194,7 @@ Style/EmptyLines: # Keep blank lines around access modifiers. Style/EmptyLinesAroundAccessModifier: - Enabled: false + Enabled: true # Keeps track of empty lines around block bodies. Style/EmptyLinesAroundBlockBody: diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb index 6194f6ce91..264fdccde8 100644 --- a/app/services/oauth2/access_token_validation_service.rb +++ b/app/services/oauth2/access_token_validation_service.rb @@ -22,6 +22,7 @@ module Oauth2::AccessTokenValidationService end protected + # True if the token's scope is a superset of required scopes, # or the required scopes is empty. def sufficient_scope?(token, scopes) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index aff7ccb157..f9bb577532 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -93,6 +93,7 @@ module Gitlab end protected + def base_config Gitlab.config.ldap end From 080997d87edcafd7afd3c3aa01da441e87493fdd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 3 Jun 2016 10:59:43 +0200 Subject: [PATCH 130/507] Enable Lint/AmbiguousOperator rubocop cop See #17478 --- .rubocop.yml | 2 +- app/models/project_services/irker_service.rb | 2 +- lib/api/repositories.rb | 6 +++--- lib/gitlab/key_fingerprint.rb | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 84a8015b41..c7a9e69719 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -771,7 +771,7 @@ Metrics/PerceivedComplexity: # Checks for ambiguous operators in the first argument of a method invocation # without parentheses. Lint/AmbiguousOperator: - Enabled: false + Enabled: true # Checks for ambiguous regexp literals in the first argument of a method # invocation without parentheses. diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 2e5e854fc5..58cb720c3c 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -83,7 +83,7 @@ class IrkerService < Service self.channels = recipients.split(/\s+/).map do |recipient| format_channel(recipient) end - channels.reject! &:nil? + channels.reject!(&:nil?) end def format_channel(recipient) diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 62161aadb9..9cb14e95eb 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -57,7 +57,7 @@ module API not_found! "File" unless blob content_type 'text/plain' - header *Gitlab::Workhorse.send_git_blob(repo, blob) + header(*Gitlab::Workhorse.send_git_blob(repo, blob)) end # Get a raw blob contents by blob sha @@ -83,7 +83,7 @@ module API env['api.format'] = :txt content_type blob.mime_type - header *Gitlab::Workhorse.send_git_blob(repo, blob) + header(*Gitlab::Workhorse.send_git_blob(repo, blob)) end # Get a an archive of the repository @@ -98,7 +98,7 @@ module API authorize! :download_code, user_project begin - header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]) + header(*Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format])) rescue not_found!('File') end diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb index baf52ff750..8684b4636e 100644 --- a/lib/gitlab/key_fingerprint.rb +++ b/lib/gitlab/key_fingerprint.rb @@ -17,9 +17,9 @@ module Gitlab file.rewind cmd = [] - cmd.push *%W(ssh-keygen) - cmd.push *%W(-E md5) if explicit_fingerprint_algorithm? - cmd.push *%W(-lf #{file.path}) + cmd.push('ssh-keygen') + cmd.push('-E', 'md5') if explicit_fingerprint_algorithm? + cmd.push('-lf', file.path) cmd_output, cmd_status = popen(cmd, '/tmp') end From 7cc897d74e72367ac782eb7cd469f4c26acc3ef0 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 3 Jun 2016 09:16:45 +0000 Subject: [PATCH 131/507] Let contributors know where to start gitlab-org/gitlab-ce/issues/14905#note_12235996 --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a15f8c4fec..e952855fde 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -308,7 +308,7 @@ tests are least likely to receive timely feedback. The workflow to make a merge request is as follows: 1. Fork the project into your personal space on GitLab.com -1. Create a feature branch +1. Create a feature branch, branch away from `master`. 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Add your changes to the [CHANGELOG](CHANGELOG) 1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide] From b2acebb4efdf8023f685cfd14489c49e56d126a7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 12:00:16 +0200 Subject: [PATCH 132/507] Use ci_commits table --- app/models/ci/pipeline.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 74347cf142..4d4f895136 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -3,6 +3,8 @@ module Ci extend Ci::Model include Statuseable + self.table_name = 'ci_commits' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id has_many :statuses, class_name: 'CommitStatus' has_many :builds, class_name: 'Ci::Build' From a51ccf36922f6b97a0d91756813cffd869dd214d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 3 Jun 2016 11:04:27 +0100 Subject: [PATCH 133/507] Backported from EE shared form for web hooks --- app/views/projects/hooks/index.html.haml | 92 +--------------------- app/views/shared/web_hooks/_form.html.haml | 91 +++++++++++++++++++++ 2 files changed, 92 insertions(+), 91 deletions(-) create mode 100644 app/views/shared/web_hooks/_form.html.haml diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 917a0b805b..8faad35146 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -1,91 +1 @@ -- page_title "Webhooks" -.row.prepend-top-default - .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 - = page_title - %p - #{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be - used for binding events when something is happening within the project. - .col-lg-9.append-bottom-default - %h5.prepend-top-0 - Add new webhook - = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project) do |f| - = form_errors(@hook) - - .form-group - = f.label :url, "URL", class: "label-light" - = f.text_field :url, class: "form-control", placeholder: "http://example.com/trigger-ci.json" - .form-group - = f.label :token, "Secret Token", class: 'label-light' - = f.text_field :token, class: "form-control", placeholder: '' - %p.help-block - Use this token to validate received payloads - .form-group - = f.label :url, "Trigger", class: "label-light" - %div - = f.check_box :push_events, class: "pull-left" - .prepend-left-20 - = f.label :push_events, class: "label-light append-bottom-0" do - Push events - %p.light - This url will be triggered by a push to the repository - %div - = f.check_box :tag_push_events, class: "pull-left" - .prepend-left-20 - = f.label :tag_push_events, class: "label-light append-bottom-0" do - Tag push events - %p.light - This url will be triggered when a new tag is pushed to the repository - %div - = f.check_box :note_events, class: "pull-left" - .prepend-left-20 - = f.label :note_events, class: "label-light append-bottom-0" do - Comments - %p.light - This url will be triggered when someone adds a comment - %div - = f.check_box :issues_events, class: "pull-left" - .prepend-left-20 - = f.label :issues_events, class: "label-light append-bottom-0" do - Issues events - %p.light - This url will be triggered when an issue is created/updated/merged - %div - = f.check_box :merge_requests_events, class: "pull-left" - .prepend-left-20 - = f.label :merge_requests_events, class: "label-light append-bottom-0" do - Merge Request events - %p.light - This url will be triggered when a merge request is created/updated/merged - %div - = f.check_box :build_events, class: "pull-left" - .prepend-left-20 - = f.label :build_events, class: "label-light append-bottom-0" do - Build events - %p.light - This url will be triggered when the build status changes - %div - = f.check_box :wiki_page_events, class: 'pull-left' - .prepend-left-20 - = f.label :wiki_page_events, class: 'label-light append-bottom-0' do - Wiki Page events - %p.light - This url will be triggered when a wiki page is created/updated - .form-group - = f.label :enable_ssl_verification, "SSL verification", class: "label-light" - %div - = f.check_box :enable_ssl_verification, class: "pull-left" - .prepend-left-20 - = f.label :enable_ssl_verification, class: "label-light append-bottom-0" do - Enable SSL verification - = f.submit "Add Webhook", class: "btn btn-create" - %hr - %h5.prepend-top-default - Webhooks (#{@hooks.count}) - - if @hooks.any? - %ul.well-list - - @hooks.each do |hook| - = render "project_hook", hook: hook - - else - %p.settings-message.text-center.append-bottom-0 - No webhooks found, add one in the form above. += render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project] diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml new file mode 100644 index 0000000000..d1e861ca80 --- /dev/null +++ b/app/views/shared/web_hooks/_form.html.haml @@ -0,0 +1,91 @@ +- page_title "Webhooks" +- context_title = @project ? 'project' : 'group' + +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p + #{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be + used for binding events when something is happening within the project. + .col-lg-9.append-bottom-default + = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f| + = form_errors(hook) + + .form-group + = f.label :url, "URL", class: 'label-light' + = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' + .form-group + = f.label :token, "Secret Token", class: 'label-light' + = f.text_field :token, class: "form-control", placeholder: '' + %p.help-block + Use this token to validate received payloads + .form-group + = f.label :url, "Trigger", class: 'label-light' + %ul.list-unstyled + %li + = f.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = f.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + %li + = f.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = f.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + %li + = f.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = f.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment + %li + = f.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = f.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This url will be triggered when an issue is created/updated/merged + %li + = f.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = f.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This url will be triggered when a merge request is created/updated/merged + %li + = f.check_box :build_events, class: 'pull-left' + .prepend-left-20 + = f.label :build_events, class: 'list-label' do + %strong Build events + %p.light + This url will be triggered when the build status changes + %li + = f.check_box :wiki_page_events, class: 'pull-left' + .prepend-left-20 + = f.label :wiki_page_events, class: 'list-label' do + %strong Wiki Page events + %p.light + This url will be triggered when a wiki page is created/updated + .form-group + = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox' + .checkbox + = f.label :enable_ssl_verification do + = f.check_box :enable_ssl_verification + %strong Enable SSL verification + = f.submit "Add Webhook", class: "btn btn-create" + %hr + %h5.prepend-top-default + Webhooks (#{hooks.count}) + - if hooks.any? + %ul.well-list + - hooks.each do |hook| + = render "project_hook", hook: hook + - else + %p.settings-message.text-center.append-bottom-0 + No webhooks found, add one in the form above. From 717fdd6d42f119dd1e0f457c40563cc35985fbae Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 12:29:00 +0200 Subject: [PATCH 134/507] Rename Ci::Build commit to pipeline --- features/steps/project/commits/commits.rb | 2 +- features/steps/project/merge_requests.rb | 2 +- features/steps/shared/builds.rb | 2 +- spec/factories/ci/builds.rb | 2 +- spec/factories/commit_statuses.rb | 2 +- spec/features/admin/admin_builds_spec.rb | 24 +++++------ spec/features/admin/admin_runners_spec.rb | 2 +- spec/features/builds_spec.rb | 2 +- spec/features/commits_spec.rb | 4 +- .../merge_when_build_succeeds_spec.rb | 4 +- spec/features/pipelines_spec.rb | 20 ++++----- .../security/project/public_access_spec.rb | 2 +- spec/lib/ci/charts_spec.rb | 2 +- spec/lib/gitlab/badge/build_spec.rb | 2 +- spec/models/build_spec.rb | 18 ++++---- spec/models/ci/commit_spec.rb | 42 +++++++++---------- spec/models/commit_status_spec.rb | 30 ++++++------- spec/models/generic_commit_status_spec.rb | 2 +- spec/requests/api/builds_spec.rb | 12 +++--- spec/requests/api/commit_statuses_spec.rb | 4 +- spec/requests/ci/api/builds_spec.rb | 12 +++--- .../ci/image_for_build_service_spec.rb | 2 +- .../ci/register_build_service_spec.rb | 2 +- .../add_todo_when_build_fails_service_spec.rb | 4 +- 24 files changed, 100 insertions(+), 100 deletions(-) diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index bf01a78cb3..33d3eeab0d 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -165,7 +165,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'commit has ci status' do @project.enable_ci ci_commit = create :ci_commit, project: @project, sha: sample_commit.id - create :ci_build, commit: ci_commit + create :ci_build, pipeline: ci_commit end step 'repository contains ".gitlab-ci.yml" file' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index b30346790e..0ac7d3a250 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -520,7 +520,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps project = merge_request.source_project project.enable_ci ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch - create :ci_build, commit: ci_commit + create :ci_build, pipeline: ci_commit end step 'I should see merge request "Bug NS-05" with CI status' do diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index cf30e23b6b..92d7bed045 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -23,7 +23,7 @@ module SharedBuilds end step 'project has another build that is running' do - create(:ci_build, commit: @ci_commit, name: 'second build', status: 'running') + create(:ci_build, pipeline: @ci_commit, name: 'second build', status: 'running') end step 'I visit recent build details page' do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index cd49e559b7..e2cb08d2a8 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -16,7 +16,7 @@ FactoryGirl.define do } end - commit factory: :ci_commit + pipeline factory: :ci_commit trait :success do status 'success' diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index b7c2b32cb1..67301fbd9a 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -3,7 +3,7 @@ FactoryGirl.define do name 'default' status 'success' description 'commit status' - commit factory: :ci_commit_with_one_job + pipeline factory: :ci_commit_with_one_job started_at 'Tue, 26 Jan 2016 08:21:42 +0100' finished_at 'Tue, 26 Jan 2016 08:23:42 +0100' diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index 7bbe20fec4..f5aedcb054 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -11,10 +11,10 @@ describe 'Admin Builds' do context 'All tab' do context 'when have builds' do it 'shows all builds' do - create(:ci_build, commit: commit, status: :pending) - create(:ci_build, commit: commit, status: :running) - create(:ci_build, commit: commit, status: :success) - create(:ci_build, commit: commit, status: :failed) + create(:ci_build, pipeline: commit, status: :pending) + create(:ci_build, pipeline: commit, status: :running) + create(:ci_build, pipeline: commit, status: :success) + create(:ci_build, pipeline: commit, status: :failed) visit admin_builds_path @@ -39,9 +39,9 @@ describe 'Admin Builds' do context 'Running tab' do context 'when have running builds' do it 'shows running builds' do - build1 = create(:ci_build, commit: commit, status: :pending) - build2 = create(:ci_build, commit: commit, status: :success) - build3 = create(:ci_build, commit: commit, status: :failed) + build1 = create(:ci_build, pipeline: commit, status: :pending) + build2 = create(:ci_build, pipeline: commit, status: :success) + build3 = create(:ci_build, pipeline: commit, status: :failed) visit admin_builds_path(scope: :running) @@ -55,7 +55,7 @@ describe 'Admin Builds' do context 'when have no builds running' do it 'shows a message' do - create(:ci_build, commit: commit, status: :success) + create(:ci_build, pipeline: commit, status: :success) visit admin_builds_path(scope: :running) @@ -69,9 +69,9 @@ describe 'Admin Builds' do context 'Finished tab' do context 'when have finished builds' do it 'shows finished builds' do - build1 = create(:ci_build, commit: commit, status: :pending) - build2 = create(:ci_build, commit: commit, status: :running) - build3 = create(:ci_build, commit: commit, status: :success) + build1 = create(:ci_build, pipeline: commit, status: :pending) + build2 = create(:ci_build, pipeline: commit, status: :running) + build3 = create(:ci_build, pipeline: commit, status: :success) visit admin_builds_path(scope: :finished) @@ -85,7 +85,7 @@ describe 'Admin Builds' do context 'when have no builds finished' do it 'shows a message' do - create(:ci_build, commit: commit, status: :running) + create(:ci_build, pipeline: commit, status: :running) visit admin_builds_path(scope: :finished) diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 8ebd4a6808..2c87a25913 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -9,7 +9,7 @@ describe "Admin Runners" do before do runner = FactoryGirl.create(:ci_runner) commit = FactoryGirl.create(:ci_commit) - FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) + FactoryGirl.create(:ci_build, pipeline: commit, runner_id: runner.id) visit admin_runners_path end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 7a05d30e8b..c1c21d4b78 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -6,7 +6,7 @@ describe "Builds" do before do login_as(:user) @commit = FactoryGirl.create :ci_commit - @build = FactoryGirl.create :ci_build, commit: @commit + @build = FactoryGirl.create :ci_build, pipeline: @commit @project = @commit.project @project.team << [@user, :developer] end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 20f0b27bcc..d8f5a2f804 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -16,7 +16,7 @@ describe 'Commits' do end context 'commit status is Generic Commit Status' do - let!(:status) { FactoryGirl.create :generic_commit_status, commit: commit } + let!(:status) { FactoryGirl.create :generic_commit_status, pipeline: commit } before do project.team << [@user, :reporter] @@ -39,7 +39,7 @@ describe 'Commits' do end context 'commit status is Ci Build' do - let!(:build) { FactoryGirl.create :ci_build, commit: commit } + let!(:build) { FactoryGirl.create :ci_build, pipeline: commit } let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } context 'when logged as developer' do diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 7aa7eb965e..eaa3e6b147 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -13,7 +13,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do context "Active build for Merge Request" do let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } - let!(:ci_build) { create(:ci_build, commit: ci_commit) } + let!(:ci_build) { create(:ci_build, pipeline: ci_commit) } before do login_as user @@ -48,7 +48,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } - let!(:ci_build) { create(:ci_build, commit: ci_commit) } + let!(:ci_build) { create(:ci_build, pipeline: ci_commit) } before do login_as user diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 093f92ffc6..2026cb7cf1 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -31,7 +31,7 @@ describe "Pipelines" do end context 'cancelable pipeline' do - let!(:running) { create(:ci_build, :running, commit: pipeline, stage: 'test', commands: 'test') } + let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') } before { visit namespace_project_pipelines_path(project.namespace, project) } @@ -47,7 +47,7 @@ describe "Pipelines" do end context 'retryable pipelines' do - let!(:failed) { create(:ci_build, :failed, commit: pipeline, stage: 'test', commands: 'test') } + let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') } before { visit namespace_project_pipelines_path(project.namespace, project) } @@ -64,7 +64,7 @@ describe "Pipelines" do context 'for generic statuses' do context 'when running' do - let!(:running) { create(:generic_commit_status, status: 'running', commit: pipeline, stage: 'test') } + let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } before { visit namespace_project_pipelines_path(project.namespace, project) } @@ -78,7 +78,7 @@ describe "Pipelines" do end context 'when failed' do - let!(:running) { create(:generic_commit_status, status: 'failed', commit: pipeline, stage: 'test') } + let!(:running) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') } before { visit namespace_project_pipelines_path(project.namespace, project) } @@ -94,7 +94,7 @@ describe "Pipelines" do context 'downloadable pipelines' do context 'with artifacts' do - let!(:with_artifacts) { create(:ci_build, :artifacts, :success, commit: pipeline, name: 'rspec tests', stage: 'test') } + let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') } before { visit namespace_project_pipelines_path(project.namespace, project) } @@ -103,7 +103,7 @@ describe "Pipelines" do end context 'without artifacts' do - let!(:without_artifacts) { create(:ci_build, :success, commit: pipeline, name: 'rspec', stage: 'test') } + let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } it { expect(page).not_to have_selector('.build-artifacts') } end @@ -114,10 +114,10 @@ describe "Pipelines" do let(:pipeline) { create(:ci_commit, project: project, ref: 'master') } before do - @success = create(:ci_build, :success, commit: pipeline, stage: 'build', name: 'build') - @failed = create(:ci_build, :failed, commit: pipeline, stage: 'test', name: 'test', commands: 'test') - @running = create(:ci_build, :running, commit: pipeline, stage: 'deploy', name: 'deploy') - @external = create(:generic_commit_status, status: 'success', commit: pipeline, name: 'jenkins', stage: 'external') + @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') + @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') + @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') + @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') end before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 4def4f99bc..4ce367c3c6 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -143,7 +143,7 @@ describe "Public Project Access", feature: true do describe "GET /:project_path/builds/:id" do let(:commit) { create(:ci_commit, project: project) } - let(:build) { create(:ci_build, commit: commit) } + let(:build) { create(:ci_build, pipeline: commit) } subject { namespace_project_build_path(project.namespace, project, build.id) } context "when allowed for public" do diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index 9d1215a576..2be50edc34 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -5,7 +5,7 @@ describe Ci::Charts, lib: true do context "build_times" do before do @commit = FactoryGirl.create(:ci_commit) - FactoryGirl.create(:ci_build, commit: @commit) + FactoryGirl.create(:ci_build, pipeline: @commit) end it 'should return build times in minutes' do diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index b6f7a2e7ec..e87bf41ea2 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -43,7 +43,7 @@ describe Gitlab::Badge::Build do context 'build exists' do let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) } - let!(:build) { create(:ci_build, commit: ci_commit) } + let!(:build) { create(:ci_build, pipeline: ci_commit) } context 'build success' do diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 5c6c30c20e..86d581721b 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -3,15 +3,15 @@ require 'spec_helper' describe Ci::Build, models: true do let(:project) { create(:project) } let(:commit) { create(:ci_commit, project: project) } - let(:build) { create(:ci_build, commit: commit) } + let(:build) { create(:ci_build, pipeline: commit) } it { is_expected.to validate_presence_of :ref } it { is_expected.to respond_to :trace_html } describe '#first_pending' do - let!(:first) { create(:ci_build, commit: commit, status: 'pending', created_at: Date.yesterday) } - let!(:second) { create(:ci_build, commit: commit, status: 'pending') } + let!(:first) { create(:ci_build, pipeline: commit, status: 'pending', created_at: Date.yesterday) } + let!(:second) { create(:ci_build, pipeline: commit, status: 'pending') } subject { Ci::Build.first_pending } it { is_expected.to be_a(Ci::Build) } @@ -219,7 +219,7 @@ describe Ci::Build, models: true do context 'and trigger variables' do let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: commit, trigger: trigger) } let(:trigger_variables) do [ { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } @@ -428,10 +428,10 @@ describe Ci::Build, models: true do end describe '#depends_on_builds' do - let!(:build) { create(:ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build') } - let!(:rspec_test) { create(:ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test') } - let!(:rubocop_test) { create(:ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test') } - let!(:staging) { create(:ci_build, commit: commit, name: 'staging', stage_idx: 2, stage: 'deploy') } + let!(:build) { create(:ci_build, pipeline: commit, name: 'build', stage_idx: 0, stage: 'build') } + let!(:rspec_test) { create(:ci_build, pipeline: commit, name: 'rspec', stage_idx: 1, stage: 'test') } + let!(:rubocop_test) { create(:ci_build, pipeline: commit, name: 'rubocop', stage_idx: 1, stage: 'test') } + let!(:staging) { create(:ci_build, pipeline: commit, name: 'staging', stage_idx: 2, stage: 'deploy') } it 'to have no dependents if this is first build' do expect(build.depends_on_builds).to be_empty @@ -500,7 +500,7 @@ describe Ci::Build, models: true do before do @merge_request = create_mr(build, commit, factory: :merge_request_with_diffs) commit2 = create(:ci_commit, project: project) - @build2 = create(:ci_build, commit: commit2) + @build2 = create(:ci_build, pipeline: commit2) commits = [double(id: commit.sha), double(id: commit2.sha)] allow(@merge_request).to receive(:commits).and_return(commits) diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 2c6e6db682..8426f28d44 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -42,8 +42,8 @@ describe Ci::Pipeline, models: true do subject { commit.retried } before do - @commit1 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy' - @commit2 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy' + @commit1 = FactoryGirl.create :ci_build, pipeline: commit, name: 'deploy' + @commit2 = FactoryGirl.create :ci_build, pipeline: commit, name: 'deploy' end it 'returns old builds' do @@ -264,14 +264,14 @@ describe Ci::Pipeline, models: true do let(:commit) { FactoryGirl.create :ci_commit } it "returns finished_at of latest build" do - build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60 - FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120 + build = FactoryGirl.create :ci_build, pipeline: commit, finished_at: Time.now - 60 + FactoryGirl.create :ci_build, pipeline: commit, finished_at: Time.now - 120 expect(commit.finished_at.to_i).to eq(build.finished_at.to_i) end it "returns nil if there is no finished build" do - FactoryGirl.create :ci_not_started_build, commit: commit + FactoryGirl.create :ci_not_started_build, pipeline: commit expect(commit.finished_at).to be_nil end @@ -282,27 +282,27 @@ describe Ci::Pipeline, models: true do let(:commit) { FactoryGirl.create :ci_commit, project: project } it "calculates average when there are two builds with coverage" do - FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: commit + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: commit expect(commit.coverage).to eq("35.00") end it "calculates average when there are two builds with coverage and one with nil" do - FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit - FactoryGirl.create :ci_build, commit: commit + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: commit + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: commit + FactoryGirl.create :ci_build, pipeline: commit expect(commit.coverage).to eq("35.00") end it "calculates average when there are two builds with coverage and one is retried" do - FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, commit: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: commit + FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: commit + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: commit expect(commit.coverage).to eq("35.00") end it "calculates average when there is one build without coverage" do - FactoryGirl.create :ci_build, commit: commit + FactoryGirl.create :ci_build, pipeline: commit expect(commit.coverage).to be_nil end end @@ -312,7 +312,7 @@ describe Ci::Pipeline, models: true do context 'no failed builds' do before do - FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'success' + FactoryGirl.create :ci_build, name: "rspec", pipeline: commit, status: 'success' end it 'be not retryable' do @@ -322,8 +322,8 @@ describe Ci::Pipeline, models: true do context 'with failed builds' do before do - FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'running' - FactoryGirl.create :ci_build, name: "rubocop", commit: commit, status: 'failed' + FactoryGirl.create :ci_build, name: "rspec", pipeline: commit, status: 'running' + FactoryGirl.create :ci_build, name: "rubocop", pipeline: commit, status: 'failed' end it 'be retryable' do @@ -337,8 +337,8 @@ describe Ci::Pipeline, models: true do subject { CommitStatus.where(commit: [commit, commit2]).stages } before do - FactoryGirl.create :ci_build, commit: commit2, stage: 'test', stage_idx: 1 - FactoryGirl.create :ci_build, commit: commit, stage: 'build', stage_idx: 0 + FactoryGirl.create :ci_build, pipeline: commit2, stage: 'test', stage_idx: 1 + FactoryGirl.create :ci_build, pipeline: commit, stage: 'build', stage_idx: 0 end it 'return all stages' do @@ -353,7 +353,7 @@ describe Ci::Pipeline, models: true do end context 'dependent objects' do - let(:commit_status) { build :commit_status, commit: commit } + let(:commit_status) { build :commit_status, pipeline: commit } it 'execute update_state after saving dependent object' do expect(commit).to receive(:update_state).and_return(true) @@ -363,7 +363,7 @@ describe Ci::Pipeline, models: true do context 'update state' do let(:current) { Time.now.change(usec: 0) } - let(:build) { FactoryGirl.create :ci_build, :success, commit: commit, started_at: current - 120, finished_at: current - 60 } + let(:build) { FactoryGirl.create :ci_build, :success, pipeline: commit, started_at: current - 120, finished_at: current - 60 } before do build diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 434e58cfd0..b435d83572 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe CommitStatus, models: true do let(:commit) { FactoryGirl.create :ci_commit } - let(:commit_status) { FactoryGirl.create :commit_status, commit: commit } + let(:commit_status) { FactoryGirl.create :commit_status, pipeline: commit } it { is_expected.to belong_to(:commit) } it { is_expected.to belong_to(:user) } @@ -121,11 +121,11 @@ describe CommitStatus, models: true do subject { CommitStatus.latest.order(:id) } before do - @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'cc', status: 'success' - @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'bb', status: 'success' - @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'success' + @commit1 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, pipeline: commit, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: 'cc', status: 'success' + @commit4 = FactoryGirl.create :commit_status, pipeline: commit, name: 'cc', ref: 'bb', status: 'success' + @commit5 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: 'bb', status: 'success' end it 'return unique statuses' do @@ -137,11 +137,11 @@ describe CommitStatus, models: true do subject { CommitStatus.running_or_pending.order(:id) } before do - @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' - @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed' - @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled' + @commit1 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, pipeline: commit, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: nil, status: 'success' + @commit4 = FactoryGirl.create :commit_status, pipeline: commit, name: 'dd', ref: nil, status: 'failed' + @commit5 = FactoryGirl.create :commit_status, pipeline: commit, name: 'ee', ref: nil, status: 'canceled' end it 'return statuses that are running or pending' do @@ -172,10 +172,10 @@ describe CommitStatus, models: true do describe '#stages' do before do - FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'success' - FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'failed' - FactoryGirl.create :commit_status, commit: commit, stage: 'deploy', stage_idx: 2, status: 'running' - FactoryGirl.create :commit_status, commit: commit, stage: 'test', stage_idx: 1, status: 'success' + FactoryGirl.create :commit_status, pipeline: commit, stage: 'build', stage_idx: 0, status: 'success' + FactoryGirl.create :commit_status, pipeline: commit, stage: 'build', stage_idx: 0, status: 'failed' + FactoryGirl.create :commit_status, pipeline: commit, stage: 'deploy', stage_idx: 2, status: 'running' + FactoryGirl.create :commit_status, pipeline: commit, stage: 'test', stage_idx: 1, status: 'success' end context 'stages list' do diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index d0e02618b6..d2cd37c9b4 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe GenericCommitStatus, models: true do let(:commit) { FactoryGirl.create :ci_commit } - let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit } + let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: commit } describe :context do subject { generic_commit_status.context } diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 0fbc984c06..35eb23c2a9 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -10,7 +10,7 @@ describe API::API, api: true do let!(:developer) { create(:project_member, :developer, user: user, project: project) } let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } let(:commit) { create(:ci_commit, project: project)} - let(:build) { create(:ci_build, commit: commit) } + let(:build) { create(:ci_build, pipeline: commit) } describe 'GET /projects/:id/builds ' do let(:query) { '' } @@ -102,7 +102,7 @@ describe API::API, api: true do before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } context 'build with artifacts' do - let(:build) { create(:ci_build, :artifacts, commit: commit) } + let(:build) { create(:ci_build, :artifacts, pipeline: commit) } context 'authorized user' do let(:download_headers) do @@ -131,7 +131,7 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id/trace' do - let(:build) { create(:ci_build, :trace, commit: commit) } + let(:build) { create(:ci_build, :trace, pipeline: commit) } before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } @@ -181,7 +181,7 @@ describe API::API, api: true do end describe 'POST /projects/:id/builds/:build_id/retry' do - let(:build) { create(:ci_build, :canceled, commit: commit) } + let(:build) { create(:ci_build, :canceled, pipeline: commit) } before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } @@ -218,7 +218,7 @@ describe API::API, api: true do end context 'build is erasable' do - let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, commit: commit) } + let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: commit) } it 'should erase build content' do expect(response.status).to eq 201 @@ -234,7 +234,7 @@ describe API::API, api: true do end context 'build is not erasable' do - let(:build) { create(:ci_build, :trace, project: project, commit: commit) } + let(:build) { create(:ci_build, :trace, project: project, pipeline: commit) } it 'should respond with forbidden' do expect(response.status).to eq 403 diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 00d426e979..5c5850c558 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -5,7 +5,7 @@ describe API::CommitStatuses, api: true do let!(:project) { create(:project) } let(:commit) { project.repository.commit } - let(:commit_status) { create(:commit_status, commit: ci_commit) } + let(:commit_status) { create(:commit_status, pipeline: ci_commit) } let(:guest) { create_user(:guest) } let(:reporter) { create_user(:reporter) } let(:developer) { create_user(:developer) } @@ -27,7 +27,7 @@ describe API::CommitStatuses, api: true do let(:statuses_id) { json_response.map { |status| status['id'] } } def create_status(commit, opts = {}) - create(:commit_status, { commit: commit, ref: commit.ref }.merge(opts)) + create(:commit_status, { pipeline: commit, ref: commit.ref }.merge(opts)) end let!(:status1) { create_status(master, status: 'running') } diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index e5124ea5ea..10e3631f5f 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -39,7 +39,7 @@ describe Ci::API::API do it "should return 404 error if no builds for specific runner" do commit = FactoryGirl.create(:ci_commit, project: shared_project) - FactoryGirl.create(:ci_build, commit: commit, status: 'pending') + FactoryGirl.create(:ci_build, pipeline: commit, status: 'pending') post ci_api("/builds/register"), token: runner.token @@ -48,7 +48,7 @@ describe Ci::API::API do it "should return 404 error if no builds for shared runner" do commit = FactoryGirl.create(:ci_commit, project: project) - FactoryGirl.create(:ci_build, commit: commit, status: 'pending') + FactoryGirl.create(:ci_build, pipeline: commit, status: 'pending') post ci_api("/builds/register"), token: shared_runner.token @@ -85,7 +85,7 @@ describe Ci::API::API do trigger = FactoryGirl.create(:ci_trigger, project: project) commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) + trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: commit, trigger: trigger) commit.create_builds(nil, trigger_request) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") @@ -132,7 +132,7 @@ describe Ci::API::API do context 'when build has no tags' do before do commit = create(:ci_commit, project: project) - create(:ci_build, commit: commit, tags: []) + create(:ci_build, pipeline: commit, tags: []) end context 'when runner is allowed to pick untagged builds' do @@ -164,7 +164,7 @@ describe Ci::API::API do describe "PUT /builds/:id" do let(:commit) {create(:ci_commit, project: project)} - let(:build) { create(:ci_build, :trace, commit: commit, runner_id: runner.id) } + let(:build) { create(:ci_build, :trace, pipeline: commit, runner_id: runner.id) } before do build.run! @@ -238,7 +238,7 @@ describe Ci::API::API do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } let(:commit) { create(:ci_commit, project: project) } - let(:build) { create(:ci_build, commit: commit, runner_id: runner.id) } + let(:build) { create(:ci_build, pipeline: commit, runner_id: runner.id) } let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 4cc4b3870d..4b6d0889f6 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -6,7 +6,7 @@ module Ci let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } let(:commit) { project.ensure_ci_commit(commit_sha, 'master') } - let(:build) { FactoryGirl.create(:ci_build, commit: commit) } + let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) } describe :execute do before { build } diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index e81f9e757a..6f4d29a198 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -5,7 +5,7 @@ module Ci let!(:service) { RegisterBuildService.new } let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false } let!(:commit) { FactoryGirl.create :ci_commit, project: project } - let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit } + let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: commit } let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index f70716c9d1..394ebebe31 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -23,7 +23,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do describe '#execute' do context 'commit status with ref' do - let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, commit: ci_commit) } + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: ci_commit) } it 'notifies the todo service' do expect(todo_service).to receive(:merge_request_build_failed).with(merge_request) @@ -52,7 +52,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do describe '#close' do context 'commit status with ref' do - let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, commit: ci_commit) } + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: ci_commit) } it 'notifies the todo service' do expect(todo_service).to receive(:merge_request_build_retried).with(merge_request) From 57a3f2845653da1926ea38c061db0b9b08b2902a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 3 Jun 2016 11:48:11 +0100 Subject: [PATCH 135/507] Added minimum password length to password field Closes #17765 --- app/controllers/sessions_controller.rb | 1 + app/views/devise/shared/_signup_box.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d68c2a708e..c1b940bf9e 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -14,6 +14,7 @@ class SessionsController < Devise::SessionsController before_action :load_recaptcha def new + set_minimum_password_length if Gitlab.config.ldap.enabled @ldap_servers = Gitlab::LDAP::Config.servers else diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 510215bb8c..905a8dbcd8 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -16,7 +16,7 @@ %div = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true .form-group.append-bottom-20#password-strength - = f.password_field :password, class: "form-control bottom", placeholder: "Password", required: true + = f.password_field :password, class: "form-control bottom", placeholder: "Password - minimum length #{@minimum_password_length} characters", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters" %div - if current_application_settings.recaptcha_enabled = recaptcha_tags From 3577b57f6b03a0d3c3d32daab6dc6ccf5f6e45f7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 13:09:49 +0200 Subject: [PATCH 136/507] Try to use `pipeline` where applicable --- app/models/ci/build.rb | 12 ++++++------ app/models/commit_status.rb | 2 +- lib/gitlab/build_data_builder.rb | 2 +- spec/factories/ci/builds.rb | 2 +- spec/factories/commit_statuses.rb | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 74441eb97d..77837e9384 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -118,12 +118,12 @@ module Ci .reorder(iid: :asc) merge_requests.find do |merge_request| - merge_request.commits.any? { |ci| ci.id == commit.sha } + merge_request.commits.any? { |ci| ci.id == pipeline.sha } end end def project_id - commit.project.id + pipeline.project_id end def project_name @@ -359,8 +359,8 @@ module Ci end def global_yaml_variables - if commit.config_processor - commit.config_processor.global_variables.map do |key, value| + if pipeline.config_processor + pipeline.config_processor.global_variables.map do |key, value| { key: key, value: value, public: true } end else @@ -369,8 +369,8 @@ module Ci end def job_yaml_variables - if commit.config_processor - commit.config_processor.job_variables(name).map do |key, value| + if pipeline.config_processor + pipeline.config_processor.job_variables(name).map do |key, value| { key: key, value: value, public: true } end else diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 3752bf09f1..98a8b541e9 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -55,7 +55,7 @@ class CommitStatus < ActiveRecord::Base delegate :sha, :short_sha, to: :commit def before_sha - commit.before_sha || Gitlab::Git::BLANK_SHA + pipeline.before_sha || Gitlab::Git::BLANK_SHA end def self.stages diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb index 34e949130d..9f45aefda0 100644 --- a/lib/gitlab/build_data_builder.rb +++ b/lib/gitlab/build_data_builder.rb @@ -3,7 +3,7 @@ module Gitlab class << self def build(build) project = build.project - commit = build.commit + commit = build.pipeline user = build.user data = { diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index e2cb08d2a8..903e331296 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -43,7 +43,7 @@ FactoryGirl.define do end after(:build) do |build, evaluator| - build.project = build.commit.project + build.project = build.pipeline.project end factory :ci_not_started_build do diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 67301fbd9a..65afcc5e50 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -8,7 +8,7 @@ FactoryGirl.define do finished_at 'Tue, 26 Jan 2016 08:23:42 +0100' after(:build) do |build, evaluator| - build.project = build.commit.project + build.project = build.pipeline.project end factory :generic_commit_status, class: GenericCommitStatus do From 08baa9983ee586c10b4fa28f68b10ba8e7807125 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 13:34:03 +0200 Subject: [PATCH 137/507] Use pipelines in context of Project --- app/views/projects/commit/_commit_box.html.haml | 6 +++--- app/views/projects/graphs/ci/_overall.haml | 2 +- app/views/projects/issues/_merge_requests.html.haml | 4 ++-- app/views/projects/issues/_related_branches.html.haml | 6 +++--- app/views/projects/merge_requests/_merge_request.html.haml | 4 ++-- app/views/projects/merge_requests/widget/_show.html.haml | 2 +- app/views/projects/pipelines/_head.html.haml | 2 +- db/fixtures/development/14_builds.rb | 2 +- lib/api/builds.rb | 2 +- lib/api/commit_statuses.rb | 4 ++-- spec/requests/api/builds_spec.rb | 2 +- spec/requests/api/commits_spec.rb | 2 +- spec/services/ci/image_for_build_service_spec.rb | 2 +- 13 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 6674d58417..b117517c0d 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -53,13 +53,13 @@ - if @commit.status .commit-info-row Builds for - = pluralize(@commit.ci_commits.count, 'pipeline') + = pluralize(@commit.pipelines.count, 'pipeline') = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do = ci_icon_for_status(@commit.status) = ci_label_for_status(@commit.status) - - if @commit.ci_commits.duration + - if @commit.pipelines.duration in - = time_interval_in_words @commit.ci_commits.duration + = time_interval_in_words @commit.pipelines.duration .commit-box.content-block %h3.commit-title diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml index 4b12e5f2da..edc4f7b079 100644 --- a/app/views/projects/graphs/ci/_overall.haml +++ b/app/views/projects/graphs/ci/_overall.haml @@ -16,4 +16,4 @@ %li Commits covered: %strong - = @project.ci_commits.count(:all) + = @project.pipelines.count(:all) diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 2f9dc867d0..313c9433de 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -6,8 +6,8 @@ - @merge_requests.each do |merge_request| %li %span.merge-request-ci-status - - if merge_request.ci_commit - = render_pipeline_status(merge_request.ci_commit) + - if merge_request.pipeline + = render_pipeline_status(merge_request.pipeline) - elsif has_any_ci = icon('blank fw') %span.merge-request-id diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index 5f9d291998..b9bb6fe559 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -5,10 +5,10 @@ - @related_branches.each do |branch| %li - sha = @project.repository.find_branch(branch).target - - ci_commit = @project.ci_commit(sha, branch) if sha - - if ci_commit + - pipeline = @project.pipeline(sha, branch) if sha + - if ci_copipelinemmit %span.related-branch-ci-status - = render_pipeline_status(ci_commit) + = render_pipeline_status(pipeline) %span.related-branch-info %strong = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index c02f94490a..37197c7a02 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -11,9 +11,9 @@ = icon('ban') CLOSED - - if merge_request.ci_commit + - if merge_request.pipeline %li - = render_pipeline_status(merge_request.ci_commit) + = render_pipeline_status(merge_request.pipeline) - if merge_request.open? && merge_request.broken? %li diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index b79508bdc3..d9efe81701 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -13,7 +13,7 @@ check_enable: #{@merge_request.unchecked? ? "true" : "false"}, ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", - ci_status: "#{@merge_request.ci_commit ? @merge_request.ci_commit.status : ''}", + ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}", ci_message: { normal: "Build {{status}} for \"{{title}}\"", preparing: "{{status}} build for \"{{title}}\"" diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index 6e757df541..d284694efb 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -4,7 +4,7 @@ = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span Pipelines - %span.badge.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) + %span.badge.count.ci_counter= number_with_delimiter(@project.pipelines.running_or_pending.count) - if project_nav_tab? :builds = nav_link(controller: %w(builds)) do diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb index b99d24a03c..51ff451eb4 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_builds.rb @@ -19,7 +19,7 @@ class Gitlab::Seeder::Builds commits = @project.repository.commits('master', nil, 5) commits_sha = commits.map { |commit| commit.raw.id } commits_sha.map do |sha| - @project.ensure_ci_commit(sha, 'master') + @project.ensure_pipeline(sha, 'master') end rescue [] diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 2b104f90aa..0ff8fa74a8 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -33,7 +33,7 @@ module API get ':id/repository/commits/:sha/builds' do authorize_read_builds! - commit = user_project.ci_commits.find_by_sha(params[:sha]) + commit = user_project.pipelines.find_by_sha(params[:sha]) return not_found! unless commit builds = commit.builds.order('id DESC') diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 0c02b5fd57..7fc8338076 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -22,7 +22,7 @@ module API not_found!('Commit') unless user_project.commit(params[:sha]) - ci_commits = user_project.ci_commits.where(sha: params[:sha]) + ci_commits = user_project.pipelines.where(sha: params[:sha]) statuses = ::CommitStatus.where(commit: ci_commits) statuses = statuses.latest unless parse_boolean(params[:all]) statuses = statuses.where(ref: params[:ref]) if params[:ref].present? @@ -64,7 +64,7 @@ module API ref = branches.first end - ci_commit = @project.ensure_ci_commit(commit.sha, ref) + ci_commit = @project.ensure_pipeline(commit.sha, ref) name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 35eb23c2a9..534b82dc34 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -59,7 +59,7 @@ describe API::API, api: true do describe 'GET /projects/:id/repository/commits/:sha/builds' do before do - project.ensure_ci_commit(commit.sha, 'master') + project.ensure_pipeline(commit.sha, 'master') get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user) end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index cb82ca7802..2336ec97ed 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -90,7 +90,7 @@ describe API::API, api: true do end it "should return status for CI" do - ci_commit = project.ensure_ci_commit(project.repository.commit.sha, 'master') + ci_commit = project.ensure_pipeline(project.repository.commit.sha, 'master') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response.status).to eq(200) expect(json_response['status']).to eq(ci_commit.status) diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 4b6d0889f6..476a888e39 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,7 +5,7 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:commit) { project.ensure_ci_commit(commit_sha, 'master') } + let(:commit) { project.ensure_pipeline(commit_sha, 'master') } let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) } describe :execute do From fe5735a8602b286953477eff5c58ce88b0054c7d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 13:34:38 +0200 Subject: [PATCH 138/507] Delegate to pipeline instead of commit --- app/models/commit_status.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 98a8b541e9..e53c483b90 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -52,7 +52,7 @@ class CommitStatus < ActiveRecord::Base end end - delegate :sha, :short_sha, to: :commit + delegate :sha, :short_sha, to: :pipeline def before_sha pipeline.before_sha || Gitlab::Git::BLANK_SHA From bcd009e661291554cffe404123de629860519602 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 13:58:35 +0200 Subject: [PATCH 139/507] Use foreign_key to define column name --- app/models/ci/pipeline.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 4d4f895136..92fc7c89bd 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -6,8 +6,8 @@ module Ci self.table_name = 'ci_commits' belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - has_many :statuses, class_name: 'CommitStatus' - has_many :builds, class_name: 'Ci::Build' + has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id + has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' validates_presence_of :sha From d501850e05ebadcbf2f957cbf35a0ffa6dbe31ff Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 3 Jun 2016 14:20:34 +0200 Subject: [PATCH 140/507] Add gitlab ci configuration class that holds hash As for now, we keep this class inside a oryginal config processor class. We will move implementation to this class and delegate to it from current config processor. After original gitlab ci yaml processor not longer has relevant impelemntation we will replace it with new configuration class. --- lib/ci/gitlab_ci_yaml_processor.rb | 10 +++------ lib/gitlab/ci/config.rb | 21 ++++++++++++++++++ spec/lib/gitlab/ci/config_spec.rb | 34 ++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 lib/gitlab/ci/config.rb create mode 100644 spec/lib/gitlab/ci/config_spec.rb diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 026a5ac97c..9a60c5ab84 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -12,18 +12,14 @@ module Ci attr_reader :before_script, :after_script, :image, :services, :path, :cache def initialize(config, path = nil) - @config = YAML.safe_load(config, [Symbol], [], true) + @config = Gitlab::Ci::Config.new(config).to_hash @path = path - unless @config.is_a? Hash - raise ValidationError, "YAML should be a hash" - end - - @config = @config.deep_symbolize_keys - initial_parsing validate! + rescue Gitlab::Ci::Config::ParserError => e + raise ValidationError, e.message end def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil) diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb new file mode 100644 index 0000000000..8f88ccf5bf --- /dev/null +++ b/lib/gitlab/ci/config.rb @@ -0,0 +1,21 @@ +module Gitlab + module Ci + class Config + class ParserError < StandardError; end + + def initialize(config) + @config = YAML.safe_load(config, [Symbol], [], true) + + unless @config.is_a?(Hash) + raise ParserError, 'YAML should be a hash' + end + + @config = @config.deep_symbolize_keys + end + + def to_hash + @config + end + end + end +end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb new file mode 100644 index 0000000000..6e25170671 --- /dev/null +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config do + let(:config) do + described_class.new(yml) + end + + context 'when yml config is valid' do + let(:yml) do + <<-EOS + image: ruby:2.2 + + rspec: + script: + - gem install rspec + - rspec + EOS + end + + describe '#to_hash' do + it 'returns hash created from string' do + hash = { + image: 'ruby:2.2', + rspec: { + script: ['gem install rspec', + 'rspec'] + } + } + + expect(config.to_hash).to eq hash + end + end + end +end From 4d5f7aa00827f425dd3d2c3bd5a395c8e948f0ba Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 14:46:17 +0200 Subject: [PATCH 141/507] Fix more places where we should rename ci_commit to pipeline --- app/models/ci/pipeline.rb | 2 +- app/models/ci/trigger_request.rb | 2 +- spec/models/build_spec.rb | 2 +- spec/models/ci/{commit_spec.rb => pipeline_spec.rb} | 2 +- spec/models/commit_status_spec.rb | 10 +++++----- spec/models/merge_request_spec.rb | 12 ++++++------ spec/models/project_spec.rb | 2 +- .../merge_when_build_succeeds_service_spec.rb | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) rename spec/models/ci/{commit_spec.rb => pipeline_spec.rb} (99%) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 92fc7c89bd..9352c6f325 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -8,7 +8,7 @@ module Ci belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id - has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id validates_presence_of :sha validates_presence_of :status diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 47632c4b40..59fc9951d1 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -3,7 +3,7 @@ module Ci extend Ci::Model belongs_to :trigger, class_name: 'Ci::Trigger' - belongs_to :commit, class_name: 'Ci::Pipeline' + belongs_to :commit, class_name: 'Ci::Pipeline', foreign_key: :commit_id has_many :builds, class_name: 'Ci::Build' serialize :variables diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 86d581721b..6c30d85249 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -219,7 +219,7 @@ describe Ci::Build, models: true do context 'and trigger variables' do let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: commit, trigger: trigger) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) } let(:trigger_variables) do [ { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/pipeline_spec.rb similarity index 99% rename from spec/models/ci/commit_spec.rb rename to spec/models/ci/pipeline_spec.rb index 8426f28d44..2f8b1d790a 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -334,7 +334,7 @@ describe Ci::Pipeline, models: true do describe '#stages' do let(:commit2) { FactoryGirl.create :ci_commit, project: project } - subject { CommitStatus.where(commit: [commit, commit2]).stages } + subject { CommitStatus.where(pipeline: [commit, commit2]).stages } before do FactoryGirl.create :ci_build, pipeline: commit2, stage: 'test', stage_idx: 1 diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index b435d83572..ca630e5bc9 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -4,15 +4,15 @@ describe CommitStatus, models: true do let(:commit) { FactoryGirl.create :ci_commit } let(:commit_status) { FactoryGirl.create :commit_status, pipeline: commit } - it { is_expected.to belong_to(:commit) } + it { is_expected.to belong_to(:pipeline) } it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:project) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) } - it { is_expected.to delegate_method(:sha).to(:commit) } - it { is_expected.to delegate_method(:short_sha).to(:commit) } + it { is_expected.to delegate_method(:sha).to(:pipeline) } + it { is_expected.to delegate_method(:short_sha).to(:pipeline) } it { is_expected.to respond_to :success? } it { is_expected.to respond_to :failed? } @@ -179,7 +179,7 @@ describe CommitStatus, models: true do end context 'stages list' do - subject { CommitStatus.where(commit: commit).stages } + subject { CommitStatus.where(pipeline: commit).stages } it 'return ordered list of stages' do is_expected.to eq(%w(build test deploy)) @@ -187,7 +187,7 @@ describe CommitStatus, models: true do end context 'stages with statuses' do - subject { CommitStatus.where(commit: commit).stages_status } + subject { CommitStatus.where(pipeline: commit).stages_status } it 'return list of stages with statuses' do is_expected.to eq({ diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 348f2e2f49..21d7a8d836 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -390,19 +390,19 @@ describe MergeRequest, models: true do subject { create :merge_request, :simple } end - describe '#ci_commit' do + describe '#pipeline' do describe 'when the source project exists' do it 'returns the latest commit' do - commit = double(:commit, id: '123abc') - ci_commit = double(:ci_commit, ref: 'master') + commit = double(:commit, id: '123abc') + pipeline = double(:ci_commit, ref: 'master') allow(subject).to receive(:last_commit).and_return(commit) - expect(subject.source_project).to receive(:ci_commit). + expect(subject.source_project).to receive(:pipeline). with('123abc', 'master'). - and_return(ci_commit) + and_return(pipeline) - expect(subject.pipeline).to eq(ci_commit) + expect(subject.pipeline).to eq(pipeline) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 44debdbdc1..1135d7b609 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -22,7 +22,7 @@ describe Project, models: true do it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:commit_statuses) } - it { is_expected.to have_many(:ci_commits) } + it { is_expected.to have_many(:pipelines) } it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runners) } diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 7f8ea8d7c2..d050f4cd95 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -110,8 +110,8 @@ describe MergeRequests::MergeWhenBuildSucceedsService do context 'properly handles multiple stages' do let(:ref) { mr_merge_if_green_enabled.source_branch } - let(:build) { create(:ci_build, commit: ci_commit, ref: ref, name: 'build', stage: 'build') } - let(:test) { create(:ci_build, commit: ci_commit, ref: ref, name: 'test', stage: 'test') } + let(:build) { create(:ci_build, pipeline: ci_commit, ref: ref, name: 'build', stage: 'build') } + let(:test) { create(:ci_build, pipeline: ci_commit, ref: ref, name: 'test', stage: 'test') } before do # This behavior of MergeRequest: we instantiate a new object From 3ffa494ffe06105d6e36a46df52e8a842be0ab69 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 3 Jun 2016 14:57:34 +0200 Subject: [PATCH 142/507] =?UTF-8?q?Changes=20after=20more=20review=20from?= =?UTF-8?q?=20R=C3=A9my?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../projects/git_http_controller.rb | 16 +++++++----- lib/gitlab/auth.rb | 26 +++++++++---------- spec/lib/gitlab/auth_spec.rb | 8 +++--- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 16a85d6f62..5dfa10d218 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -41,15 +41,17 @@ class Projects::GitHttpController < Projects::ApplicationController return if project && project.public? && upload_pack? authenticate_or_request_with_http_basic do |login, password| - user, type = Gitlab::Auth.find(login, password, project: project, ip: request.ip) + auth_result = Gitlab::Auth.find(login, password, project: project, ip: request.ip) - if (type == :ci) && upload_pack? + if auth_result.type == :ci && upload_pack? @ci = true - elsif (type == :oauth) && !upload_pack? - @user = nil + elsif auth_result.type == :oauth && !upload_pack? + # Not allowed else - @user = user + @user = auth_result.user end + + ci? || user end end @@ -73,7 +75,7 @@ class Projects::GitHttpController < Projects::ApplicationController def project_id_with_suffix id = params[:project_id] || '' - %w{.wiki.git .git}.each do |suffix| + %w[.wiki.git .git].each do |suffix| if id.end_with?(suffix) # Be careful to only remove the suffix from the end of 'id'. # Accidentally removing it from the middle is how security @@ -109,7 +111,7 @@ class Projects::GitHttpController < Projects::ApplicationController if action_name == 'info_refs' params[:service] else - action_name.gsub('_', '-') + action_name.dasherize end end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index d156fa2978..672642ebfb 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,22 +1,23 @@ module Gitlab class Auth + Result = Struct.new(:user, :type) + class << self def find(login, password, project:, ip:) raise "Must provide an IP for rate limiting" if ip.nil? - user = nil - type = nil + result = Result.new if valid_ci_request?(login, password, project) - type = :ci - elsif user = find_in_gitlab_or_ldap(login, password) - type = :master_or_ldap - elsif user = oauth_access_token_check(login, password) - type = :oauth + result.type = :ci + elsif result.user = find_in_gitlab_or_ldap(login, password) + result.type = :gitlab_or_ldap + elsif result.user = oauth_access_token_check(login, password) + result.type = :oauth end - rate_limit!(ip, success: !!user || (type == :ci), login: login) - [user, type] + rate_limit!(ip, success: !!result.user || (result.type == :ci), login: login) + result end def find_in_gitlab_or_ldap(login, password) @@ -67,7 +68,7 @@ module Gitlab # 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. + # false positives. # # Otherwise, we let Rack::Attack know there was a failed authentication # attempt from this IP. This information is stored in the Rails cache @@ -78,15 +79,14 @@ module Gitlab return unless config.enabled if success - # A successful login will reset the auth failure count from this IP Rack::Attack::Allow2Ban.reset(ip, config) else banned = Rack::Attack::Allow2Ban.filter(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?(ip) + # Don't increment the ban counter for this IP false else + # Increment the ban counter for this IP true end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 3c41c4b068..a814ad2a4e 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::Auth, lib: true do ip = 'ip' expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token') - expect(gl_auth.find('gitlab-ci-token', token, project: project, ip: ip)).to eq([nil, :ci]) + expect(gl_auth.find('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci)) end it 'recognizes master passwords' do @@ -19,7 +19,7 @@ describe Gitlab::Auth, lib: true do ip = 'ip' expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) - expect(gl_auth.find(user.username, 'password', project: nil, ip: ip)).to eq([user, :master_or_ldap]) + expect(gl_auth.find(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap)) end it 'recognizes OAuth tokens' do @@ -29,7 +29,7 @@ describe Gitlab::Auth, lib: true do ip = 'ip' expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') - expect(gl_auth.find("oauth2", token.token, project: nil, ip: ip)).to eq([user, :oauth]) + expect(gl_auth.find("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth)) end it 'returns double nil for invalid credentials' do @@ -37,7 +37,7 @@ describe Gitlab::Auth, lib: true do ip = 'ip' expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login) - expect(gl_auth.find(login, 'bar', project: nil, ip: ip)).to eq([nil, nil]) + expect(gl_auth.find(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new) end end From 1564074648afc12fc788a7b5e2eb896dc74f62ef Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 3 Jun 2016 15:28:35 +0200 Subject: [PATCH 143/507] =?UTF-8?q?Refactor=20=5Fallowed=3F=20methods=20as?= =?UTF-8?q?=20R=C3=A9my=20asked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../projects/git_http_controller.rb | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 5dfa10d218..bf7ba7a582 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -128,26 +128,20 @@ class Projects::GitHttpController < Projects::ApplicationController end def upload_pack_allowed? - if !Gitlab.config.gitlab_shell.upload_pack - false - elsif ci? - true - elsif user + return false unless Gitlab.config.gitlab_shell.upload_pack + + if user Gitlab::GitAccess.new(user, project).download_access_check.allowed? else - project.public? + ci? || project.public? end end def receive_pack_allowed? - if !Gitlab.config.gitlab_shell.receive_pack - false - elsif user - # Skip user authorization on upload request. - # It will be done by the pre-receive hook in the repository. - true - else - false - end + return false unless Gitlab.config.gitlab_shell.receive_pack + + # Skip user authorization on upload request. + # It will be done by the pre-receive hook in the repository. + user.present? end end From 9423547f6181bc7e4c9c32e86bd7f72b4c094de0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 15:35:16 +0200 Subject: [PATCH 144/507] Fix other places where we still use commit attribute of Build --- app/controllers/projects/commit_controller.rb | 6 ++-- app/models/ci/pipeline.rb | 2 +- lib/api/commit_statuses.rb | 6 ++-- .../add_todo_when_build_fails_service_spec.rb | 8 ++--- .../merge_when_build_succeeds_service_spec.rb | 32 +++++++++---------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 287388652e..737e6c931f 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -104,7 +104,7 @@ class Projects::CommitController < Projects::ApplicationController end def ci_builds - @ci_builds ||= Ci::Build.where(commit: ci_commits) + @ci_builds ||= Ci::Build.where(pipeline: ci_commits) end def define_show_vars @@ -117,8 +117,8 @@ class Projects::CommitController < Projects::ApplicationController @diff_refs = [commit.parent || commit, commit] @notes_count = commit.notes.count - @statuses = CommitStatus.where(commit: ci_commits) - @builds = Ci::Build.where(commit: ci_commits) + @statuses = CommitStatus.where(pipeline: ci_commits) + @builds = Ci::Build.where(pipeline: ci_commits) end def assign_change_commit_vars(mr_source_branch) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 9352c6f325..9b5b46f492 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -23,7 +23,7 @@ module Ci def self.stages # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries - CommitStatus.where(commit: pluck(:id)).stages + CommitStatus.where(pipeline: pluck(:id)).stages end def project_id diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 7fc8338076..088d5bac58 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -23,7 +23,7 @@ module API not_found!('Commit') unless user_project.commit(params[:sha]) ci_commits = user_project.pipelines.where(sha: params[:sha]) - statuses = ::CommitStatus.where(commit: ci_commits) + statuses = ::CommitStatus.where(pipeline: ci_commits) statuses = statuses.latest unless parse_boolean(params[:all]) statuses = statuses.where(ref: params[:ref]) if params[:ref].present? statuses = statuses.where(stage: params[:stage]) if params[:stage].present? @@ -67,8 +67,8 @@ module API ci_commit = @project.ensure_pipeline(commit.sha, ref) name = params[:name] || params[:context] - status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) - status ||= GenericCommitStatus.new(project: @project, commit: ci_commit, user: current_user) + status = GenericCommitStatus.running_or_pending.find_by(pipeline: ci_commit, name: name, ref: params[:ref]) + status ||= GenericCommitStatus.new(project: @project, pipeline: ci_commit, user: current_user) status.update(attrs) case params[:state].to_s diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index 394ebebe31..a7f52a2fa2 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -6,7 +6,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do let(:merge_request) { create(:merge_request) } let(:project) { create(:project) } let(:sha) { '1234567890abcdef1234567890abcdef12345678' } - let(:ci_commit) { create(:ci_commit_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) } + let(:pipeline) { create(:ci_commit_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) } let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') } let(:todo_service) { TodoService.new } @@ -17,13 +17,13 @@ describe MergeRequests::AddTodoWhenBuildFailsService do end before do - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) allow(service).to receive(:todo_service).and_return(todo_service) end describe '#execute' do context 'commit status with ref' do - let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: ci_commit) } + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) } it 'notifies the todo service' do expect(todo_service).to receive(:merge_request_build_failed).with(merge_request) @@ -52,7 +52,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do describe '#close' do context 'commit status with ref' do - let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: ci_commit) } + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) } it 'notifies the todo service' do expect(todo_service).to receive(:merge_request_build_retried).with(merge_request) diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index d050f4cd95..6d6539a0e7 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -10,7 +10,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do source_project: project, target_project: project, state: "opened") end - let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) } + let(:pipeline) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) } let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') } describe "#execute" do @@ -21,7 +21,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do context 'first time enabling' do before do - allow(merge_request).to receive(:ci_commit).and_return(ci_commit) + allow(merge_request).to receive(:pipeline).and_return(pipeline) service.execute(merge_request) end @@ -43,9 +43,9 @@ describe MergeRequests::MergeWhenBuildSucceedsService do let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) } before do - allow(mr_merge_if_green_enabled).to receive(:ci_commit).and_return(ci_commit) + allow(mr_merge_if_green_enabled).to receive(:pipeline).and_return(pipeline) allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true) - allow(ci_commit).to receive(:success?).and_return(true) + allow(pipeline).to receive(:success?).and_return(true) end it 'updates the merge params' do @@ -62,8 +62,8 @@ describe MergeRequests::MergeWhenBuildSucceedsService do let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } it "merges all merge requests with merge when build succeeds enabled" do - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:success?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow(pipeline).to receive(:success?).and_return(true) expect(MergeWorker).to receive(:perform_async) service.trigger(build) @@ -75,8 +75,8 @@ describe MergeRequests::MergeWhenBuildSucceedsService do let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } it "merges all merge requests with merge when build succeeds enabled" do - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:success?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow(pipeline).to receive(:success?).and_return(true) allow(old_build).to receive(:sha).and_return('1234abcdef') expect(MergeWorker).not_to receive(:perform_async) @@ -99,9 +99,9 @@ describe MergeRequests::MergeWhenBuildSucceedsService do it 'discovers branches and merges all merge requests when status is success' do allow(project.repository).to receive(:branch_names_contains). with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch]) - allow(ci_commit).to receive(:success?).and_return(true) - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:success?).and_return(true) + allow(pipeline).to receive(:success?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow(pipeline).to receive(:success?).and_return(true) expect(MergeWorker).to receive(:perform_async) service.trigger(commit_status) @@ -110,17 +110,17 @@ describe MergeRequests::MergeWhenBuildSucceedsService do context 'properly handles multiple stages' do let(:ref) { mr_merge_if_green_enabled.source_branch } - let(:build) { create(:ci_build, pipeline: ci_commit, ref: ref, name: 'build', stage: 'build') } - let(:test) { create(:ci_build, pipeline: ci_commit, ref: ref, name: 'test', stage: 'test') } + let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') } + let(:test) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') } before do # This behavior of MergeRequest: we instantiate a new object - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_wrap_original do - Ci::Pipeline.find(ci_commit.id) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do + Ci::Pipeline.find(pipeline.id) end # We create test after the build - allow(ci_commit).to receive(:create_next_builds).and_wrap_original do + allow(pipeline).to receive(:create_next_builds).and_wrap_original do test end end From 50a357d7e8fcc7c7ec25eff01495c39f7f90ffd8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 3 Jun 2016 15:49:52 +0200 Subject: [PATCH 145/507] Use #present? --- app/controllers/projects/git_http_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index bf7ba7a582..e53158c412 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -124,7 +124,7 @@ class Projects::GitHttpController < Projects::ApplicationController end def ci? - !!@ci + @ci.present? end def upload_pack_allowed? From bf2bd2373d9179616436990b483aec52fcfe0f8e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 15:58:37 +0200 Subject: [PATCH 146/507] Fix merge_request handling --- app/views/projects/issues/_merge_requests.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 313c9433de..75f36579b1 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -2,7 +2,7 @@ %h2.merge-requests-title = pluralize(@merge_requests.count, 'Related Merge Request') %ul.unstyled-list - - has_any_ci = @merge_requests.any?(&:ci_commit) + - has_any_ci = @merge_requests.any?(&:pipeline) - @merge_requests.each do |merge_request| %li %span.merge-request-ci-status From 46d5760c76bc3ba8222698d12cab5bc4d01c822b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 3 Jun 2016 16:04:59 +0200 Subject: [PATCH 147/507] Fewer silly instance variables --- .../projects/git_http_controller.rb | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index e53158c412..380139a9c3 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -88,17 +88,6 @@ class Projects::GitHttpController < Projects::ApplicationController [nil, nil] end - def repository - @repository ||= begin - _, suffix = project_id_with_suffix - if suffix == '.wiki.git' - project.wiki.repository - else - project.repository - end - end - end - def upload_pack? git_command == 'git-upload-pack' end @@ -119,6 +108,15 @@ class Projects::GitHttpController < Projects::ApplicationController render json: Gitlab::Workhorse.git_http_ok(repository, user) end + def repository + _, suffix = project_id_with_suffix + if suffix == '.wiki.git' + project.wiki.repository + else + project.repository + end + end + def render_not_found render text: 'Not Found', status: :not_found end From 20c7144ed20bad499b878425d5fbab408ad066b5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 16:22:26 +0200 Subject: [PATCH 148/507] Rename all `[ci_]commit` to `[ci_]pipeline` in specs and features --- features/steps/project/commits/commits.rb | 4 +- features/steps/project/merge_requests.rb | 4 +- features/steps/shared/builds.rb | 6 +- features/steps/shared/project.rb | 2 +- .../merge_requests_controller_spec.rb | 2 +- spec/factories/ci/builds.rb | 2 +- spec/factories/ci/commits.rb | 10 +- spec/factories/commit_statuses.rb | 2 +- spec/features/admin/admin_builds_spec.rb | 26 +- spec/features/admin/admin_runners_spec.rb | 4 +- spec/features/builds_spec.rb | 2 +- spec/features/commits_spec.rb | 56 ++-- .../merge_requests/created_from_fork_spec.rb | 2 +- .../merge_when_build_succeeds_spec.rb | 6 +- spec/features/pipelines_spec.rb | 6 +- spec/features/projects/commit/builds_spec.rb | 2 +- .../security/project/public_access_spec.rb | 4 +- spec/lib/ci/charts_spec.rb | 10 +- spec/lib/gitlab/badge/build_spec.rb | 5 +- spec/models/build_spec.rb | 54 ++-- spec/models/ci/pipeline_spec.rb | 262 +++++++++--------- spec/models/commit_status_spec.rb | 44 +-- spec/models/generic_commit_status_spec.rb | 4 +- spec/models/merge_request_spec.rb | 2 +- spec/models/project_spec.rb | 16 +- spec/requests/api/builds_spec.rb | 18 +- spec/requests/api/merge_requests_spec.rb | 4 +- spec/requests/api/triggers_spec.rb | 12 +- spec/requests/ci/api/builds_spec.rb | 48 ++-- spec/requests/ci/api/triggers_spec.rb | 12 +- .../services/ci/create_builds_service_spec.rb | 4 +- .../ci/create_trigger_request_service_spec.rb | 6 +- .../ci/register_build_service_spec.rb | 4 +- .../create_commit_builds_service_spec.rb | 78 +++--- .../add_todo_when_build_fails_service_spec.rb | 2 +- .../merge_when_build_succeeds_service_spec.rb | 2 +- spec/services/system_note_service_spec.rb | 3 +- spec/support/stub_gitlab_calls.rb | 6 +- spec/workers/post_receive_spec.rb | 4 +- 39 files changed, 369 insertions(+), 371 deletions(-) diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 33d3eeab0d..239036e431 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -164,8 +164,8 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'commit has ci status' do @project.enable_ci - ci_commit = create :ci_commit, project: @project, sha: sample_commit.id - create :ci_build, pipeline: ci_commit + pipeline = create :ci_pipeline, project: @project, sha: sample_commit.id + create :ci_build, pipeline: pipeline end step 'repository contains ".gitlab-ci.yml" file' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 0ac7d3a250..37aa169881 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -519,8 +519,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step '"Bug NS-05" has CI status' do project = merge_request.source_project project.enable_ci - ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch - create :ci_build, pipeline: ci_commit + pipeline = create :ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch + create :ci_build, pipeline: pipeline end step 'I should see merge request "Bug NS-05" with CI status' do diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index 92d7bed045..4d6b258f57 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -10,8 +10,8 @@ module SharedBuilds end step 'project has a recent build' do - @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha, ref: 'master') - @build = create(:ci_build_with_coverage, commit: @ci_commit) + @pipeline = create(:ci_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') + @build = create(:ci_build_with_coverage, pipeline: @pipeline) end step 'recent build is successful' do @@ -23,7 +23,7 @@ module SharedBuilds end step 'project has another build that is running' do - create(:ci_build, pipeline: @ci_commit, name: 'second build', status: 'running') + create(:ci_build, pipeline: @pipeline, name: 'second build', status: 'running') end step 'I visit recent build details page' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index ce9ea7ee18..b3411c0311 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -230,7 +230,7 @@ module SharedProject step 'project "Shop" has CI build' do project = Project.find_by(name: "Shop") - create :ci_commit, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped' + create :ci_pipeline, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped' end step 'I should see last commit with CI status' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 8499bf07e9..2a9ee881e9 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -250,7 +250,7 @@ describe Projects::MergeRequestsController do end before do - create(:ci_empty_commit, project: project, sha: merge_request.source_sha, ref: merge_request.source_branch) + create(:ci_empty_pipeline, project: project, sha: merge_request.source_sha, ref: merge_request.source_branch) end it 'returns :merge_when_build_succeeds' do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 903e331296..fe05a0cfc0 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -16,7 +16,7 @@ FactoryGirl.define do } end - pipeline factory: :ci_commit + pipeline factory: :ci_pipeline trait :success do status 'success' diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index d1082f0132..a039bef6f3 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -17,30 +17,30 @@ # FactoryGirl.define do - factory :ci_empty_commit, class: Ci::Pipeline do + factory :ci_empty_pipeline, class: Ci::Pipeline do sha '97de212e80737a608d939f648d959671fb0a0142' project factory: :empty_project - factory :ci_commit_without_jobs do + factory :ci_pipeline_without_jobs do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) } end end - factory :ci_commit_with_one_job do + factory :ci_pipeline_with_one_job do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) } end end - factory :ci_commit_with_two_jobs do + factory :ci_pipeline_with_two_job do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) } end end - factory :ci_commit do + factory :ci_pipeline do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } end diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 65afcc5e50..1e5c479616 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -3,7 +3,7 @@ FactoryGirl.define do name 'default' status 'success' description 'commit status' - pipeline factory: :ci_commit_with_one_job + pipeline factory: :ci_pipeline_with_one_job started_at 'Tue, 26 Jan 2016 08:21:42 +0100' finished_at 'Tue, 26 Jan 2016 08:23:42 +0100' diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index f5aedcb054..a6198389f0 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -6,15 +6,15 @@ describe 'Admin Builds' do end describe 'GET /admin/builds' do - let(:commit) { create(:ci_commit) } + let(:pipeline) { create(:ci_pipeline) } context 'All tab' do context 'when have builds' do it 'shows all builds' do - create(:ci_build, pipeline: commit, status: :pending) - create(:ci_build, pipeline: commit, status: :running) - create(:ci_build, pipeline: commit, status: :success) - create(:ci_build, pipeline: commit, status: :failed) + create(:ci_build, pipeline: pipeline, status: :pending) + create(:ci_build, pipeline: pipeline, status: :running) + create(:ci_build, pipeline: pipeline, status: :success) + create(:ci_build, pipeline: pipeline, status: :failed) visit admin_builds_path @@ -39,9 +39,9 @@ describe 'Admin Builds' do context 'Running tab' do context 'when have running builds' do it 'shows running builds' do - build1 = create(:ci_build, pipeline: commit, status: :pending) - build2 = create(:ci_build, pipeline: commit, status: :success) - build3 = create(:ci_build, pipeline: commit, status: :failed) + build1 = create(:ci_build, pipeline: pipeline, status: :pending) + build2 = create(:ci_build, pipeline: pipeline, status: :success) + build3 = create(:ci_build, pipeline: pipeline, status: :failed) visit admin_builds_path(scope: :running) @@ -55,7 +55,7 @@ describe 'Admin Builds' do context 'when have no builds running' do it 'shows a message' do - create(:ci_build, pipeline: commit, status: :success) + create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :running) @@ -69,9 +69,9 @@ describe 'Admin Builds' do context 'Finished tab' do context 'when have finished builds' do it 'shows finished builds' do - build1 = create(:ci_build, pipeline: commit, status: :pending) - build2 = create(:ci_build, pipeline: commit, status: :running) - build3 = create(:ci_build, pipeline: commit, status: :success) + build1 = create(:ci_build, pipeline: pipeline, status: :pending) + build2 = create(:ci_build, pipeline: pipeline, status: :running) + build3 = create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :finished) @@ -85,7 +85,7 @@ describe 'Admin Builds' do context 'when have no builds finished' do it 'shows a message' do - create(:ci_build, pipeline: commit, status: :running) + create(:ci_build, pipeline: pipeline, status: :running) visit admin_builds_path(scope: :finished) diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 2c87a25913..9499cd4e02 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -8,8 +8,8 @@ describe "Admin Runners" do describe "Runners page" do before do runner = FactoryGirl.create(:ci_runner) - commit = FactoryGirl.create(:ci_commit) - FactoryGirl.create(:ci_build, pipeline: commit, runner_id: runner.id) + pipeline = FactoryGirl.create(:ci_pipeline) + FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id) visit admin_runners_path end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index c1c21d4b78..19bc067881 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -5,7 +5,7 @@ describe "Builds" do before do login_as(:user) - @commit = FactoryGirl.create :ci_commit + @commit = FactoryGirl.create :ci_pipeline @build = FactoryGirl.create :ci_build, pipeline: @commit @project = @commit.project @project.team << [@user, :developer] diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index d8f5a2f804..45e1a157a1 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -8,15 +8,15 @@ describe 'Commits' do describe 'CI' do before do login_as :user - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end - let!(:commit) do - FactoryGirl.create :ci_commit, project: project, sha: project.commit.sha + let!(:pipeline) do + FactoryGirl.create :ci_pipeline, project: project, sha: project.commit.sha end context 'commit status is Generic Commit Status' do - let!(:status) { FactoryGirl.create :generic_commit_status, pipeline: commit } + let!(:status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } before do project.team << [@user, :reporter] @@ -24,10 +24,10 @@ describe 'Commits' do describe 'Commit builds' do before do - visit ci_status_path(commit) + visit ci_status_path(pipeline) end - it { expect(page).to have_content commit.sha[0..7] } + it { expect(page).to have_content pipeline.sha[0..7] } it 'contains generic commit status build' do page.within('.table-holder') do @@ -39,7 +39,7 @@ describe 'Commits' do end context 'commit status is Ci Build' do - let!(:build) { FactoryGirl.create :ci_build, pipeline: commit } + let!(:build) { FactoryGirl.create :ci_build, pipeline: pipeline } let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } context 'when logged as developer' do @@ -53,7 +53,7 @@ describe 'Commits' do end it 'should show build status' do - page.within("//li[@id='commit-#{commit.short_sha}']") do + page.within("//li[@id='commit-#{pipeline.short_sha}']") do expect(page).to have_css(".ci-status-link") end end @@ -61,12 +61,12 @@ describe 'Commits' do describe 'Commit builds' do before do - visit ci_status_path(commit) + visit ci_status_path(pipeline) end - it { expect(page).to have_content commit.sha[0..7] } - it { expect(page).to have_content commit.git_commit_message } - it { expect(page).to have_content commit.git_author_name } + it { expect(page).to have_content pipeline.sha[0..7] } + it { expect(page).to have_content pipeline.git_commit_message } + it { expect(page).to have_content pipeline.git_author_name } end context 'Download artifacts' do @@ -75,7 +75,7 @@ describe 'Commits' do end it do - visit ci_status_path(commit) + visit ci_status_path(pipeline) click_on 'Download artifacts' expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) end @@ -83,7 +83,7 @@ describe 'Commits' do describe 'Cancel all builds' do it 'cancels commit' do - visit ci_status_path(commit) + visit ci_status_path(pipeline) click_on 'Cancel running' expect(page).to have_content 'canceled' end @@ -91,7 +91,7 @@ describe 'Commits' do describe 'Cancel build' do it 'cancels build' do - visit ci_status_path(commit) + visit ci_status_path(pipeline) click_on 'Cancel' expect(page).to have_content 'canceled' end @@ -100,13 +100,13 @@ describe 'Commits' do describe '.gitlab-ci.yml not found warning' do context 'ci builds enabled' do it "does not show warning" do - visit ci_status_path(commit) + visit ci_status_path(pipeline) expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' end it 'shows warning' do - stub_ci_commit_yaml_file(nil) - visit ci_status_path(commit) + stub_ci_pipeline_yaml_file(nil) + visit ci_status_path(pipeline) expect(page).to have_content '.gitlab-ci.yml not found in this commit' end end @@ -114,8 +114,8 @@ describe 'Commits' do context 'ci builds disabled' do before do stub_ci_builds_disabled - stub_ci_commit_yaml_file(nil) - visit ci_status_path(commit) + stub_ci_pipeline_yaml_file(nil) + visit ci_status_path(pipeline) end it 'does not show warning' do @@ -129,13 +129,13 @@ describe 'Commits' do before do project.team << [@user, :reporter] build.update_attributes(artifacts_file: artifacts_file) - visit ci_status_path(commit) + visit ci_status_path(pipeline) end it do - expect(page).to have_content commit.sha[0..7] - expect(page).to have_content commit.git_commit_message - expect(page).to have_content commit.git_author_name + expect(page).to have_content pipeline.sha[0..7] + expect(page).to have_content pipeline.git_commit_message + expect(page).to have_content pipeline.git_author_name expect(page).to have_link('Download artifacts') expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Retry failed') @@ -148,13 +148,13 @@ describe 'Commits' do visibility_level: Gitlab::VisibilityLevel::INTERNAL, public_builds: false) build.update_attributes(artifacts_file: artifacts_file) - visit ci_status_path(commit) + visit ci_status_path(pipeline) end it do - expect(page).to have_content commit.sha[0..7] - expect(page).to have_content commit.git_commit_message - expect(page).to have_content commit.git_author_name + expect(page).to have_content pipeline.sha[0..7] + expect(page).to have_content pipeline.git_commit_message + expect(page).to have_content pipeline.git_author_name expect(page).not_to have_link('Download artifacts') expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Retry failed') diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index edc0bdec3d..12d7e52629 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -29,7 +29,7 @@ feature 'Merge request created from fork' do include WaitForAjax given(:pipeline) do - create(:ci_commit_with_two_jobs, project: fork_project, + create(:ci_pipeline_with_two_job, project: fork_project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) end diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index eaa3e6b147..843bd5aced 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -12,7 +12,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end context "Active build for Merge Request" do - let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } let!(:ci_build) { create(:ci_build, pipeline: ci_commit) } before do @@ -47,8 +47,8 @@ feature 'Merge When Build Succeeds', feature: true, js: true do merge_user: user, title: "MepMep", merge_when_build_succeeds: true) end - let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } - let!(:ci_build) { create(:ci_build, pipeline: ci_commit) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:ci_build) { create(:ci_build, pipeline: pipeline) } before do login_as user diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 2026cb7cf1..98703ef3ac 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -12,7 +12,7 @@ describe "Pipelines" do end describe 'GET /:project/pipelines' do - let!(:pipeline) { create(:ci_commit, project: project, ref: 'master', status: 'running') } + let!(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', status: 'running') } [:all, :running, :branches].each do |scope| context "displaying #{scope}" do @@ -111,7 +111,7 @@ describe "Pipelines" do end describe 'GET /:project/pipelines/:id' do - let(:pipeline) { create(:ci_commit, project: project, ref: 'master') } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } before do @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') @@ -165,7 +165,7 @@ describe "Pipelines" do before { fill_in('Create for', with: 'master') } context 'with gitlab-ci.yml' do - before { stub_ci_commit_to_return_yaml_file } + before { stub_ci_pipeline_to_return_yaml_file } it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) } end diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index 40ba0bdc11..73dd568929 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -11,7 +11,7 @@ feature 'project commit builds' do context 'when no builds triggered yet' do background do - create(:ci_commit, project: project, + create(:ci_pipeline, project: project, sha: project.commit.sha, ref: 'master') end diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 4ce367c3c6..c5f741709a 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -142,8 +142,8 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/builds/:id" do - let(:commit) { create(:ci_commit, project: project) } - let(:build) { create(:ci_build, pipeline: commit) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } subject { namespace_project_build_path(project.namespace, project, build.id) } context "when allowed for public" do diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index 2be50edc34..9c6b4ea508 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -4,19 +4,19 @@ describe Ci::Charts, lib: true do context "build_times" do before do - @commit = FactoryGirl.create(:ci_commit) - FactoryGirl.create(:ci_build, pipeline: @commit) + @pipeline = FactoryGirl.create(:ci_pipeline) + FactoryGirl.create(:ci_build, pipeline: @pipeline) end it 'should return build times in minutes' do - chart = Ci::Charts::BuildTime.new(@commit.project) + chart = Ci::Charts::BuildTime.new(@pipeline.project) expect(chart.build_times).to eq([2]) end it 'should handle nil build times' do - create(:ci_commit, duration: nil, project: @commit.project) + create(:ci_pipeline, duration: nil, project: @pipeline.project) - chart = Ci::Charts::BuildTime.new(@commit.project) + chart = Ci::Charts::BuildTime.new(@pipeline.project) expect(chart.build_times).to eq([2, 0]) end end diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index e87bf41ea2..530c43bfc0 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -42,9 +42,8 @@ describe Gitlab::Badge::Build do end context 'build exists' do - let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) } - let!(:build) { create(:ci_build, pipeline: ci_commit) } - + let(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: branch) } + let!(:build) { create(:ci_build, pipeline: pipeline) } context 'build success' do before { build.success! } diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 6c30d85249..2beb6cc598 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -2,16 +2,16 @@ require 'spec_helper' describe Ci::Build, models: true do let(:project) { create(:project) } - let(:commit) { create(:ci_commit, project: project) } - let(:build) { create(:ci_build, pipeline: commit) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } it { is_expected.to validate_presence_of :ref } it { is_expected.to respond_to :trace_html } describe '#first_pending' do - let!(:first) { create(:ci_build, pipeline: commit, status: 'pending', created_at: Date.yesterday) } - let!(:second) { create(:ci_build, pipeline: commit, status: 'pending') } + let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } + let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } subject { Ci::Build.first_pending } it { is_expected.to be_a(Ci::Build) } @@ -97,7 +97,7 @@ describe Ci::Build, models: true do # describe :timeout do # subject { build.timeout } # - # it { is_expected.to eq(commit.project.timeout) } + # it { is_expected.to eq(pipeline.project.timeout) } # end describe '#options' do @@ -124,13 +124,13 @@ describe Ci::Build, models: true do describe '#project' do subject { build.project } - it { is_expected.to eq(commit.project) } + it { is_expected.to eq(pipeline.project) } end describe '#project_id' do subject { build.project_id } - it { is_expected.to eq(commit.project_id) } + it { is_expected.to eq(pipeline.project_id) } end describe '#project_name' do @@ -219,7 +219,7 @@ describe Ci::Build, models: true do context 'and trigger variables' do let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } let(:trigger_variables) do [ { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } @@ -428,10 +428,10 @@ describe Ci::Build, models: true do end describe '#depends_on_builds' do - let!(:build) { create(:ci_build, pipeline: commit, name: 'build', stage_idx: 0, stage: 'build') } - let!(:rspec_test) { create(:ci_build, pipeline: commit, name: 'rspec', stage_idx: 1, stage: 'test') } - let!(:rubocop_test) { create(:ci_build, pipeline: commit, name: 'rubocop', stage_idx: 1, stage: 'test') } - let!(:staging) { create(:ci_build, pipeline: commit, name: 'staging', stage_idx: 2, stage: 'deploy') } + let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } + let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } + let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') } + let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') } it 'to have no dependents if this is first build' do expect(build.depends_on_builds).to be_empty @@ -451,19 +451,19 @@ describe Ci::Build, models: true do end end - def create_mr(build, commit, factory: :merge_request, created_at: Time.now) - create(factory, source_project_id: commit.gl_project_id, - target_project_id: commit.gl_project_id, + def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) + create(factory, source_project_id: pipeline.gl_project_id, + target_project_id: pipeline.gl_project_id, source_branch: build.ref, created_at: created_at) end describe '#merge_request' do - context 'when a MR has a reference to the commit' do + context 'when a MR has a reference to the pipeline' do before do - @merge_request = create_mr(build, commit, factory: :merge_request) + @merge_request = create_mr(build, pipeline, factory: :merge_request) - commits = [double(id: commit.sha)] + commits = [double(id: pipeline.sha)] allow(@merge_request).to receive(:commits).and_return(commits) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) end @@ -473,19 +473,19 @@ describe Ci::Build, models: true do end end - context 'when there is not a MR referencing the commit' do + context 'when there is not a MR referencing the pipeline' do it 'returns nil' do expect(build.merge_request).to be_nil end end - context 'when more than one MR have a reference to the commit' do + context 'when more than one MR have a reference to the pipeline' do before do - @merge_request = create_mr(build, commit, factory: :merge_request) + @merge_request = create_mr(build, pipeline, factory: :merge_request) @merge_request.close! - @merge_request2 = create_mr(build, commit, factory: :merge_request) + @merge_request2 = create_mr(build, pipeline, factory: :merge_request) - commits = [double(id: commit.sha)] + commits = [double(id: pipeline.sha)] allow(@merge_request).to receive(:commits).and_return(commits) allow(@merge_request2).to receive(:commits).and_return(commits) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2]) @@ -498,11 +498,11 @@ describe Ci::Build, models: true do context 'when a Build is created after the MR' do before do - @merge_request = create_mr(build, commit, factory: :merge_request_with_diffs) - commit2 = create(:ci_commit, project: project) - @build2 = create(:ci_build, pipeline: commit2) + @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs) + pipeline2 = create(:ci_pipeline, project: project) + @build2 = create(:ci_build, pipeline: pipeline2) - commits = [double(id: commit.sha), double(id: commit2.sha)] + commits = [double(id: pipeline.sha), double(id: pipeline2.sha)] allow(@merge_request).to receive(:commits).and_return(commits) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 2f8b1d790a..0d769ed732 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Ci::Pipeline, models: true do let(:project) { FactoryGirl.create :empty_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:statuses) } @@ -18,62 +18,62 @@ describe Ci::Pipeline, models: true do describe :valid_commit_sha do context 'commit.sha can not start with 00000000' do before do - commit.sha = '0' * 40 - commit.valid_commit_sha + pipeline.sha = '0' * 40 + pipeline.valid_commit_sha end - it('commit errors should not be empty') { expect(commit.errors).not_to be_empty } + it('commit errors should not be empty') { expect(pipeline.errors).not_to be_empty } end end describe :short_sha do - subject { commit.short_sha } + subject { pipeline.short_sha } it 'has 8 items' do expect(subject.size).to eq(8) end - it { expect(commit.sha).to start_with(subject) } + it { expect(pipeline.sha).to start_with(subject) } end describe :create_next_builds do end describe :retried do - subject { commit.retried } + subject { pipeline.retried } before do - @commit1 = FactoryGirl.create :ci_build, pipeline: commit, name: 'deploy' - @commit2 = FactoryGirl.create :ci_build, pipeline: commit, name: 'deploy' + @build1 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy' + @build2 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy' end it 'returns old builds' do - is_expected.to contain_exactly(@commit1) + is_expected.to contain_exactly(@build1) end end describe :create_builds do - let!(:commit) { FactoryGirl.create :ci_commit, project: project, ref: 'master', tag: false } + let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false } def create_builds(trigger_request = nil) - commit.create_builds(nil, trigger_request) + pipeline.create_builds(nil, trigger_request) end def create_next_builds - commit.create_next_builds(commit.builds.order(:id).last) + pipeline.create_next_builds(pipeline.builds.order(:id).last) end it 'creates builds' do expect(create_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(2) + pipeline.builds.update_all(status: "success") + expect(pipeline.builds.count(:all)).to eq(2) expect(create_next_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(4) + pipeline.builds.update_all(status: "success") + expect(pipeline.builds.count(:all)).to eq(4) expect(create_next_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(5) + pipeline.builds.update_all(status: "success") + expect(pipeline.builds.count(:all)).to eq(5) expect(create_next_builds).to be_falsey end @@ -95,14 +95,14 @@ describe Ci::Pipeline, models: true do end before do - stub_ci_commit_yaml_file(YAML.dump(yaml)) + stub_ci_pipeline_yaml_file(YAML.dump(yaml)) create_builds end it 'properly schedules builds' do - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:drop) - expect(commit.builds.pluck(:status)).to contain_exactly('pending', 'failed') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:drop) + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending', 'failed') end end @@ -136,183 +136,183 @@ describe Ci::Pipeline, models: true do end before do - stub_ci_commit_yaml_file(YAML.dump(yaml)) + stub_ci_pipeline_yaml_file(YAML.dump(yaml)) end context 'when builds are successful' do it 'properly creates builds' do expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') - commit.reload - expect(commit.status).to eq('success') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('success') end end context 'when test job fails' do it 'properly creates builds' do expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:drop) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') - commit.reload - expect(commit.status).to eq('failed') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') end end context 'when test and test_failure jobs fail' do it 'properly creates builds' do expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:drop) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - commit.builds.running_or_pending.each(&:drop) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:drop) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') - commit.reload - expect(commit.status).to eq('failed') + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') end end context 'when deploy job fails' do it 'properly creates builds' do expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - commit.builds.running_or_pending.each(&:drop) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') - commit.reload - expect(commit.status).to eq('failed') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') end end context 'when build is canceled in the second stage' do it 'does not schedule builds after build has been canceled' do expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) - expect(commit.builds.running_or_pending).not_to be_empty + expect(pipeline.builds.running_or_pending).not_to be_empty - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:cancel) + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:cancel) - expect(commit.builds.running_or_pending).to be_empty - expect(commit.reload.status).to eq('canceled') + expect(pipeline.builds.running_or_pending).to be_empty + expect(pipeline.reload.status).to eq('canceled') end end end end describe "#finished_at" do - let(:commit) { FactoryGirl.create :ci_commit } + let(:pipeline) { FactoryGirl.create :ci_pipeline } it "returns finished_at of latest build" do - build = FactoryGirl.create :ci_build, pipeline: commit, finished_at: Time.now - 60 - FactoryGirl.create :ci_build, pipeline: commit, finished_at: Time.now - 120 + build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60 + FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120 - expect(commit.finished_at.to_i).to eq(build.finished_at.to_i) + expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i) end it "returns nil if there is no finished build" do - FactoryGirl.create :ci_not_started_build, pipeline: commit + FactoryGirl.create :ci_not_started_build, pipeline: pipeline - expect(commit.finished_at).to be_nil + expect(pipeline.finished_at).to be_nil end end describe "coverage" do let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" } - let(:commit) { FactoryGirl.create :ci_commit, project: project } + let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } it "calculates average when there are two builds with coverage" do - FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: commit - expect(commit.coverage).to eq("35.00") + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline + expect(pipeline.coverage).to eq("35.00") end it "calculates average when there are two builds with coverage and one with nil" do - FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: commit - FactoryGirl.create :ci_build, pipeline: commit - expect(commit.coverage).to eq("35.00") + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline + FactoryGirl.create :ci_build, pipeline: pipeline + expect(pipeline.coverage).to eq("35.00") end it "calculates average when there are two builds with coverage and one is retried" do - FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: commit - expect(commit.coverage).to eq("35.00") + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline + FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline + expect(pipeline.coverage).to eq("35.00") end it "calculates average when there is one build without coverage" do - FactoryGirl.create :ci_build, pipeline: commit - expect(commit.coverage).to be_nil + FactoryGirl.create :ci_build, pipeline: pipeline + expect(pipeline.coverage).to be_nil end end describe '#retryable?' do - subject { commit.retryable? } + subject { pipeline.retryable? } context 'no failed builds' do before do - FactoryGirl.create :ci_build, name: "rspec", pipeline: commit, status: 'success' + FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success' end it 'be not retryable' do @@ -322,8 +322,8 @@ describe Ci::Pipeline, models: true do context 'with failed builds' do before do - FactoryGirl.create :ci_build, name: "rspec", pipeline: commit, status: 'running' - FactoryGirl.create :ci_build, name: "rubocop", pipeline: commit, status: 'failed' + FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running' + FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed' end it 'be retryable' do @@ -333,12 +333,12 @@ describe Ci::Pipeline, models: true do end describe '#stages' do - let(:commit2) { FactoryGirl.create :ci_commit, project: project } - subject { CommitStatus.where(pipeline: [commit, commit2]).stages } + let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project } + subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages } before do - FactoryGirl.create :ci_build, pipeline: commit2, stage: 'test', stage_idx: 1 - FactoryGirl.create :ci_build, pipeline: commit, stage: 'build', stage_idx: 0 + FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1 + FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0 end it 'return all stages' do @@ -348,22 +348,22 @@ describe Ci::Pipeline, models: true do describe '#update_state' do it 'execute update_state after touching object' do - expect(commit).to receive(:update_state).and_return(true) - commit.touch + expect(pipeline).to receive(:update_state).and_return(true) + pipeline.touch end context 'dependent objects' do - let(:commit_status) { build :commit_status, pipeline: commit } + let(:commit_status) { build :commit_status, pipeline: pipeline } it 'execute update_state after saving dependent object' do - expect(commit).to receive(:update_state).and_return(true) + expect(pipeline).to receive(:update_state).and_return(true) commit_status.save end end context 'update state' do let(:current) { Time.now.change(usec: 0) } - let(:build) { FactoryGirl.create :ci_build, :success, pipeline: commit, started_at: current - 120, finished_at: current - 60 } + let(:build) { FactoryGirl.create :ci_build, :success, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 } before do build @@ -371,18 +371,18 @@ describe Ci::Pipeline, models: true do [:status, :started_at, :finished_at, :duration].each do |param| it "update #{param}" do - expect(commit.send(param)).to eq(build.send(param)) + expect(pipeline.send(param)).to eq(build.send(param)) end end end end describe '#branch?' do - subject { commit.branch? } + subject { pipeline.branch? } context 'is not a tag' do before do - commit.tag = false + pipeline.tag = false end it 'return true when tag is set to false' do @@ -392,7 +392,7 @@ describe Ci::Pipeline, models: true do context 'is not a tag' do before do - commit.tag = true + pipeline.tag = true end it 'return false when tag is set to true' do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index ca630e5bc9..8fb605fff8 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe CommitStatus, models: true do - let(:commit) { FactoryGirl.create :ci_commit } - let(:commit_status) { FactoryGirl.create :commit_status, pipeline: commit } + let(:pipeline) { FactoryGirl.create :ci_pipeline } + let(:commit_status) { FactoryGirl.create :commit_status, pipeline: pipeline } it { is_expected.to belong_to(:pipeline) } it { is_expected.to belong_to(:user) } @@ -121,11 +121,11 @@ describe CommitStatus, models: true do subject { CommitStatus.latest.order(:id) } before do - @commit1 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, pipeline: commit, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: 'cc', status: 'success' - @commit4 = FactoryGirl.create :commit_status, pipeline: commit, name: 'cc', ref: 'bb', status: 'success' - @commit5 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: 'bb', status: 'success' + @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'cc', status: 'success' + @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'bb', status: 'success' + @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success' end it 'return unique statuses' do @@ -137,11 +137,11 @@ describe CommitStatus, models: true do subject { CommitStatus.running_or_pending.order(:id) } before do - @commit1 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, pipeline: commit, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, pipeline: commit, name: 'aa', ref: nil, status: 'success' - @commit4 = FactoryGirl.create :commit_status, pipeline: commit, name: 'dd', ref: nil, status: 'failed' - @commit5 = FactoryGirl.create :commit_status, pipeline: commit, name: 'ee', ref: nil, status: 'canceled' + @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: nil, status: 'success' + @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'dd', ref: nil, status: 'failed' + @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled' end it 'return statuses that are running or pending' do @@ -152,17 +152,17 @@ describe CommitStatus, models: true do describe '#before_sha' do subject { commit_status.before_sha } - context 'when no before_sha is set for ci::commit' do - before { commit.before_sha = nil } + context 'when no before_sha is set for pipeline' do + before { pipeline.before_sha = nil } it 'return blank sha' do is_expected.to eq(Gitlab::Git::BLANK_SHA) end end - context 'for before_sha set for ci::commit' do + context 'for before_sha set for pipeline' do let(:value) { '1234' } - before { commit.before_sha = value } + before { pipeline.before_sha = value } it 'return the set value' do is_expected.to eq(value) @@ -172,14 +172,14 @@ describe CommitStatus, models: true do describe '#stages' do before do - FactoryGirl.create :commit_status, pipeline: commit, stage: 'build', stage_idx: 0, status: 'success' - FactoryGirl.create :commit_status, pipeline: commit, stage: 'build', stage_idx: 0, status: 'failed' - FactoryGirl.create :commit_status, pipeline: commit, stage: 'deploy', stage_idx: 2, status: 'running' - FactoryGirl.create :commit_status, pipeline: commit, stage: 'test', stage_idx: 1, status: 'success' + FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'success' + FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'failed' + FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'deploy', stage_idx: 2, status: 'running' + FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'test', stage_idx: 1, status: 'success' end context 'stages list' do - subject { CommitStatus.where(pipeline: commit).stages } + subject { CommitStatus.where(pipeline: pipeline).stages } it 'return ordered list of stages' do is_expected.to eq(%w(build test deploy)) @@ -187,7 +187,7 @@ describe CommitStatus, models: true do end context 'stages with statuses' do - subject { CommitStatus.where(pipeline: commit).stages_status } + subject { CommitStatus.where(pipeline: pipeline).stages_status } it 'return list of stages with statuses' do is_expected.to eq({ diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index d2cd37c9b4..c4e781dd1d 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe GenericCommitStatus, models: true do - let(:commit) { FactoryGirl.create :ci_commit } - let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: commit } + let(:pipeline) { FactoryGirl.create :ci_pipeline } + let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } describe :context do subject { generic_commit_status.context } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 21d7a8d836..1b7cbc3efd 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -394,7 +394,7 @@ describe MergeRequest, models: true do describe 'when the source project exists' do it 'returns the latest commit' do commit = double(:commit, id: '123abc') - pipeline = double(:ci_commit, ref: 'master') + pipeline = double(:ci_pipeline, ref: 'master') allow(subject).to receive(:last_commit).and_return(commit) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1135d7b609..89f66092b1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -399,23 +399,23 @@ describe Project, models: true do end end - describe :ci_commit do + describe :pipeline do let(:project) { create :project } - let(:commit) { create :ci_commit, project: project, ref: 'master' } + let(:pipeline) { create :pipeline, project: project, ref: 'master' } - subject { project.pipeline(commit.sha, 'master') } + subject { project.pipeline(pipeline.sha, 'master') } - it { is_expected.to eq(commit) } + it { is_expected.to eq(pipeline) } context 'return latest' do - let(:commit2) { create :ci_commit, project: project, ref: 'master' } + let(:pipeline2) { create :pipeline, project: project, ref: 'master' } before do - commit - commit2 + pipeline + pipeline2 end - it { is_expected.to eq(commit2) } + it { is_expected.to eq(pipeline2) } end end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 534b82dc34..6cb7be188e 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -9,8 +9,8 @@ describe API::API, api: true do let!(:project) { create(:project, creator_id: user.id) } let!(:developer) { create(:project_member, :developer, user: user, project: project) } let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } - let(:commit) { create(:ci_commit, project: project)} - let(:build) { create(:ci_build, pipeline: commit) } + let(:pipeline) { create(:ci_pipeline, project: project)} + let(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do let(:query) { '' } @@ -59,8 +59,8 @@ describe API::API, api: true do describe 'GET /projects/:id/repository/commits/:sha/builds' do before do - project.ensure_pipeline(commit.sha, 'master') - get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user) + project.ensure_pipeline(pipeline.sha, 'master') + get api("/projects/#{project.id}/repository/commits/#{pipeline.sha}/builds", api_user) end context 'authorized user' do @@ -102,7 +102,7 @@ describe API::API, api: true do before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } context 'build with artifacts' do - let(:build) { create(:ci_build, :artifacts, pipeline: commit) } + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } context 'authorized user' do let(:download_headers) do @@ -131,7 +131,7 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id/trace' do - let(:build) { create(:ci_build, :trace, pipeline: commit) } + let(:build) { create(:ci_build, :trace, pipeline: pipeline) } before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } @@ -181,7 +181,7 @@ describe API::API, api: true do end describe 'POST /projects/:id/builds/:build_id/retry' do - let(:build) { create(:ci_build, :canceled, pipeline: commit) } + let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } @@ -218,7 +218,7 @@ describe API::API, api: true do end context 'build is erasable' do - let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: commit) } + let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } it 'should erase build content' do expect(response.status).to eq 201 @@ -234,7 +234,7 @@ describe API::API, api: true do end context 'build is not erasable' do - let(:build) { create(:ci_build, :trace, project: project, pipeline: commit) } + let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } it 'should respond with forbidden' do expect(response.status).to eq 403 diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 04cf15641d..33fbdde768 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -388,7 +388,7 @@ describe API::API, api: true do end describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do - let(:pipeline) { create(:ci_commit_without_jobs) } + let(:pipeline) { create(:ci_pipeline_without_jobs) } it "should return merge_request in case of success" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) @@ -443,7 +443,7 @@ describe API::API, api: true do it "enables merge when build succeeds if the ci is active" do allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) - allow(ci_commit).to receive(:active?).and_return(true) + allow(pipeline).to receive(:active?).and_return(true) put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 1a0da8d6ba..fdd4ec6d76 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -23,7 +23,7 @@ describe API::API do end before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end context 'Handles errors' do @@ -44,13 +44,13 @@ describe API::API do end context 'Have a commit' do - let(:commit) { project.pipelines.last } + let(:pipeline) { project.pipelines.last } it 'should create builds' do post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') expect(response.status).to eq(201) - commit.builds.reload - expect(commit.builds.size).to eq(2) + pipeline.builds.reload + expect(pipeline.builds.size).to eq(2) end it 'should return bad request with no builds created if there\'s no commit for that ref' do @@ -79,8 +79,8 @@ describe API::API do it 'create trigger request with variables' do post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') expect(response.status).to eq(201) - commit.builds.reload - expect(commit.builds.first.trigger_request.variables).to eq(variables) + pipeline.builds.reload + expect(pipeline.builds.first.trigger_request.variables).to eq(variables) end end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 10e3631f5f..e8508f8f95 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -7,7 +7,7 @@ describe Ci::API::API do let(:project) { FactoryGirl.create(:empty_project) } before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end describe "Builds API for runners" do @@ -20,9 +20,9 @@ describe Ci::API::API do describe "POST /builds/register" do it "should start a build" do - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - commit.create_builds(nil) - build = commit.builds.first + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') + pipeline.create_builds(nil) + build = pipeline.builds.first post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -38,8 +38,8 @@ describe Ci::API::API do end it "should return 404 error if no builds for specific runner" do - commit = FactoryGirl.create(:ci_commit, project: shared_project) - FactoryGirl.create(:ci_build, pipeline: commit, status: 'pending') + pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project) + FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending') post ci_api("/builds/register"), token: runner.token @@ -47,8 +47,8 @@ describe Ci::API::API do end it "should return 404 error if no builds for shared runner" do - commit = FactoryGirl.create(:ci_commit, project: project) - FactoryGirl.create(:ci_build, pipeline: commit, status: 'pending') + pipeline = FactoryGirl.create(:ci_pipeline, project: project) + FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending') post ci_api("/builds/register"), token: shared_runner.token @@ -56,8 +56,8 @@ describe Ci::API::API do end it "returns options" do - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - commit.create_builds(nil) + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') + pipeline.create_builds(nil) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -66,8 +66,8 @@ describe Ci::API::API do end it "returns variables" do - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - commit.create_builds(nil) + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') + pipeline.create_builds(nil) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -83,10 +83,10 @@ describe Ci::API::API do it "returns variables for triggers" do trigger = FactoryGirl.create(:ci_trigger, project: project) - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: commit, trigger: trigger) - commit.create_builds(nil, trigger_request) + trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) + pipeline.create_builds(nil, trigger_request) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -103,9 +103,9 @@ describe Ci::API::API do end it "returns dependent builds" do - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - commit.create_builds(nil, nil) - commit.builds.where(stage: 'test').each(&:success) + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') + pipeline.create_builds(nil, nil) + pipeline.builds.where(stage: 'test').each(&:success) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -131,8 +131,8 @@ describe Ci::API::API do context 'when build has no tags' do before do - commit = create(:ci_commit, project: project) - create(:ci_build, pipeline: commit, tags: []) + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, pipeline: pipeline, tags: []) end context 'when runner is allowed to pick untagged builds' do @@ -163,8 +163,8 @@ describe Ci::API::API do end describe "PUT /builds/:id" do - let(:commit) {create(:ci_commit, project: project)} - let(:build) { create(:ci_build, :trace, pipeline: commit, runner_id: runner.id) } + let(:pipeline) {create(:ci_pipeline, project: project)} + let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) } before do build.run! @@ -237,8 +237,8 @@ describe Ci::API::API do context "Artifacts" do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - let(:commit) { create(:ci_commit, project: project) } - let(:build) { create(:ci_build, pipeline: commit, runner_id: runner.id) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) } let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index 441f8d613e..72f6a3c981 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -15,7 +15,7 @@ describe Ci::API::API do end before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end context 'Handles errors' do @@ -36,13 +36,13 @@ describe Ci::API::API do end context 'Have a commit' do - let(:commit) { project.pipelines.last } + let(:pipeline) { project.pipelines.last } it 'should create builds' do post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options expect(response.status).to eq(201) - commit.builds.reload - expect(commit.builds.size).to eq(2) + pipeline.builds.reload + expect(pipeline.builds.size).to eq(2) end it 'should return bad request with no builds created if there\'s no commit for that ref' do @@ -71,8 +71,8 @@ describe Ci::API::API do it 'create trigger request with variables' do post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables) expect(response.status).to eq(201) - commit.builds.reload - expect(commit.builds.first.trigger_request.variables).to eq(variables) + pipeline.builds.reload + expect(pipeline.builds.first.trigger_request.variables).to eq(variables) end end end diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb index ecc3a88a26..984b78487d 100644 --- a/spec/services/ci/create_builds_service_spec.rb +++ b/spec/services/ci/create_builds_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreateBuildsService, services: true do - let(:commit) { create(:ci_commit, ref: 'master') } + let(:pipeline) { create(:ci_pipeline, ref: 'master') } let(:user) { create(:user) } describe '#execute' do @@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do # subject do - described_class.new(commit).execute(commit, nil, user, status) + described_class.new(pipeline).execute('test', nil, user, status) end context 'next builds available' do diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index dbdc5370bd..ae4b7aca82 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -6,7 +6,7 @@ describe Ci::CreateTriggerRequestService, services: true do let(:trigger) { create(:ci_trigger, project: project) } before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end describe :execute do @@ -27,8 +27,8 @@ describe Ci::CreateTriggerRequestService, services: true do subject { service.execute(project, trigger, 'master') } before do - stub_ci_commit_yaml_file('{}') - FactoryGirl.create :ci_commit, project: project + stub_ci_pipeline_yaml_file('{}') + FactoryGirl.create :ci_pipeline, project: project end it { expect(subject).to be_nil } diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 6f4d29a198..d91fc57429 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -4,8 +4,8 @@ module Ci describe RegisterBuildService, services: true do let!(:service) { RegisterBuildService.new } let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false } - let!(:commit) { FactoryGirl.create :ci_commit, project: project } - let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: commit } + let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } + let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline } let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 3785723a03..202eede8e2 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -6,12 +6,12 @@ describe CreateCommitBuildsService, services: true do let(:user) { nil } before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end describe :execute do context 'valid params' do - let(:commit) do + let(:pipeline) do service.execute(project, user, ref: 'refs/heads/master', before: '00000000', @@ -20,11 +20,11 @@ describe CreateCommitBuildsService, services: true do ) end - it { expect(commit).to be_kind_of(Ci::Pipeline) } - it { expect(commit).to be_valid } - it { expect(commit).to be_persisted } - it { expect(commit).to eq(project.pipelines.last) } - it { expect(commit.builds.first).to be_kind_of(Ci::Build) } + it { expect(pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(pipeline).to be_valid } + it { expect(pipeline).to be_persisted } + it { expect(pipeline).to eq(project.pipelines.last) } + it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } end context "skip tag if there is no build for it" do @@ -40,7 +40,7 @@ describe CreateCommitBuildsService, services: true do it "creates commit if there is no appropriate job but deploy job has right ref setting" do config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } }) - stub_ci_commit_yaml_file(config) + stub_ci_pipeline_yaml_file(config) result = service.execute(project, user, ref: 'refs/heads/0_1', @@ -53,7 +53,7 @@ describe CreateCommitBuildsService, services: true do end it 'skips creating ci_commit for refs without .gitlab-ci.yml' do - stub_ci_commit_yaml_file(nil) + stub_ci_pipeline_yaml_file(nil) result = service.execute(project, user, ref: 'refs/heads/0_1', before: '00000000', @@ -67,18 +67,18 @@ describe CreateCommitBuildsService, services: true do it 'fails commits if yaml is invalid' do message = 'message' allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } - stub_ci_commit_yaml_file('invalid: file: file') + stub_ci_pipeline_yaml_file('invalid: file: file') commits = [{ message: message }] - commit = service.execute(project, user, + pipeline = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', commits: commits ) - expect(commit).to be_persisted - expect(commit.builds.any?).to be false - expect(commit.status).to eq('failed') - expect(commit.yaml_errors).not_to be_nil + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq('failed') + expect(pipeline.yaml_errors).not_to be_nil end describe :ci_skip? do @@ -90,45 +90,45 @@ describe CreateCommitBuildsService, services: true do it "skips builds creation if there is [ci skip] tag in commit message" do commits = [{ message: message }] - commit = service.execute(project, user, + pipeline = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', commits: commits ) - expect(commit).to be_persisted - expect(commit.builds.any?).to be false - expect(commit.status).to eq("skipped") + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") end it "does not skips builds creation if there is no [ci skip] tag in commit message" do allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } commits = [{ message: "some message" }] - commit = service.execute(project, user, + pipeline = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', commits: commits ) - expect(commit).to be_persisted - expect(commit.builds.first.name).to eq("staging") + expect(pipeline).to be_persisted + expect(pipeline.builds.first.name).to eq("staging") end it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do - stub_ci_commit_yaml_file('invalid: file: fiile') + stub_ci_pipeline_yaml_file('invalid: file: fiile') commits = [{ message: message }] - commit = service.execute(project, user, + pipeline = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', commits: commits ) - expect(commit).to be_persisted - expect(commit.builds.any?).to be false - expect(commit.status).to eq("skipped") - expect(commit.yaml_errors).to be_nil + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + expect(pipeline.yaml_errors).to be_nil end end @@ -136,40 +136,40 @@ describe CreateCommitBuildsService, services: true do allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml } commits = [{ message: "message" }] - commit = service.execute(project, user, + pipeline = service.execute(project, user, ref: 'refs/heads/master', before: '00000000', after: '31das312', commits: commits ) - expect(commit).to be_persisted - expect(commit.builds.count(:all)).to eq(2) + expect(pipeline).to be_persisted + expect(pipeline.builds.count(:all)).to eq(2) - commit = service.execute(project, user, + pipeline = service.execute(project, user, ref: 'refs/heads/master', before: '00000000', after: '31das312', commits: commits ) - expect(commit).to be_persisted - expect(commit.builds.count(:all)).to eq(2) + expect(pipeline).to be_persisted + expect(pipeline.builds.count(:all)).to eq(2) end it "creates commit with failed status if yaml is invalid" do - stub_ci_commit_yaml_file('invalid: file') + stub_ci_pipeline_yaml_file('invalid: file') commits = [{ message: "some message" }] - commit = service.execute(project, user, + pipeline = service.execute(project, user, ref: 'refs/tags/0_1', before: '00000000', after: '31das312', commits: commits ) - expect(commit).to be_persisted - expect(commit.status).to eq("failed") - expect(commit.builds.any?).to be false + expect(pipeline).to be_persisted + expect(pipeline.status).to eq("failed") + expect(pipeline.builds.any?).to be false end end end diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index a7f52a2fa2..dd656c3bbb 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -6,7 +6,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do let(:merge_request) { create(:merge_request) } let(:project) { create(:project) } let(:sha) { '1234567890abcdef1234567890abcdef12345678' } - let(:pipeline) { create(:ci_commit_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) } + let(:pipeline) { create(:ci_pipeline_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) } let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') } let(:todo_service) { TodoService.new } diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 6d6539a0e7..4da8146e3d 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -10,7 +10,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do source_project: project, target_project: project, state: "opened") end - let(:pipeline) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) } + let(:pipeline) { create(:ci_pipeline_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) } let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') } describe "#execute" do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 29e0a63d8c..09f0ee3871 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -208,7 +208,7 @@ describe SystemNoteService, services: true do end describe '.merge_when_build_succeeds' do - let(:ci_commit) { build(:ci_commit_without_jobs )} + let(:pipeline) { build(:ci_pipeline_without_jobs )} let(:noteable) do create(:merge_request, source_project: project, target_project: project) end @@ -223,7 +223,6 @@ describe SystemNoteService, services: true do end describe '.cancel_merge_when_build_succeeds' do - let(:ci_commit) { build(:ci_commit_without_jobs) } let(:noteable) do create(:merge_request, source_project: project, target_project: project) end diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 49660a48a8..93f96cacc0 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -13,11 +13,11 @@ module StubGitlabCalls allow_any_instance_of(Network).to receive(:projects) { project_hash_array } end - def stub_ci_commit_to_return_yaml_file - stub_ci_commit_yaml_file(gitlab_ci_yaml) + def stub_ci_pipeline_to_return_yaml_file + stub_ci_pipeline_yaml_file(gitlab_ci_yaml) end - def stub_ci_commit_yaml_file(ci_yaml) + def stub_ci_pipeline_yaml_file(ci_yaml) allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml } end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 7d0cfed362..b8e73682c9 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -53,13 +53,13 @@ describe PostReceive do subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) } context "creates a Ci::Pipeline for every change" do - before { stub_ci_commit_to_return_yaml_file } + before { stub_ci_pipeline_to_return_yaml_file } it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) } end context "does not create a Ci::Pipeline" do - before { stub_ci_commit_yaml_file(nil) } + before { stub_ci_pipeline_yaml_file(nil) } it { expect{ subject }.not_to change{ Ci::Pipeline.count } } end From 393ec8e74a328660b3d7eaafb25708bc1fa13ee3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 16:22:53 +0200 Subject: [PATCH 149/507] Rename `commit` to `pipeline` in application code --- app/services/ci/create_builds_service.rb | 18 +++++++++--------- app/services/create_commit_builds_service.rb | 14 +++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 18274ce24e..41ce6982cb 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -1,7 +1,7 @@ module Ci class CreateBuildsService - def initialize(commit) - @commit = commit + def initialize(pipeline) + @pipeline = pipeline end def execute(stage, user, status, trigger_request = nil) @@ -21,8 +21,8 @@ module Ci builds_attrs.map do |build_attrs| # don't create the same build twice - unless @commit.builds.find_by(ref: @commit.ref, tag: @commit.tag, - trigger_request: trigger_request, name: build_attrs[:name]) + unless @pipeline.builds.find_by(ref: @pipeline.ref, tag: @pipeline.tag, + trigger_request: trigger_request, name: build_attrs[:name]) build_attrs.slice!(:name, :commands, :tag_list, @@ -31,13 +31,13 @@ module Ci :stage, :stage_idx) - build_attrs.merge!(ref: @commit.ref, - tag: @commit.tag, + build_attrs.merge!(ref: @pipeline.ref, + tag: @pipeline.tag, trigger_request: trigger_request, user: user, - project: @commit.project) + project: @pipeline.project) - @commit.builds.create!(build_attrs) + @pipeline.builds.create!(build_attrs) end end end @@ -45,7 +45,7 @@ module Ci private def config_processor - @config_processor ||= @commit.config_processor + @config_processor ||= @pipeline.config_processor end end end diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 70a7d4bef4..9091424ccf 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -18,23 +18,23 @@ class CreateCommitBuildsService return false end - commit = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) + pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) # Skip creating ci_commit when no gitlab-ci.yml is found - unless commit.ci_yaml_file + unless pipeline.ci_yaml_file return false end # Create a new ci_commit - commit.save! + pipeline.save! # Skip creating builds for commits that have [ci skip] - unless commit.skip_ci? + unless pipeline.skip_ci? # Create builds for commit - commit.create_builds(user) + pipeline.create_builds(user) end - commit.touch - commit + pipeline.touch + pipeline end end From 1927a2d30bf88ab9fe3c1235cb529f5fd8dc01be Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 16:27:50 +0200 Subject: [PATCH 150/507] Rename all ci_commit[s] in application code to pipeline[s] --- app/controllers/projects/commit_controller.rb | 10 ++-- .../projects/merge_requests_controller.rb | 2 +- app/helpers/ci_status_helper.rb | 6 +-- .../ci/create_trigger_request_service.rb | 6 +-- app/services/ci/image_for_build_service.rb | 6 +-- app/services/create_commit_builds_service.rb | 4 +- .../merge_when_build_succeeds_service.rb | 4 +- app/views/projects/commit/_builds.html.haml | 4 +- .../projects/commit/_ci_commit.html.haml | 52 ------------------- app/views/projects/commit/_pipeline.html.haml | 52 +++++++++++++++++++ .../merge_requests/_new_submit.html.haml | 4 +- .../projects/merge_requests/_show.html.haml | 2 +- .../merge_requests/show/_builds.html.haml | 2 +- .../merge_requests/widget/_heading.html.haml | 6 +-- .../widget/open/_accept.html.haml | 4 +- app/views/projects/pipelines/show.html.haml | 2 +- lib/api/commit_statuses.rb | 10 ++-- 17 files changed, 88 insertions(+), 88 deletions(-) delete mode 100644 app/views/projects/commit/_ci_commit.html.haml create mode 100644 app/views/projects/commit/_pipeline.html.haml diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 737e6c931f..20637fa46f 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -99,12 +99,12 @@ class Projects::CommitController < Projects::ApplicationController @commit ||= @project.commit(params[:id]) end - def ci_commits - @ci_commits ||= project.pipelines.where(sha: commit.sha) + def pipelines + @pipelines ||= project.pipelines.where(sha: commit.sha) end def ci_builds - @ci_builds ||= Ci::Build.where(pipeline: ci_commits) + @ci_builds ||= Ci::Build.where(pipeline: pipelines) end def define_show_vars @@ -117,8 +117,8 @@ class Projects::CommitController < Projects::ApplicationController @diff_refs = [commit.parent || commit, commit] @notes_count = commit.notes.count - @statuses = CommitStatus.where(pipeline: ci_commits) - @builds = Ci::Build.where(pipeline: ci_commits) + @statuses = CommitStatus.where(pipeline: pipelines) + @builds = Ci::Build.where(pipeline: pipelines) end def assign_change_commit_vars(mr_source_branch) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e6924a6a45..1fca05e949 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -316,7 +316,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff @pipeline = @merge_request.pipeline - @statuses = @ci_commit.statuses if @pipeline + @statuses = @pipeline.statuses if @pipeline if @merge_request.locked_long_ago? @merge_request.unlock_mr diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index cfad17dcac..07e5c14684 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,7 +1,7 @@ module CiStatusHelper - def ci_status_path(ci_commit) - project = ci_commit.project - builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) + def ci_status_path(pipeline) + project = pipeline.project + builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) end def ci_status_with_icon(status, target = nil) diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index cd8a2b2510..c3194f45b1 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -7,14 +7,14 @@ module Ci # check if ref is tag tag = project.repository.find_tag(ref).present? - ci_commit = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag) + pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag) trigger_request = trigger.trigger_requests.create!( variables: variables, - commit: ci_commit, + commit: pipeline, ) - if ci_commit.create_builds(nil, trigger_request) + if pipeline.create_builds(nil, trigger_request) trigger_request end end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 90eb3e365f..75d847d5be 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -3,9 +3,9 @@ module Ci def execute(project, opts) sha = opts[:sha] || ref_sha(project, opts[:ref]) - ci_commits = project.pipelines.where(sha: sha) - ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref] - image_name = image_for_status(ci_commits.status) + pipelines = project.pipelines.where(sha: sha) + pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref] + image_name = image_for_status(pipelines.status) image_path = Rails.root.join('public/ci', image_name) OpenStruct.new(path: image_path, name: image_name) diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 9091424ccf..418f5cf809 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -20,12 +20,12 @@ class CreateCommitBuildsService pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) - # Skip creating ci_commit when no gitlab-ci.yml is found + # Skip creating pipeline when no gitlab-ci.yml is found unless pipeline.ci_yaml_file return false end - # Create a new ci_commit + # Create a new pipeline pipeline.save! # Skip creating builds for commits that have [ci skip] diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index 8fd6a4ea1f..12edfb2d67 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -20,10 +20,10 @@ module MergeRequests # Triggers the automatic merge of merge_request once the build succeeds def trigger(commit_status) - each_merge_request(commit_status) do |merge_request, ci_commit| + each_merge_request(commit_status) do |merge_request, pipeline| next unless merge_request.merge_when_build_succeeds? next unless merge_request.mergeable? - next unless ci_commit.success? + next unless pipeline.success? MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) end diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 7f7a15aa21..a508382578 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,2 +1,2 @@ -- @ci_commits.each do |ci_commit| - = render "ci_commit", ci_commit: ci_commit, pipeline_details: true +- @pipelines.each do |pipeline| + = render "pipeline", pipeline: pipeline, pipeline_details: true diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml deleted file mode 100644 index 32ff4d3097..0000000000 --- a/app/views/projects/commit/_ci_commit.html.haml +++ /dev/null @@ -1,52 +0,0 @@ -.row-content-block.build-content.middle-block - .pull-right - - if can?(current_user, :update_pipeline, ci_commit.project) - - if ci_commit.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_namespace_project_pipeline_path(ci_commit.project.namespace, ci_commit.project, ci_commit.id), class: 'btn btn-grouped btn-primary', method: :post - - - if ci_commit.builds.running_or_pending.any? - = link_to "Cancel running", cancel_namespace_project_pipeline_path(ci_commit.project.namespace, ci_commit.project, ci_commit.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - - .oneline.clearfix - - if defined?(pipeline_details) && pipeline_details - Pipeline - = link_to "##{ci_commit.id}", namespace_project_pipeline_path(ci_commit.project.namespace, ci_commit.project, ci_commit.id), class: "monospace" - with - = pluralize ci_commit.statuses.count(:id), "build" - - if ci_commit.ref - for - = link_to ci_commit.ref, namespace_project_commits_path(ci_commit.project.namespace, ci_commit.project, ci_commit.ref), class: "monospace" - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to ci_commit.short_sha, namespace_project_commit_path(ci_commit.project.namespace, ci_commit.project, ci_commit.sha), class: "monospace" - - if ci_commit.duration - in - = time_interval_in_words ci_commit.duration - -- if ci_commit.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - ci_commit.yaml_errors.split(",").each do |error| - %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} - -- if ci_commit.project.builds_enabled? && !ci_commit.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit - -.table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Name - %th Tags - %th Duration - %th Finished at - - if ci_commit.project.build_coverage_enabled? - %th Coverage - %th - - ci_commit.statuses.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: ci_commit.statuses.where(stage: stage) diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml new file mode 100644 index 0000000000..0411137b7c --- /dev/null +++ b/app/views/projects/commit/_pipeline.html.haml @@ -0,0 +1,52 @@ +.row-content-block.build-content.middle-block + .pull-right + - if can?(current_user, :update_pipeline, pipeline.project) + - if pipeline.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post + + - if pipeline.builds.running_or_pending.any? + = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + + .oneline.clearfix + - if defined?(pipeline_details) && pipeline_details + Pipeline + = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" + with + = pluralize pipeline.statuses.count(:id), "build" + - if pipeline.ref + for + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" + - if pipeline.duration + in + = time_interval_in_words pipeline.duration + +- if pipeline.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - pipeline.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + +- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +.table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Name + %th Tags + %th Duration + %th Finished at + - if pipeline.project.build_coverage_enabled? + %th Coverage + %th + - pipeline.statuses.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage) diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 18b3f9e154..a5e67b9572 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -23,7 +23,7 @@ = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size - - if @ci_commit + - if @pipeline %li.builds-tab.active = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do Builds @@ -43,7 +43,7 @@ %p To preserve performance the line changes are not shown. - else = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false - - if @ci_commit + - if @pipeline #builds.builds.tab-pane = render "projects/merge_requests/show/builds" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 7af227129e..c2e1a78d3c 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -54,7 +54,7 @@ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size - - if @ci_commit + - if @pipeline %li.builds-tab = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do Builds diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml index a116ffe2e1..81de60f116 100644 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -1,2 +1,2 @@ -= render "projects/commit/ci_commit", ci_commit: @ci_commit, link_to_commit: true += render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 4d38175461..08a38d283d 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,7 +1,7 @@ -- if @ci_commit +- if @pipeline .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) } + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } = ci_icon_for_status(status) %span CI build @@ -9,7 +9,7 @@ for - commit = @merge_request.last_commit = succeed "." do - = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 0d49b6471a..60d7d6ff1f 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,4 +1,4 @@ -- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil +- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token @@ -6,7 +6,7 @@ .accept-merge-holder.clearfix.js-toggle-container .clearfix .accept-action - - if @ci_commit && @ci_commit.active? + - if @pipeline && @pipeline.active? %span.btn-group = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do Merge When Build Succeeds diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 2aad560241..75943c6427 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -5,4 +5,4 @@ = render "projects/pipelines/info" %div.block-connector -= render "projects/commit/ci_commit", ci_commit: @pipeline += render "projects/commit/pipeline", pipeline: @pipeline diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 088d5bac58..323a708689 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -22,8 +22,8 @@ module API not_found!('Commit') unless user_project.commit(params[:sha]) - ci_commits = user_project.pipelines.where(sha: params[:sha]) - statuses = ::CommitStatus.where(pipeline: ci_commits) + pipelines = user_project.pipelines.where(sha: params[:sha]) + statuses = ::CommitStatus.where(pipeline: pipelines) statuses = statuses.latest unless parse_boolean(params[:all]) statuses = statuses.where(ref: params[:ref]) if params[:ref].present? statuses = statuses.where(stage: params[:stage]) if params[:stage].present? @@ -64,11 +64,11 @@ module API ref = branches.first end - ci_commit = @project.ensure_pipeline(commit.sha, ref) + pipeline = @project.ensure_pipeline(commit.sha, ref) name = params[:name] || params[:context] - status = GenericCommitStatus.running_or_pending.find_by(pipeline: ci_commit, name: name, ref: params[:ref]) - status ||= GenericCommitStatus.new(project: @project, pipeline: ci_commit, user: current_user) + status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref]) + status ||= GenericCommitStatus.new(project: @project, pipeline: pipeline, user: current_user) status.update(attrs) case params[:state].to_s From 836061ddfde6652c1e240ac093c32b9d95b8cd84 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 16:28:15 +0200 Subject: [PATCH 151/507] Rename remaining ci_commits in specs --- .../features/merge_requests/merge_when_build_succeeds_spec.rb | 2 +- spec/requests/api/commit_statuses_spec.rb | 2 +- spec/requests/api/commits_spec.rb | 4 ++-- spec/services/create_commit_builds_service_spec.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 843bd5aced..c5e6412d7b 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -13,7 +13,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do context "Active build for Merge Request" do let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } - let!(:ci_build) { create(:ci_build, pipeline: ci_commit) } + let!(:ci_build) { create(:ci_build, pipeline: pipeline) } before do login_as user diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 5c5850c558..298cdbad32 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -5,7 +5,7 @@ describe API::CommitStatuses, api: true do let!(:project) { create(:project) } let(:commit) { project.repository.commit } - let(:commit_status) { create(:commit_status, pipeline: ci_commit) } + let(:commit_status) { create(:commit_status, pipeline: pipeline) } let(:guest) { create_user(:guest) } let(:reporter) { create_user(:reporter) } let(:developer) { create_user(:developer) } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 2336ec97ed..6fc38f537d 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -90,10 +90,10 @@ describe API::API, api: true do end it "should return status for CI" do - ci_commit = project.ensure_pipeline(project.repository.commit.sha, 'master') + pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response.status).to eq(200) - expect(json_response['status']).to eq(ci_commit.status) + expect(json_response['status']).to eq(pipeline.status) end end diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 202eede8e2..706319b63e 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -52,7 +52,7 @@ describe CreateCommitBuildsService, services: true do end end - it 'skips creating ci_commit for refs without .gitlab-ci.yml' do + it 'skips creating pipeline for refs without .gitlab-ci.yml' do stub_ci_pipeline_yaml_file(nil) result = service.execute(project, user, ref: 'refs/heads/0_1', From 77d4d5e3a694b73a7db7de7f630ce62912a896e8 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 3 Jun 2016 16:35:56 +0200 Subject: [PATCH 152/507] :police_car: --- app/services/issues/move_service.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 138730ca35..ab667456db 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -74,12 +74,12 @@ module Issues end def rewrite_award_emoji - @old_issue.award_emoji.each do |award| - new_award = award.dup - new_award.awardable = @new_issue - new_award.save - end - end + @old_issue.award_emoji.each do |award| + new_award = award.dup + new_award.awardable = @new_issue + new_award.save + end + end def rewrite_content(content) return unless content From fa35aea3ddf1093db26f8b7fec78175a5f88af7a Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 3 Jun 2016 17:07:40 +0200 Subject: [PATCH 153/507] Refactor Gitlab::Auth rate limiting --- lib/gitlab/auth.rb | 36 +++++++++------------------- lib/gitlab/auth/rate_limiter.rb | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 lib/gitlab/auth/rate_limiter.rb diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 672642ebfb..dd6ba84c97 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,5 +1,5 @@ module Gitlab - class Auth + module Auth Result = Struct.new(:user, :type) class << self @@ -64,34 +64,20 @@ module Gitlab end def rate_limit!(ip, success:, login:) - # 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. - # - # 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 - return unless config.enabled + rate_limiter = IpRateLimiter.new(ip) + return unless rate_limiter.enabled? if success - Rack::Attack::Allow2Ban.reset(ip, config) + # Repeated login 'failures' are normal behavior for some Git clients so + # it is important to reset the ban counter once the client has proven + # they are not a 'bad guy'. + rate_limiter.reset! else - banned = Rack::Attack::Allow2Ban.filter(ip, config) do - if config.ip_whitelist.include?(ip) - # Don't increment the ban counter for this IP - false - else - # Increment the ban counter for this IP - true - end - end + # Register a login failure so that Rack::Attack can block the next + # request from this IP if needed. + rate_limiter.register_fail!(ip, config) - if banned + if rate_limiter.banned? Rails.logger.info "IP #{ip} failed to login " \ "as #{login} but has been temporarily banned from Git auth" end diff --git a/lib/gitlab/auth/rate_limiter.rb b/lib/gitlab/auth/rate_limiter.rb new file mode 100644 index 0000000000..4be9f6d0ef --- /dev/null +++ b/lib/gitlab/auth/rate_limiter.rb @@ -0,0 +1,42 @@ +module Gitlab + module Auth + class IpRateLimiter + attr_reader :ip + + def initialize(ip) + @ip = ip + @banned = false + end + + def enabled? + config.enabled + end + + def reset! + Rack::Attack::Allow2Ban.reset(ip, config) + end + + def register_fail! + # Allow2Ban.filter will return false if this IP has not failed too often yet + @banned = Rack::Attack::Allow2Ban.filter(ip, config) do + # If we return false here, the failure for this IP is ignored by Allow2Ban + ignore_failure? + end + end + + def banned? + @banned + end + + private + + def config + Gitlab.config.rack_attack.git_basic_auth + end + + def ignore_failure? + config.ip_whitelist.exclude?(ip) + end + end + end +end From 0a1fccb2ed0e0479dbaa4c22726d5c7a440f014c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 20:17:15 -0500 Subject: [PATCH 154/507] Try to use knapsack --- .gitlab-ci.yml | 91 ++++++++++++++++++++--------------------- Gemfile | 1 + Gemfile.lock | 5 +++ Rakefile | 3 ++ features/support/env.rb | 3 ++ spec/knapsack_merger.rb | 41 +++++++++++++++++++ spec/spec_helper.rb | 4 ++ 7 files changed, 101 insertions(+), 47 deletions(-) create mode 100644 spec/knapsack_merger.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85730e1b68..a819610c41 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,58 +25,55 @@ before_script: - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate +.rspec_tests: &rspec_tests + stage: test + variables: + RAILS_ENV: "test" + SIMPLECOV: "true" + script: + - JOB_NAME=( $CI_BUILD_NAME ) + - export CI_NODE_INDEX=${JOB_NAME[1]} + - export CI_NODE_TOTAL=${JOB_NAME[2]} + - bundle exec rake assets:precompile 2>/dev/null + - bundle exec rake knapsack:rspec + +.spinach_tests: &spinach_tests + stage: test + variables: + RAILS_ENV: "test" + SIMPLECOV: "true" + script: + - JOB_NAME=( $CI_BUILD_NAME ) + - export CI_NODE_INDEX=${JOB_NAME[1]} + - export CI_NODE_TOTAL=${JOB_NAME[2]} + - bundle exec rake assets:precompile 2>/dev/null + - bundle exec rake knapsack:cucumber + stages: - test - notifications -spec:feature: - stage: test - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature +spec 0 10: *rspec_tests +spec 1 10: *rspec_tests +spec 2 10: *rspec_tests +spec 3 10: *rspec_tests +spec 4 10: *rspec_tests +spec 5 10: *rspec_tests +spec 6 10: *rspec_tests +spec 7 10: *rspec_tests +spec 8 10: *rspec_tests +spec 9 10: *rspec_tests -spec:api: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api - -spec:models: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models - -spec:lib: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib - -spec:services: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services - -spec:other: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other - -spinach:project:half: - stage: test - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half - -spinach:project:rest: - stage: test - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest - -spinach:other: - stage: test - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other +spinach 0 10: *spinach_tests +spinach 1 10: *spinach_tests +spinach 2 10: *spinach_tests +spinach 3 10: *spinach_tests +spinach 4 10: *spinach_tests +spinach 5 10: *spinach_tests +spinach 6 10: *spinach_tests +spinach 7 10: *spinach_tests +spinach 8 10: *spinach_tests +spinach 9 10: *spinach_tests teaspoon: stage: test diff --git a/Gemfile b/Gemfile index d9429de786..b9ae1aecb5 100644 --- a/Gemfile +++ b/Gemfile @@ -313,6 +313,7 @@ group :test do gem 'webmock', '~> 1.21.0' gem 'test_after_commit', '~> 0.4.2' gem 'sham_rack' + gem 'knapsack' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 8ae25269e6..930a0f3f8d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -358,6 +358,9 @@ GEM actionpack (>= 3.0.0) activesupport (>= 3.0.0) kgio (2.10.0) + knapsack (1.9.0) + rake + timecop (>= 0.1.0) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.4.1) @@ -728,6 +731,7 @@ GEM thor (0.19.1) thread_safe (0.3.5) tilt (2.0.2) + timecop (0.8.1) timfel-krb5-auth (0.8.3) tinder (1.10.1) eventmachine (~> 1.0) @@ -874,6 +878,7 @@ DEPENDENCIES jquery-ui-rails (~> 5.0.0) jwt kaminari (~> 0.17.0) + knapsack letter_opener_web (~> 1.3.0) licensee (~> 8.0.0) loofah (~> 2.0.3) diff --git a/Rakefile b/Rakefile index 5dd389d567..16261bf8ae 100755 --- a/Rakefile +++ b/Rakefile @@ -3,8 +3,11 @@ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) +require 'knapsack' relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__) require relative_url_conf if File.exist?("#{relative_url_conf}.rb") Gitlab::Application.load_tasks + +Knapsack.load_tasks diff --git a/features/support/env.rb b/features/support/env.rb index 357d164d87..6ebd012a40 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -11,6 +11,7 @@ ENV['RAILS_ENV'] = 'test' require './config/environment' require 'rspec/expectations' require 'sidekiq/testing/inline' +require 'knapsack' require_relative 'capybara' require_relative 'db_cleaner' @@ -20,6 +21,8 @@ require_relative 'rerun' require Rails.root.join('spec', 'support', f) end +Knapsack::Adapters::CucumberAdapter.bind + Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } WebMock.allow_net_connect! diff --git a/spec/knapsack_merger.rb b/spec/knapsack_merger.rb new file mode 100644 index 0000000000..c6bcefe846 --- /dev/null +++ b/spec/knapsack_merger.rb @@ -0,0 +1,41 @@ +begin + class Knapsack::Report + alias_method :save_without_leading_existing_report, :save + + def load_existing_report + Knapsack::Presenter.existing_report = open + rescue + false + end + + def save + load_existing_report + save_without_leading_existing_report + end + end + + class << Knapsack::Presenter + attr_accessor :existing_report + + def initialize + @existing_report = [] + end + + def report_hash + return current_report_hash unless existing_report + existing_report.merge(current_report_hash).sort.to_h + end + + def current_report_hash + Knapsack.tracker.test_files_with_time + end + + def report_yml + report_hash.to_yaml + end + + def report_json + JSON.pretty_generate(report_hash) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 576d16e7ea..84b9ee75f6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,6 +15,10 @@ require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'rspec/retry' +require 'knapsack' +require_relative 'knapsack_merger' + +Knapsack::Adapters::RSpecAdapter.bind # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. From 28469ac712c37584a709fc33a8cfaa25a810711c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 20:34:35 -0500 Subject: [PATCH 155/507] Touch reports --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a819610c41..f2252e9018 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,6 +31,7 @@ before_script: RAILS_ENV: "test" SIMPLECOV: "true" script: + - touch knapsack_rspec_report.json - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} @@ -43,6 +44,7 @@ before_script: RAILS_ENV: "test" SIMPLECOV: "true" script: + - touch knapsack_cucumber_report.json - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} From 5ebc2a53ef536982f93fb7d32dd616a1162c07b3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 20:41:17 -0500 Subject: [PATCH 156/507] Use build stage --- .gitlab-ci.yml | 55 ++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2252e9018..d7b99cf27b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,8 @@ variables: MYSQL_ALLOW_EMPTY_PASSWORD: "1" # retry tests only in CI environment RSPEC_RETRY_RETRY_COUNT: "3" + RAILS_ENV: "test" + SIMPLECOV: "true" before_script: - source ./scripts/prepare_build.sh @@ -23,15 +25,11 @@ before_script: - touch log/application.log - touch log/test.log - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate + - bundle exec rake db:drop db:create db:schema:load db:migrate .rspec_tests: &rspec_tests stage: test - variables: - RAILS_ENV: "test" - SIMPLECOV: "true" script: - - touch knapsack_rspec_report.json - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} @@ -40,11 +38,7 @@ before_script: .spinach_tests: &spinach_tests stage: test - variables: - RAILS_ENV: "test" - SIMPLECOV: "true" script: - - touch knapsack_cucumber_report.json - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} @@ -52,9 +46,22 @@ before_script: - bundle exec rake knapsack:cucumber stages: +- build - test - notifications +prepare: + stage: build + script: + - bundle exec rake assets:precompile + - echo "{}" > knapsack_rspec_report.json + - echo "{}" > knapsack_cucumber_report.json + artifacts: + paths: + - assets/public/ + - knapsack_rspec_report.json + - knapsack_cucumber_report.json + spec 0 10: *rspec_tests spec 1 10: *rspec_tests spec 2 10: *rspec_tests @@ -80,7 +87,7 @@ spinach 9 10: *spinach_tests teaspoon: stage: test script: - - RAILS_ENV=test bundle exec teaspoon + - bundle exec teaspoon rubocop: stage: test @@ -117,7 +124,7 @@ bundler:audit: db-migrate-reset: stage: test script: - - RAILS_ENV=test bundle exec rake db:migrate:reset + - bundle exec rake db:migrate:reset # Ruby 2.2 jobs @@ -127,8 +134,8 @@ spec:feature:ruby22: only: - master script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature + - bundle exec rake assets:precompile 2>/dev/null + - bundle exec rake spec:feature cache: key: "ruby22" paths: @@ -140,7 +147,7 @@ spec:api:ruby22: only: - master script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api + - bundle exec rake spec:api cache: key: "ruby22" paths: @@ -152,7 +159,7 @@ spec:models:ruby22: only: - master script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models + - bundle exec rake spec:models cache: key: "ruby22" paths: @@ -164,7 +171,7 @@ spec:lib:ruby22: only: - master script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib + - bundle exec rake spec:lib cache: key: "ruby22" paths: @@ -176,7 +183,7 @@ spec:services:ruby22: only: - master script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services + - bundle exec rake spec:services cache: key: "ruby22" paths: @@ -188,7 +195,7 @@ spec:other:ruby22: only: - master script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other + - bundle exec rake spec:other cache: key: "ruby22" paths: @@ -200,8 +207,8 @@ spinach:project:half:ruby22: only: - master script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half + - bundle exec rake assets:precompile 2>/dev/null + - bundle exec rake spinach:project:half cache: key: "ruby22" paths: @@ -213,8 +220,8 @@ spinach:project:rest:ruby22: only: - master script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest + - bundle exec rake assets:precompile 2>/dev/null + - bundle exec rake spinach:project:rest cache: key: "ruby22" paths: @@ -226,8 +233,8 @@ spinach:other:ruby22: only: - master script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other + - bundle exec rake assets:precompile 2>/dev/null + - bundle exec rake spinach:other cache: key: "ruby22" paths: From 71edcf59ee1f8bf7047ca293d5693642feeb3669 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 20:51:09 -0500 Subject: [PATCH 157/507] Improve .gitlab-ci.yml --- .gitlab-ci.yml | 110 +++++++++++++++++-------------------------------- 1 file changed, 38 insertions(+), 72 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d7b99cf27b..dc81a69855 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,35 +15,13 @@ variables: RSPEC_RETRY_RETRY_COUNT: "3" RAILS_ENV: "test" SIMPLECOV: "true" + USE_DB: "true" before_script: - source ./scripts/prepare_build.sh - - ruby -v - - which ruby - - retry gem install bundler --no-ri --no-rdoc - cp config/gitlab.yml.example config/gitlab.yml - - touch log/application.log - - touch log/test.log - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - - bundle exec rake db:drop db:create db:schema:load db:migrate - -.rspec_tests: &rspec_tests - stage: test - script: - - JOB_NAME=( $CI_BUILD_NAME ) - - export CI_NODE_INDEX=${JOB_NAME[1]} - - export CI_NODE_TOTAL=${JOB_NAME[2]} - - bundle exec rake assets:precompile 2>/dev/null - - bundle exec rake knapsack:rspec - -.spinach_tests: &spinach_tests - stage: test - script: - - JOB_NAME=( $CI_BUILD_NAME ) - - export CI_NODE_INDEX=${JOB_NAME[1]} - - export CI_NODE_TOTAL=${JOB_NAME[2]} - - bundle exec rake assets:precompile 2>/dev/null - - bundle exec rake knapsack:cucumber + - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' stages: - build @@ -52,6 +30,8 @@ stages: prepare: stage: build + variables: + USE_DB: "false" script: - bundle exec rake assets:precompile - echo "{}" > knapsack_rspec_report.json @@ -62,57 +42,48 @@ prepare: - knapsack_rspec_report.json - knapsack_cucumber_report.json -spec 0 10: *rspec_tests -spec 1 10: *rspec_tests -spec 2 10: *rspec_tests -spec 3 10: *rspec_tests -spec 4 10: *rspec_tests -spec 5 10: *rspec_tests -spec 6 10: *rspec_tests -spec 7 10: *rspec_tests -spec 8 10: *rspec_tests -spec 9 10: *rspec_tests - -spinach 0 10: *spinach_tests -spinach 1 10: *spinach_tests -spinach 2 10: *spinach_tests -spinach 3 10: *spinach_tests -spinach 4 10: *spinach_tests -spinach 5 10: *spinach_tests -spinach 6 10: *spinach_tests -spinach 7 10: *spinach_tests -spinach 8 10: *spinach_tests -spinach 9 10: *spinach_tests - -teaspoon: +.knapsack: &knapsack stage: test script: - - bundle exec teaspoon + - JOB_NAME=( $CI_BUILD_NAME ) + - export CI_NODE_INDEX=${JOB_NAME[1]} + - export CI_NODE_TOTAL=${JOB_NAME[2]} + - bundle exec rake knapsack:${JOB_NAME[0]} -rubocop: +.exec: &exec stage: test script: - - bundle exec rubocop + - bundle exec $CI_BUILD_NAME -scss-lint: - stage: test - script: - - bundle exec rake scss_lint +rspec 0 10: *knapsack +rspec 1 10: *knapsack +rspec 2 10: *knapsack +rspec 3 10: *knapsack +rspec 4 10: *knapsack +rspec 5 10: *knapsack +rspec 6 10: *knapsack +rspec 7 10: *knapsack +rspec 8 10: *knapsack +rspec 9 10: *knapsack -brakeman: - stage: test - script: - - bundle exec rake brakeman +spinach 0 10: *knapsack +spinach 1 10: *knapsack +spinach 2 10: *knapsack +spinach 3 10: *knapsack +spinach 4 10: *knapsack +spinach 5 10: *knapsack +spinach 6 10: *knapsack +spinach 7 10: *knapsack +spinach 8 10: *knapsack +spinach 9 10: *knapsack -flog: - stage: test - script: - - bundle exec rake flog - -flay: - stage: test - script: - - bundle exec rake flay +teaspoon: *exec +rubocop: *exec +rake scss_lint: *exec +rake brakeman: *exec +rake flog: *exec +rake flay: *exec +rake db:migrate:reset: *exec bundler:audit: stage: test @@ -121,11 +92,6 @@ bundler:audit: script: - "bundle exec bundle-audit check --update --ignore OSVDB-115941" -db-migrate-reset: - stage: test - script: - - bundle exec rake db:migrate:reset - # Ruby 2.2 jobs spec:feature:ruby22: From e77c7116cc83775c0f20f0c106b92a56681208a2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 20:52:19 -0500 Subject: [PATCH 158/507] Fix assets --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dc81a69855..99ec069a53 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,7 @@ prepare: - echo "{}" > knapsack_cucumber_report.json artifacts: paths: - - assets/public/ + - public/assets/ - knapsack_rspec_report.json - knapsack_cucumber_report.json From f768d2ccd4506be132bf6903a22d5f5748dec24b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 21:04:26 -0500 Subject: [PATCH 159/507] add bundler --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 99ec069a53..3896c37c33 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ variables: before_script: - source ./scripts/prepare_build.sh - cp config/gitlab.yml.example config/gitlab.yml + - retry gem install bundler --no-ri --no-rdoc - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' From 1276bc6c069dca8410ec39ab880bf5a0b61eca9f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 23:34:54 -0500 Subject: [PATCH 160/507] Test --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3896c37c33..29359e935e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ variables: before_script: - source ./scripts/prepare_build.sh - cp config/gitlab.yml.example config/gitlab.yml - - retry gem install bundler --no-ri --no-rdoc + - retry gem install bundler - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' From 713bbed74e2d144d5e92969fd942621c166bf319 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 23:45:16 -0500 Subject: [PATCH 161/507] Test --- .gitlab-ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 29359e935e..20c86aa00f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,6 +29,11 @@ stages: - test - notifications +test-bundler: + stage: build + script: + - bundle exec bundle --version + prepare: stage: build variables: From 2494569d59a8f2fa4c61f62a06f0afe739635a6d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 23:57:25 -0500 Subject: [PATCH 162/507] Test --- .gitlab-ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 20c86aa00f..50233a2d8a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ variables: before_script: - source ./scripts/prepare_build.sh - cp config/gitlab.yml.example config/gitlab.yml - - retry gem install bundler + #- retry gem install bundler - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' @@ -31,7 +31,10 @@ stages: test-bundler: stage: build + variables: + USE_DB: "false" script: + - retry gem install bundler - bundle exec bundle --version prepare: From 3e61c8feb448bf44b025e7e7b02935b73ea52765 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 00:31:38 -0500 Subject: [PATCH 163/507] Test --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 50233a2d8a..2aaafdf0d9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ variables: before_script: - source ./scripts/prepare_build.sh - cp config/gitlab.yml.example config/gitlab.yml + - bundle --version #- retry gem install bundler - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' From 20dc6a708b4bad9507412e3a96c1c0bd25cf6fc4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 19:27:34 -0500 Subject: [PATCH 164/507] Use knapsack directly --- .gitlab-ci.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2aaafdf0d9..837aed3ae0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,6 @@ before_script: - source ./scripts/prepare_build.sh - cp config/gitlab.yml.example config/gitlab.yml - bundle --version - #- retry gem install bundler - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' @@ -30,20 +29,12 @@ stages: - test - notifications -test-bundler: - stage: build - variables: - USE_DB: "false" - script: - - retry gem install bundler - - bundle exec bundle --version - prepare: stage: build variables: USE_DB: "false" script: - - bundle exec rake assets:precompile + #- bundle exec rake assets:precompile - echo "{}" > knapsack_rspec_report.json - echo "{}" > knapsack_cucumber_report.json artifacts: @@ -58,7 +49,8 @@ prepare: - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} - - bundle exec rake knapsack:${JOB_NAME[0]} + - gem install knapsack + - knapsack ${JOB_NAME[0]} .exec: &exec stage: test From 8d8d2759efaba594ec010a030bed94785016e274 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 20:23:40 -0500 Subject: [PATCH 165/507] WIP --- .gitlab-ci.yml | 2 +- features/support/env.rb | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 837aed3ae0..a5065693b5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,7 +34,7 @@ prepare: variables: USE_DB: "false" script: - #- bundle exec rake assets:precompile + - bundle exec rake assets:precompile - echo "{}" > knapsack_rspec_report.json - echo "{}" > knapsack_cucumber_report.json artifacts: diff --git a/features/support/env.rb b/features/support/env.rb index 6ebd012a40..5a34159ec9 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -21,8 +21,6 @@ require_relative 'rerun' require Rails.root.join('spec', 'support', f) end -Knapsack::Adapters::CucumberAdapter.bind - Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } WebMock.allow_net_connect! From 77f8deec2ccbde0e502ecf35e1484f2c2e4e8a54 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 20:34:36 -0500 Subject: [PATCH 166/507] Use own version of knapsack which supports spinach tests --- Gemfile | 2 +- Gemfile.lock | 4 ++-- Rakefile | 2 +- features/support/env.rb | 4 +++- spec/spec_helper.rb | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index b9ae1aecb5..f4602008ec 100644 --- a/Gemfile +++ b/Gemfile @@ -313,7 +313,7 @@ group :test do gem 'webmock', '~> 1.21.0' gem 'test_after_commit', '~> 0.4.2' gem 'sham_rack' - gem 'knapsack' + gem 'knapsack-gitlab' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 930a0f3f8d..c04a3b1351 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -358,7 +358,7 @@ GEM actionpack (>= 3.0.0) activesupport (>= 3.0.0) kgio (2.10.0) - knapsack (1.9.0) + knapsack-gitlab (1.9.0) rake timecop (>= 0.1.0) launchy (2.4.3) @@ -878,7 +878,7 @@ DEPENDENCIES jquery-ui-rails (~> 5.0.0) jwt kaminari (~> 0.17.0) - knapsack + knapsack-gitlab letter_opener_web (~> 1.3.0) licensee (~> 8.0.0) loofah (~> 2.0.3) diff --git a/Rakefile b/Rakefile index 16261bf8ae..bcfcba634d 100755 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) -require 'knapsack' +require 'knapsack-gitlab' relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__) require relative_url_conf if File.exist?("#{relative_url_conf}.rb") diff --git a/features/support/env.rb b/features/support/env.rb index 5a34159ec9..b4b3e7359b 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -11,12 +11,14 @@ ENV['RAILS_ENV'] = 'test' require './config/environment' require 'rspec/expectations' require 'sidekiq/testing/inline' -require 'knapsack' +require 'knapsack-gitlab' require_relative 'capybara' require_relative 'db_cleaner' require_relative 'rerun' +Knapsack::Adapters::SpinachAdapter.bind + %w(select2_helper test_env repo_helpers).each do |f| require Rails.root.join('spec', 'support', f) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 84b9ee75f6..f6d8dd4224 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,7 +15,7 @@ require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'rspec/retry' -require 'knapsack' +require 'knapsack-gitlab' require_relative 'knapsack_merger' Knapsack::Adapters::RSpecAdapter.bind From e275b6d2510fc94ea32e94acb44429cfcec28543 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 20:40:56 -0500 Subject: [PATCH 167/507] fix yml --- .gitlab-ci.yml | 2 +- Rakefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a5065693b5..6113656462 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,6 +22,7 @@ before_script: - cp config/gitlab.yml.example config/gitlab.yml - bundle --version - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" + - retry gem install knapsack-gitlab - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' stages: @@ -49,7 +50,6 @@ prepare: - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} - - gem install knapsack - knapsack ${JOB_NAME[0]} .exec: &exec diff --git a/Rakefile b/Rakefile index bcfcba634d..16261bf8ae 100755 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) -require 'knapsack-gitlab' +require 'knapsack' relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__) require relative_url_conf if File.exist?("#{relative_url_conf}.rb") From 9df2613a4e22741f8c0dfd6ff053adc7b0ff7a69 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 21:52:21 -0500 Subject: [PATCH 168/507] Fix knapsack usage --- .gitlab-ci.yml | 2 +- Gemfile.lock | 2 +- Rakefile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6113656462..d573d80cdb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -50,7 +50,7 @@ prepare: - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} - - knapsack ${JOB_NAME[0]} + - knapsack-gitlab ${JOB_NAME[0]} .exec: &exec stage: test diff --git a/Gemfile.lock b/Gemfile.lock index c04a3b1351..0f75c231d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -358,7 +358,7 @@ GEM actionpack (>= 3.0.0) activesupport (>= 3.0.0) kgio (2.10.0) - knapsack-gitlab (1.9.0) + knapsack-gitlab (1.9.2) rake timecop (>= 0.1.0) launchy (2.4.3) diff --git a/Rakefile b/Rakefile index 16261bf8ae..bcfcba634d 100755 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) -require 'knapsack' +require 'knapsack-gitlab' relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__) require relative_url_conf if File.exist?("#{relative_url_conf}.rb") From fb404466b697ee9e0dc914710115c74718e614ab Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 22:03:19 -0500 Subject: [PATCH 169/507] Use spinach record --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d573d80cdb..3e63c4a45d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,12 +37,12 @@ prepare: script: - bundle exec rake assets:precompile - echo "{}" > knapsack_rspec_report.json - - echo "{}" > knapsack_cucumber_report.json + - echo "{}" > knapsack_spinach_report.json artifacts: paths: - public/assets/ - knapsack_rspec_report.json - - knapsack_cucumber_report.json + - knapsack_spinach_report.json .knapsack: &knapsack stage: test From 2defc128bc46d327a19763ce72bdb90d26e27d8f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 22:07:22 -0500 Subject: [PATCH 170/507] Use more concurrency --- .gitlab-ci.yml | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3e63c4a45d..8b5db89c94 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -57,16 +57,26 @@ prepare: script: - bundle exec $CI_BUILD_NAME -rspec 0 10: *knapsack -rspec 1 10: *knapsack -rspec 2 10: *knapsack -rspec 3 10: *knapsack -rspec 4 10: *knapsack -rspec 5 10: *knapsack -rspec 6 10: *knapsack -rspec 7 10: *knapsack -rspec 8 10: *knapsack -rspec 9 10: *knapsack +rspec 0 20: *knapsack +rspec 1 20: *knapsack +rspec 2 20: *knapsack +rspec 3 20: *knapsack +rspec 4 20: *knapsack +rspec 5 20: *knapsack +rspec 6 20: *knapsack +rspec 7 20: *knapsack +rspec 8 20: *knapsack +rspec 9 20: *knapsack +rspec 10 20: *knapsack +rspec 11 20: *knapsack +rspec 12 20: *knapsack +rspec 13 20: *knapsack +rspec 14 20: *knapsack +rspec 15 20: *knapsack +rspec 16 20: *knapsack +rspec 17 20: *knapsack +rspec 18 20: *knapsack +rspec 19 20: *knapsack spinach 0 10: *knapsack spinach 1 10: *knapsack From 432b96640fb2955d118f43ae2463347745cb8386 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 22:39:21 -0500 Subject: [PATCH 171/507] More spinaches --- .gitlab-ci.yml | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8b5db89c94..d0bce69cf5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -78,16 +78,26 @@ rspec 17 20: *knapsack rspec 18 20: *knapsack rspec 19 20: *knapsack -spinach 0 10: *knapsack -spinach 1 10: *knapsack -spinach 2 10: *knapsack -spinach 3 10: *knapsack -spinach 4 10: *knapsack -spinach 5 10: *knapsack -spinach 6 10: *knapsack -spinach 7 10: *knapsack -spinach 8 10: *knapsack -spinach 9 10: *knapsack +spinach 0 20: *knapsack +spinach 1 20: *knapsack +spinach 2 20: *knapsack +spinach 3 20: *knapsack +spinach 4 20: *knapsack +spinach 5 20: *knapsack +spinach 6 20: *knapsack +spinach 7 20: *knapsack +spinach 8 20: *knapsack +spinach 9 20: *knapsack +spinach 10 20: *knapsack +spinach 11 20: *knapsack +spinach 12 20: *knapsack +spinach 13 20: *knapsack +spinach 14 20: *knapsack +spinach 15 20: *knapsack +spinach 16 20: *knapsack +spinach 17 20: *knapsack +spinach 18 20: *knapsack +spinach 19 20: *knapsack teaspoon: *exec rubocop: *exec From 1fab583266af0904dfc29facfe4551e37c06342a Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 3 Jun 2016 17:08:44 +0200 Subject: [PATCH 172/507] Remove instances of Auth.new --- app/controllers/jwt_controller.rb | 2 +- lib/gitlab/backend/grack_auth.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index cee3b6c43e..c05a55633b 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -45,7 +45,7 @@ class JwtController < ApplicationController # TODO: this is a copy and paste from grack_auth, # it should be refactored in the future - user = Gitlab::Auth.new.find(login, password) + user = Gitlab::Auth.find_in_gitlab_or_ldap(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 diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 492ffb138a..9e09d2e118 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -95,7 +95,7 @@ module Grack end def authenticate_user(login, password) - user = Gitlab::Auth.new.find_in_gitlab_or_ldap(login, password) + user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password) unless user user = oauth_access_token_check(login, password) From 03bec6b0e943a3a047fd8f6185f71a976c02506c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 3 Jun 2016 17:14:13 +0200 Subject: [PATCH 173/507] Argh mixed up all the negatives --- lib/gitlab/auth/rate_limiter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/auth/rate_limiter.rb b/lib/gitlab/auth/rate_limiter.rb index 4be9f6d0ef..1089bc9f89 100644 --- a/lib/gitlab/auth/rate_limiter.rb +++ b/lib/gitlab/auth/rate_limiter.rb @@ -20,7 +20,7 @@ module Gitlab # Allow2Ban.filter will return false if this IP has not failed too often yet @banned = Rack::Attack::Allow2Ban.filter(ip, config) do # If we return false here, the failure for this IP is ignored by Allow2Ban - ignore_failure? + ip_can_be_banned? end end @@ -34,7 +34,7 @@ module Gitlab Gitlab.config.rack_attack.git_basic_auth end - def ignore_failure? + def ip_can_be_banned? config.ip_whitelist.exclude?(ip) end end From 95c3a927b319c8495c28e6431152e5ca0c5df30b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 17:15:00 +0200 Subject: [PATCH 174/507] Use knapsack 1.11.0 --- .gitlab-ci.yml | 21 +++++++++++++-------- Gemfile | 2 +- Gemfile.lock | 4 ++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d0bce69cf5..cf85820abd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,27 +22,27 @@ before_script: - cp config/gitlab.yml.example config/gitlab.yml - bundle --version - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - - retry gem install knapsack-gitlab + - retry gem install knapsack - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' stages: -- build +- prepare - test - notifications prepare: - stage: build + stage: prepare variables: USE_DB: "false" script: - bundle exec rake assets:precompile - - echo "{}" > knapsack_rspec_report.json - - echo "{}" > knapsack_spinach_report.json + - mkdir knapsack/ + - echo "{}" > knapsack/rspec_report.json + - echo "{}" > knapsack/spinach_report.json artifacts: paths: - public/assets/ - - knapsack_rspec_report.json - - knapsack_spinach_report.json + - knapsack/ .knapsack: &knapsack stage: test @@ -50,7 +50,12 @@ prepare: - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} - - knapsack-gitlab ${JOB_NAME[0]} + - export KNAPSACK_REPORT_PATH=knapsack/${JOB_NAME}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - cp knapsack/${JOB_NAME}_report.json ${KNAPSACK_REPORT_PATH} + - knapsack ${JOB_NAME[0]} + artifacts: + paths: + - knapsack/ .exec: &exec stage: test diff --git a/Gemfile b/Gemfile index f4602008ec..b9ae1aecb5 100644 --- a/Gemfile +++ b/Gemfile @@ -313,7 +313,7 @@ group :test do gem 'webmock', '~> 1.21.0' gem 'test_after_commit', '~> 0.4.2' gem 'sham_rack' - gem 'knapsack-gitlab' + gem 'knapsack' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 0f75c231d4..8bfdf8ea9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -358,7 +358,7 @@ GEM actionpack (>= 3.0.0) activesupport (>= 3.0.0) kgio (2.10.0) - knapsack-gitlab (1.9.2) + knapsack (1.11.0) rake timecop (>= 0.1.0) launchy (2.4.3) @@ -878,7 +878,7 @@ DEPENDENCIES jquery-ui-rails (~> 5.0.0) jwt kaminari (~> 0.17.0) - knapsack-gitlab + knapsack letter_opener_web (~> 1.3.0) licensee (~> 8.0.0) loofah (~> 2.0.3) From 3f3b036defe4f22656f945f341c1d7da06d5543c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 3 Jun 2016 17:23:34 +0200 Subject: [PATCH 175/507] Use public_send --- lib/gitlab/auth.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index dd6ba84c97..bd129d7216 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -49,8 +49,7 @@ module Gitlab elsif Service.available_services_names.include?(underscored_service) # We treat underscored_service as a trusted input because it is included # in the Service.available_services_names whitelist. - service_method = "#{underscored_service}_service" - service = project.send(service_method) + service = project.public_send("#{underscored_service}_service") service && service.activated? && service.valid_token?(password) end From 792670f4ce467f278b5cdfd609a221bbbe26187d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 17:35:00 +0200 Subject: [PATCH 176/507] Merge knapsack reports and upload them to external server --- .gitlab-ci.yml | 19 ++++++++++++++++++- scripts/merge-reports | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100755 scripts/merge-reports diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cf85820abd..6fc410b4b5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -28,6 +28,7 @@ before_script: stages: - prepare - test +- post-test - notifications prepare: @@ -36,7 +37,7 @@ prepare: USE_DB: "false" script: - bundle exec rake assets:precompile - - mkdir knapsack/ + - mkdir -p knapsack/ - echo "{}" > knapsack/rspec_report.json - echo "{}" > knapsack/spinach_report.json artifacts: @@ -57,6 +58,22 @@ prepare: paths: - knapsack/ +post-tests: + stage: post-test + variables: + USE_DB: "false" + script: + - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json + - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json + - rm -f knapsack/*_node_*.json + cache: + key: "knapsack" + paths: + - knapsack/ + artifacts: + paths: + - knapsack/ + .exec: &exec stage: test script: diff --git a/scripts/merge-reports b/scripts/merge-reports new file mode 100755 index 0000000000..f7b574001a --- /dev/null +++ b/scripts/merge-reports @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby + +require 'json' +require 'yaml' + +main_report_file = ARGV.shift +unless main_report_file + puts 'usage: merge_reports [extra reports...]' + exit 1 +end + +puts "Loading #{main_report_file}..." +main_report = JSON.parse(File.read(main_report_file)) +new_report = main_report.dup + +ARGV.each do |report_file| + report = JSON.parse(File.read(report_file)) + + # Remove existing values + updates = report.delete_if do |key, value| + main_report[key] && main_report[key] == value + end + new_report.merge!(updates) + + puts "Merged #{report_file} adding #{updates.size} results." +end + +File.write(main_report_file, JSON.pretty_generate(new_report)) +puts "Saved #{main_report_file}." From 86498d4d99472edcb70ab1631f463133995a8cbf Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 17:39:53 +0200 Subject: [PATCH 177/507] Use knapsack everywhere --- Rakefile | 2 +- features/support/env.rb | 2 +- spec/spec_helper.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Rakefile b/Rakefile index bcfcba634d..16261bf8ae 100755 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) -require 'knapsack-gitlab' +require 'knapsack' relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__) require relative_url_conf if File.exist?("#{relative_url_conf}.rb") diff --git a/features/support/env.rb b/features/support/env.rb index b4b3e7359b..4552db8ad7 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -11,7 +11,7 @@ ENV['RAILS_ENV'] = 'test' require './config/environment' require 'rspec/expectations' require 'sidekiq/testing/inline' -require 'knapsack-gitlab' +require 'knapsack' require_relative 'capybara' require_relative 'db_cleaner' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f6d8dd4224..84b9ee75f6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,7 +15,7 @@ require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'rspec/retry' -require 'knapsack-gitlab' +require 'knapsack' require_relative 'knapsack_merger' Knapsack::Adapters::RSpecAdapter.bind From 903946c78a0f73e5cbdfce7b93d31c4d1bd045cd Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 1 Jun 2016 16:37:15 -0600 Subject: [PATCH 178/507] Replace colorize gem with rainbow. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colorize is a gem licensed under the GPLv2, so we can’t use it in GitLab without relicensing GitLab under the terms of the GPL. Rainbow is licensed under the MIT license and does the exact same thing as Colorize, so Rainbow was added in place of Colorize. The syntax is slightly different for Rainbow vs. Colorize, and was updated in accordance. The gem is still a dependency of Spinach, so it’s included in the development/test environments, but won’t be packaged with the actual product, and therefore doesn’t require we relicense the product. An attempt at relicensing Colorize was made, but didn’t succeed as the library owner never responded. Rainbow library: https://github.com/sickill/rainbow Relevant issue regarding licensing in GitLab's gems: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3775 --- Gemfile | 2 +- Gemfile.lock | 2 +- lib/backup/database.rb | 4 +- lib/backup/manager.rb | 30 ++-- lib/backup/repository.rb | 26 ++-- lib/gitlab/seeder.rb | 2 +- lib/tasks/gitlab/backup.rake | 80 +++++----- lib/tasks/gitlab/check.rake | 178 +++++++++++----------- lib/tasks/gitlab/cleanup.rake | 18 +-- lib/tasks/gitlab/db.rake | 8 +- lib/tasks/gitlab/git.rake | 8 +- lib/tasks/gitlab/import.rake | 14 +- lib/tasks/gitlab/info.rake | 26 ++-- lib/tasks/gitlab/shell.rake | 4 +- lib/tasks/gitlab/task_helpers.rake | 10 +- lib/tasks/gitlab/two_factor.rake | 8 +- lib/tasks/gitlab/update_commit_count.rake | 6 +- lib/tasks/gitlab/update_gitignore.rake | 4 +- lib/tasks/gitlab/web_hook.rake | 6 +- lib/tasks/migrate/migrate_iids.rake | 6 +- lib/tasks/spinach.rake | 2 +- 21 files changed, 222 insertions(+), 222 deletions(-) diff --git a/Gemfile b/Gemfile index d9429de786..a50d7e632a 100644 --- a/Gemfile +++ b/Gemfile @@ -143,7 +143,7 @@ gem 'redis-namespace' gem "httparty", '~> 0.13.3' # Colored output to console -gem "colorize", '~> 0.7.0' +gem "rainbow", '~> 2.1.0' # GitLab settings gem 'settingslogic', '~> 2.0.9' diff --git a/Gemfile.lock b/Gemfile.lock index 8ae25269e6..1771b919b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -823,7 +823,6 @@ DEPENDENCIES carrierwave (~> 0.10.0) charlock_holmes (~> 0.7.3) coffee-rails (~> 4.1.0) - colorize (~> 0.7.0) connection_pool (~> 2.0) coveralls (~> 0.8.2) creole (~> 0.5.0) @@ -914,6 +913,7 @@ DEPENDENCIES rack-oauth2 (~> 1.2.1) rails (= 4.2.6) rails-deprecated_sanitizer (~> 1.0.3) + rainbow (~> 2.1.0) raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 67b2a64bd1..22319ec662 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -86,9 +86,9 @@ module Backup def report_success(success) if success - $progress.puts '[DONE]'.green + $progress.puts '[DONE]'.color(:green) else - $progress.puts '[FAILED]'.red + $progress.puts '[FAILED]'.color(:red) end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 660ca8c292..9dd665441a 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -27,9 +27,9 @@ module Backup # Set file permissions on open to prevent chmod races. tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]} if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options) - $progress.puts "done".green + $progress.puts "done".color(:green) else - puts "creating archive #{tar_file} failed".red + puts "creating archive #{tar_file} failed".color(:red) abort 'Backup failed' end @@ -43,7 +43,7 @@ module Backup connection_settings = Gitlab.config.backup.upload.connection if connection_settings.blank? - $progress.puts "skipped".yellow + $progress.puts "skipped".color(:yellow) return end @@ -53,9 +53,9 @@ module Backup if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, encryption: Gitlab.config.backup.upload.encryption) - $progress.puts "done".green + $progress.puts "done".color(:green) else - puts "uploading backup to #{remote_directory} failed".red + puts "uploading backup to #{remote_directory} failed".color(:red) abort 'Backup failed' end end @@ -67,9 +67,9 @@ module Backup next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir)) - $progress.puts "done".green + $progress.puts "done".color(:green) else - puts "deleting tmp directory '#{dir}' failed".red + puts "deleting tmp directory '#{dir}' failed".color(:red) abort 'Backup failed' end end @@ -95,9 +95,9 @@ module Backup end end - $progress.puts "done. (#{removed} removed)".green + $progress.puts "done. (#{removed} removed)".color(:green) else - $progress.puts "skipping".yellow + $progress.puts "skipping".color(:yellow) end end @@ -124,20 +124,20 @@ module Backup $progress.print "Unpacking backup ... " unless Kernel.system(*%W(tar -xf #{tar_file})) - puts "unpacking backup failed".red + puts "unpacking backup failed".color(:red) exit 1 else - $progress.puts "done".green + $progress.puts "done".color(:green) end ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 # restoring mismatching backups can lead to unexpected problems if settings[:gitlab_version] != Gitlab::VERSION - puts "GitLab version mismatch:".red - puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".red - puts " Please switch to the following version and try again:".red - puts " version: #{settings[:gitlab_version]}".red + puts "GitLab version mismatch:".color(:red) + puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red) + puts " Please switch to the following version and try again:".color(:red) + puts " version: #{settings[:gitlab_version]}".color(:red) puts puts "Hint: git checkout v#{settings[:gitlab_version]}" exit 1 diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index a82a7e1f7b..7b91215d50 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -14,14 +14,14 @@ module Backup FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace if project.empty_repo? - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .) output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts "[DONE]".green + $progress.puts "[DONE]".color(:green) else - puts "[FAILED]".red + puts "[FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" puts output abort 'Backup failed' @@ -33,14 +33,14 @@ module Backup if File.exists?(path_to_repo(wiki)) $progress.print " * #{wiki.path_with_namespace} ... " if wiki.repository.empty? - $progress.puts " [SKIPPED]".cyan + $progress.puts " [SKIPPED]".color(:cyan) else cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all) output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts " [DONE]".green + $progress.puts " [DONE]".color(:green) else - puts " [FAILED]".red + puts " [FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" abort 'Backup failed' end @@ -71,9 +71,9 @@ module Backup end if system(*cmd, silent) - $progress.puts "[DONE]".green + $progress.puts "[DONE]".color(:green) else - puts "[FAILED]".red + puts "[FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end @@ -90,21 +90,21 @@ module Backup cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) if system(*cmd, silent) - $progress.puts " [DONE]".green + $progress.puts " [DONE]".color(:green) else - puts " [FAILED]".red + puts " [FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end end end - $progress.print 'Put GitLab hooks in repositories dirs'.yellow + $progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow) cmd = "#{Gitlab.config.gitlab_shell.path}/bin/create-hooks" if system(cmd) - $progress.puts " [DONE]".green + $progress.puts " [DONE]".color(:green) else - puts " [FAILED]".red + puts " [FAILED]".color(:red) puts "failed: #{cmd}" end diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index 2ef0e98225..7cf506ebe6 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -5,7 +5,7 @@ module Gitlab SeedFu.quiet = true yield SeedFu.quiet = false - puts "\nOK".green + puts "\nOK".color(:green) end def self.by_user(user) diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 596eaca6d0..9ee72fde92 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -40,14 +40,14 @@ namespace :gitlab do removed. MSG ask_to_continue - puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.yellow + puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow) sleep(5) end # Drop all tables Load the schema to ensure we don't have any newer tables # hanging out from a failed upgrade - $progress.puts 'Cleaning the database ... '.blue + $progress.puts 'Cleaning the database ... '.color(:blue) Rake::Task['gitlab:db:drop_tables'].invoke - $progress.puts 'done'.green + $progress.puts 'done'.color(:green) Rake::Task['gitlab:backup:db:restore'].invoke end Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories') @@ -63,141 +63,141 @@ namespace :gitlab do namespace :repo do task create: :environment do - $progress.puts "Dumping repositories ...".blue + $progress.puts "Dumping repositories ...".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("repositories") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Repository.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring repositories ...".blue + $progress.puts "Restoring repositories ...".color(:blue) Backup::Repository.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :db do task create: :environment do - $progress.puts "Dumping database ... ".blue + $progress.puts "Dumping database ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("db") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Database.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring database ... ".blue + $progress.puts "Restoring database ... ".color(:blue) Backup::Database.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :builds do task create: :environment do - $progress.puts "Dumping builds ... ".blue + $progress.puts "Dumping builds ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("builds") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Builds.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring builds ... ".blue + $progress.puts "Restoring builds ... ".color(:blue) Backup::Builds.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :uploads do task create: :environment do - $progress.puts "Dumping uploads ... ".blue + $progress.puts "Dumping uploads ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("uploads") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Uploads.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring uploads ... ".blue + $progress.puts "Restoring uploads ... ".color(:blue) Backup::Uploads.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :artifacts do task create: :environment do - $progress.puts "Dumping artifacts ... ".blue + $progress.puts "Dumping artifacts ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("artifacts") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Artifacts.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring artifacts ... ".blue + $progress.puts "Restoring artifacts ... ".color(:blue) Backup::Artifacts.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :lfs do task create: :environment do - $progress.puts "Dumping lfs objects ... ".blue + $progress.puts "Dumping lfs objects ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("lfs") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Lfs.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring lfs objects ... ".blue + $progress.puts "Restoring lfs objects ... ".color(:blue) Backup::Lfs.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :registry do task create: :environment do - $progress.puts "Dumping container registry images ... ".blue + $progress.puts "Dumping container registry images ... ".color(:blue) if Gitlab.config.registry.enabled if ENV["SKIP"] && ENV["SKIP"].include?("registry") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Registry.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end else - $progress.puts "[DISABLED]".cyan + $progress.puts "[DISABLED]".color(:cyan) end end task restore: :environment do - $progress.puts "Restoring container registry images ... ".blue + $progress.puts "Restoring container registry images ... ".color(:blue) if Gitlab.config.registry.enabled Backup::Registry.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) else - $progress.puts "[DISABLED]".cyan + $progress.puts "[DISABLED]".color(:cyan) end end end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index fad89c7376..12d6ac45fb 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -50,14 +50,14 @@ namespace :gitlab do end if correct_options.all? - puts "yes".green + puts "yes".color(:green) else print "Trying to fix Git error automatically. ..." if auto_fix_git_config(options) - puts "Success".green + puts "Success".color(:green) else - puts "Failed".red + puts "Failed".color(:red) try_fixing_it( sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") ) @@ -74,9 +74,9 @@ namespace :gitlab do database_config_file = Rails.root.join("config", "database.yml") if File.exists?(database_config_file) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Copy config/database.yml. to config/database.yml", "Check that the information in config/database.yml is correct" @@ -95,9 +95,9 @@ namespace :gitlab do gitlab_config_file = Rails.root.join("config", "gitlab.yml") if File.exists?(gitlab_config_file) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Copy config/gitlab.yml.example to config/gitlab.yml", "Update config/gitlab.yml to match your setup" @@ -114,14 +114,14 @@ namespace :gitlab do gitlab_config_file = Rails.root.join("config", "gitlab.yml") unless File.exists?(gitlab_config_file) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) end # omniauth or ldap could have been deleted from the file unless Gitlab.config['git_host'] - puts "no".green + puts "no".color(:green) else - puts "yes".red + puts "yes".color(:red) try_fixing_it( "Backup your config/gitlab.yml", "Copy config/gitlab.yml.example to config/gitlab.yml", @@ -138,16 +138,16 @@ namespace :gitlab do print "Init script exists? ... " if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.magenta + puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) return end script_path = "/etc/init.d/gitlab" if File.exists?(script_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Install the init script" ) @@ -162,7 +162,7 @@ namespace :gitlab do print "Init script up-to-date? ... " if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.magenta + puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) return end @@ -170,7 +170,7 @@ namespace :gitlab do script_path = "/etc/init.d/gitlab" unless File.exists?(script_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end @@ -178,9 +178,9 @@ namespace :gitlab do script_content = File.read(script_path) if recipe_content == script_content - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Redownload the init script" ) @@ -197,9 +197,9 @@ namespace :gitlab do migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status)) unless migration_status =~ /down\s+\d{14}/ - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( sudo_gitlab("bundle exec rake db:migrate RAILS_ENV=production") ) @@ -210,13 +210,13 @@ namespace :gitlab do def check_orphaned_group_members print "Database contains orphaned GroupMembers? ... " if GroupMember.where("user_id not in (select id from users)").count > 0 - puts "yes".red + puts "yes".color(:red) try_fixing_it( "You can delete the orphaned records using something along the lines of:", sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'") ) else - puts "no".green + puts "no".color(:green) end end @@ -226,9 +226,9 @@ namespace :gitlab do log_path = Rails.root.join("log") if File.writable?(log_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chown -R gitlab #{log_path}", "sudo chmod -R u+rwX #{log_path}" @@ -246,9 +246,9 @@ namespace :gitlab do tmp_path = Rails.root.join("tmp") if File.writable?(tmp_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chown -R gitlab #{tmp_path}", "sudo chmod -R u+rwX #{tmp_path}" @@ -264,7 +264,7 @@ namespace :gitlab do print "Uploads directory setup correctly? ... " unless File.directory?(Rails.root.join('public/uploads')) - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads" ) @@ -280,16 +280,16 @@ namespace :gitlab do if File.stat(upload_path).mode == 040700 unless Dir.exists?(upload_path_tmp) - puts 'skipped (no tmp uploads folder yet)'.magenta + puts 'skipped (no tmp uploads folder yet)'.color(:magenta) return end # If tmp upload dir has incorrect permissions, assume others do as well # Verify drwx------ permissions if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chown -R #{gitlab_user} #{upload_path}", "sudo find #{upload_path} -type f -exec chmod 0644 {} \\;", @@ -301,7 +301,7 @@ namespace :gitlab do fix_and_rerun end else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chmod 700 #{upload_path}" ) @@ -320,9 +320,9 @@ namespace :gitlab do redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/) if redis_version && (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version)) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Update your redis server to a version >= #{min_redis_version}" ) @@ -361,10 +361,10 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path if File.exists?(repo_base_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red - puts "#{repo_base_path} is missing".red + puts "no".color(:red) + puts "#{repo_base_path} is missing".color(:red) try_fixing_it( "This should have been created when setting up GitLab Shell.", "Make sure it's set correctly in config/gitlab.yml", @@ -382,14 +382,14 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path unless File.exists?(repo_base_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end unless File.symlink?(repo_base_path) - puts "no".green + puts "no".color(:green) else - puts "yes".red + puts "yes".color(:red) try_fixing_it( "Make sure it's set to the real directory in config/gitlab.yml" ) @@ -402,14 +402,14 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path unless File.exists?(repo_base_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770") - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}", "sudo chmod -R ug-s #{repo_base_path}", @@ -429,17 +429,17 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path unless File.exists?(repo_base_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end uid = uid_for(gitlab_shell_ssh_user) gid = gid_for(gitlab_shell_owner_group) if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid - puts "yes".green + puts "yes".color(:green) else - puts "no".red - puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".blue + puts "no".color(:red) + puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".color(:blue) try_fixing_it( "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}" ) @@ -456,7 +456,7 @@ namespace :gitlab do gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path unless Project.count > 0 - puts "can't check, you have no projects".magenta + puts "can't check, you have no projects".color(:magenta) return end puts "" @@ -466,12 +466,12 @@ namespace :gitlab do project_hook_directory = File.join(project.repository.path_to_repo, "hooks") if project.empty_repo? - puts "repository is empty".magenta + puts "repository is empty".color(:magenta) elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) && (File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path)) - puts 'ok'.green + puts 'ok'.color(:green) else - puts "wrong or missing hooks".red + puts "wrong or missing hooks".color(:red) try_fixing_it( sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')}"), 'Check the hooks_path in config/gitlab.yml', @@ -491,9 +491,9 @@ namespace :gitlab do check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base) puts "Running #{check_cmd}" if system(check_cmd, chdir: gitlab_shell_repo_base) - puts 'gitlab-shell self-check successful'.green + puts 'gitlab-shell self-check successful'.color(:green) else - puts 'gitlab-shell self-check failed'.red + puts 'gitlab-shell self-check failed'.color(:red) try_fixing_it( 'Make sure GitLab is running;', 'Check the gitlab-shell configuration file:', @@ -507,7 +507,7 @@ namespace :gitlab do print "projects have namespace: ... " unless Project.count > 0 - puts "can't check, you have no projects".magenta + puts "can't check, you have no projects".color(:magenta) return end puts "" @@ -516,9 +516,9 @@ namespace :gitlab do print sanitized_message(project) if project.namespace - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Migrate global projects" ) @@ -576,9 +576,9 @@ namespace :gitlab do print "Running? ... " if sidekiq_process_count > 0 - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( sudo_gitlab("RAILS_ENV=production bin/background_jobs start") ) @@ -596,9 +596,9 @@ namespace :gitlab do print 'Number of Sidekiq processes ... ' if process_count == 1 - puts '1'.green + puts '1'.color(:green) else - puts "#{process_count}".red + puts "#{process_count}".color(:red) try_fixing_it( 'sudo service gitlab stop', "sudo pkill -u #{gitlab_user} -f sidekiq", @@ -646,16 +646,16 @@ namespace :gitlab do print "Init.d configured correctly? ... " if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.magenta + puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) return end path = "/etc/default/gitlab" if File.exist?(path) && File.read(path).include?("mail_room_enabled=true") - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Enable mail_room in the init.d configuration." ) @@ -672,9 +672,9 @@ namespace :gitlab do path = Rails.root.join("Procfile") if File.exist?(path) && File.read(path) =~ /^mail_room:/ - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Enable mail_room in your Procfile." ) @@ -691,14 +691,14 @@ namespace :gitlab do path = "/etc/default/gitlab" unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true") - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end if mail_room_running? - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( sudo_gitlab("RAILS_ENV=production bin/mail_room start") ) @@ -729,9 +729,9 @@ namespace :gitlab do end if connected - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Check that the information in config/gitlab.yml is correct" ) @@ -799,7 +799,7 @@ namespace :gitlab do namespace :user do desc "GitLab | Check the integrity of a specific user's repositories" task :check_repos, [:username] => :environment do |t, args| - username = args[:username] || prompt("Check repository integrity for which username? ".blue) + username = args[:username] || prompt("Check repository integrity for which username? ".color(:blue)) user = User.find_by(username: username) if user repo_dirs = user.authorized_projects.map do |p| @@ -811,7 +811,7 @@ namespace :gitlab do repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) } else - puts "\nUser '#{username}' not found".red + puts "\nUser '#{username}' not found".color(:red) end end end @@ -820,13 +820,13 @@ namespace :gitlab do ########################## def fix_and_rerun - puts " Please #{"fix the error above"} and rerun the checks.".red + puts " Please #{"fix the error above"} and rerun the checks.".color(:red) end def for_more_information(*sources) sources = sources.shift if sources.first.is_a?(Array) - puts " For more information see:".blue + puts " For more information see:".color(:blue) sources.each do |source| puts " #{source}" end @@ -834,7 +834,7 @@ namespace :gitlab do def finished_checking(component) puts "" - puts "Checking #{component.yellow} ... #{"Finished".green}" + puts "Checking #{component.color(:yellow)} ... #{"Finished".color(:green)}" puts "" end @@ -855,14 +855,14 @@ namespace :gitlab do end def start_checking(component) - puts "Checking #{component.yellow} ..." + puts "Checking #{component.color(:yellow)} ..." puts "" end def try_fixing_it(*steps) steps = steps.shift if steps.first.is_a?(Array) - puts " Try fixing it:".blue + puts " Try fixing it:".color(:blue) steps.each do |step| puts " #{step}" end @@ -874,9 +874,9 @@ namespace :gitlab do print "GitLab Shell version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "OK (#{current_version})".green + puts "OK (#{current_version})".color(:green) else - puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".red + puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red) end end @@ -887,9 +887,9 @@ namespace :gitlab do print "Ruby version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Update your ruby to a version >= #{required_version} from #{current_version}" ) @@ -905,9 +905,9 @@ namespace :gitlab do print "Git version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Update your git to a version >= #{required_version} from #{current_version}" ) @@ -925,9 +925,9 @@ namespace :gitlab do def sanitized_message(project) if should_sanitize? - "#{project.namespace_id.to_s.yellow}/#{project.id.to_s.yellow} ... " + "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... " else - "#{project.name_with_namespace.yellow} ... " + "#{project.name_with_namespace.color(:yellow)} ... " end end @@ -940,7 +940,7 @@ namespace :gitlab do end def check_repo_integrity(repo_dir) - puts "\nChecking repo at #{repo_dir.yellow}" + puts "\nChecking repo at #{repo_dir.color(:yellow)}" git_fsck(repo_dir) check_config_lock(repo_dir) @@ -948,25 +948,25 @@ namespace :gitlab do end def git_fsck(repo_dir) - puts "Running `git fsck`".yellow + puts "Running `git fsck`".color(:yellow) system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir) end def check_config_lock(repo_dir) config_exists = File.exist?(File.join(repo_dir,'config.lock')) - config_output = config_exists ? 'yes'.red : 'no'.green - puts "'config.lock' file exists?".yellow + " ... #{config_output}" + config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green) + puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}" end def check_ref_locks(repo_dir) lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock')) if lock_files.present? - puts "Ref lock files exist:".red + puts "Ref lock files exist:".color(:red) lock_files.each do |lock_file| puts " #{lock_file}" end else - puts "No ref lock files exist".green + puts "No ref lock files exist".color(:green) end end end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 9f5852ac61..ab0028d660 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -10,7 +10,7 @@ namespace :gitlab do git_base_path = Gitlab.config.gitlab_shell.repos_path all_dirs = Dir.glob(git_base_path + '/*') - puts git_base_path.yellow + puts git_base_path.color(:yellow) puts "Looking for directories to remove... " all_dirs.reject! do |dir| @@ -29,17 +29,17 @@ namespace :gitlab do if remove_flag if FileUtils.rm_rf dir_path - puts "Removed...#{dir_path}".red + puts "Removed...#{dir_path}".color(:red) else - puts "Cannot remove #{dir_path}".red + puts "Cannot remove #{dir_path}".color(:red) end else - puts "Can be removed: #{dir_path}".red + puts "Can be removed: #{dir_path}".color(:red) end end unless remove_flag - puts "To cleanup this directories run this command with REMOVE=true".yellow + puts "To cleanup this directories run this command with REMOVE=true".color(:yellow) end end @@ -75,19 +75,19 @@ namespace :gitlab do next unless user.ldap_user? print "#{user.name} (#{user.ldap_identity.extern_uid}) ..." if Gitlab::LDAP::Access.allowed?(user) - puts " [OK]".green + puts " [OK]".color(:green) else if block_flag user.block! unless user.blocked? - puts " [BLOCKED]".red + puts " [BLOCKED]".color(:red) else - puts " [NOT IN LDAP]".yellow + puts " [NOT IN LDAP]".color(:yellow) end end end unless block_flag - puts "To block these users run this command with BLOCK=true".yellow + puts "To block these users run this command with BLOCK=true".color(:yellow) end end end diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 86f5d65f12..86584e9109 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -3,22 +3,22 @@ namespace :gitlab do desc 'GitLab | Manually insert schema migration version' task :mark_migration_complete, [:version] => :environment do |_, args| unless args[:version] - puts "Must specify a migration version as an argument".red + puts "Must specify a migration version as an argument".color(:red) exit 1 end version = args[:version].to_i if version == 0 - puts "Version '#{args[:version]}' must be a non-zero integer".red + puts "Version '#{args[:version]}' must be a non-zero integer".color(:red) exit 1 end sql = "INSERT INTO schema_migrations (version) VALUES (#{version})" begin ActiveRecord::Base.connection.execute(sql) - puts "Successfully marked '#{version}' as complete".green + puts "Successfully marked '#{version}' as complete".color(:green) rescue ActiveRecord::RecordNotUnique - puts "Migration version '#{version}' is already marked complete".yellow + puts "Migration version '#{version}' is already marked complete".color(:yellow) end end diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index 65ee430d55..f9834a4dae 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -5,7 +5,7 @@ namespace :gitlab do task repack: :environment do failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo") if failures.empty? - puts "Done".green + puts "Done".color(:green) else output_failures(failures) end @@ -15,7 +15,7 @@ namespace :gitlab do task gc: :environment do failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting") if failures.empty? - puts "Done".green + puts "Done".color(:green) else output_failures(failures) end @@ -25,7 +25,7 @@ namespace :gitlab do task prune: :environment do failures = perform_git_cmd(%W(git prune), "Git Prune") if failures.empty? - puts "Done".green + puts "Done".color(:green) else output_failures(failures) end @@ -47,7 +47,7 @@ namespace :gitlab do end def output_failures(failures) - puts "The following repositories reported errors:".red + puts "The following repositories reported errors:".color(:red) failures.each { |f| puts "- #{f}" } end diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index 1c04f47f08..4753f00c26 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -23,7 +23,7 @@ namespace :gitlab do group_name, name = File.split(path) group_name = nil if group_name == '.' - puts "Processing #{repo_path}".yellow + puts "Processing #{repo_path}".color(:yellow) if path.end_with?('.wiki') puts " * Skipping wiki repo" @@ -51,9 +51,9 @@ namespace :gitlab do group.path = group_name group.owner = user if group.save - puts " * Created Group #{group.name} (#{group.id})".green + puts " * Created Group #{group.name} (#{group.id})".color(:green) else - puts " * Failed trying to create group #{group.name}".red + puts " * Failed trying to create group #{group.name}".color(:red) end end # set project group @@ -63,17 +63,17 @@ namespace :gitlab do project = Projects::CreateService.new(user, project_params).execute if project.persisted? - puts " * Created #{project.name} (#{repo_path})".green + puts " * Created #{project.name} (#{repo_path})".color(:green) project.update_repository_size project.update_commit_count else - puts " * Failed trying to create #{project.name} (#{repo_path})".red - puts " Errors: #{project.errors.messages}".red + puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red) + puts " Errors: #{project.errors.messages}".color(:red) end end end - puts "Done!".green + puts "Done!".color(:green) end end end diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index d6883a563e..352b566df2 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -15,15 +15,15 @@ namespace :gitlab do rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s) puts "" - puts "System information".yellow - puts "System:\t\t#{os_name || "unknown".red}" + puts "System information".color(:yellow) + puts "System:\t\t#{os_name || "unknown".color(:red)}" puts "Current User:\t#{run(%W(whoami))}" - puts "Using RVM:\t#{rvm_version.present? ? "yes".green : "no"}" + puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}" puts "RVM Version:\t#{rvm_version}" if rvm_version.present? - puts "Ruby Version:\t#{ruby_version || "unknown".red}" - puts "Gem Version:\t#{gem_version || "unknown".red}" - puts "Bundler Version:#{bunder_version || "unknown".red}" - puts "Rake Version:\t#{rake_version || "unknown".red}" + puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}" + puts "Gem Version:\t#{gem_version || "unknown".color(:red)}" + puts "Bundler Version:#{bunder_version || "unknown".color(:red)}" + puts "Rake Version:\t#{rake_version || "unknown".color(:red)}" puts "Sidekiq Version:#{Sidekiq::VERSION}" @@ -39,7 +39,7 @@ namespace :gitlab do omniauth_providers.map! { |provider| provider['name'] } puts "" - puts "GitLab information".yellow + puts "GitLab information".color(:yellow) puts "Version:\t#{Gitlab::VERSION}" puts "Revision:\t#{Gitlab::REVISION}" puts "Directory:\t#{Rails.root}" @@ -47,9 +47,9 @@ namespace :gitlab do puts "URL:\t\t#{Gitlab.config.gitlab.url}" puts "HTTP Clone URL:\t#{http_clone_url}" puts "SSH Clone URL:\t#{ssh_clone_url}" - puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".green : "no"}" - puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".green : "no"}" - puts "Omniauth Providers: #{omniauth_providers.map(&:magenta).join(', ')}" if Gitlab.config.omniauth.enabled + puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".color(:green) : "no"}" + puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}" + puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled @@ -60,8 +60,8 @@ namespace :gitlab do end puts "" - puts "GitLab Shell".yellow - puts "Version:\t#{gitlab_shell_version || "unknown".red}" + puts "GitLab Shell".color(:yellow) + puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}" puts "Repositories:\t#{Gitlab.config.gitlab_shell.repos_path}" puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}" puts "Git:\t\t#{Gitlab.config.git.bin_path}" diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index dd61632e55..b1648a4602 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -118,12 +118,12 @@ namespace :gitlab do puts "" unless $?.success? - puts "Failed to add keys...".red + puts "Failed to add keys...".color(:red) exit 1 end rescue Gitlab::TaskAbortedByUserError - puts "Quitting...".red + puts "Quitting...".color(:red) exit 1 end diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index d33b5b31e1..d0c019044b 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -2,7 +2,7 @@ module Gitlab class TaskAbortedByUserError < StandardError; end end -String.disable_colorization = true unless STDOUT.isatty +require 'rainbow/ext/string' # Prevent StateMachine warnings from outputting during a cron task StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] @@ -14,7 +14,7 @@ namespace :gitlab do # Returns "yes" the user chose to continue # Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue def ask_to_continue - answer = prompt("Do you want to continue (yes/no)? ".blue, %w{yes no}) + answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no}) raise Gitlab::TaskAbortedByUserError unless answer == "yes" end @@ -98,10 +98,10 @@ namespace :gitlab do gitlab_user = Gitlab.config.gitlab.user current_user = run(%W(whoami)).chomp unless current_user == gitlab_user - puts " Warning ".colorize(:black).on_yellow - puts " You are running as user #{current_user.magenta}, we hope you know what you are doing." + puts " Warning ".color(:black).background(:yellow) + puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing." puts " Things may work\/fail for the wrong reasons." - puts " For correct results you should run this as user #{gitlab_user.magenta}." + puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}." puts "" end @warned_user_not_gitlab = true diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake index 9196677a01..fc0ccc726e 100644 --- a/lib/tasks/gitlab/two_factor.rake +++ b/lib/tasks/gitlab/two_factor.rake @@ -6,17 +6,17 @@ namespace :gitlab do count = scope.count if count > 0 - puts "This will disable 2FA for #{count.to_s.red} users..." + puts "This will disable 2FA for #{count.to_s.color(:red)} users..." begin ask_to_continue scope.find_each(&:disable_two_factor!) - puts "Successfully disabled 2FA for #{count} users.".green + puts "Successfully disabled 2FA for #{count} users.".color(:green) rescue Gitlab::TaskAbortedByUserError - puts "Quitting...".red + puts "Quitting...".color(:red) end else - puts "There are currently no users with 2FA enabled.".yellow + puts "There are currently no users with 2FA enabled.".color(:yellow) end end end diff --git a/lib/tasks/gitlab/update_commit_count.rake b/lib/tasks/gitlab/update_commit_count.rake index 9b636f12d9..3bd10b0208 100644 --- a/lib/tasks/gitlab/update_commit_count.rake +++ b/lib/tasks/gitlab/update_commit_count.rake @@ -6,15 +6,15 @@ namespace :gitlab do ask_to_continue unless ENV['force'] == 'yes' projects.find_each(batch_size: 100) do |project| - print "#{project.name_with_namespace.yellow} ... " + print "#{project.name_with_namespace.color(:yellow)} ... " unless project.repo_exists? - puts "skipping, because the repo is empty".magenta + puts "skipping, because the repo is empty".color(:magenta) next end project.update_commit_count - puts project.commit_count.to_s.green + puts project.commit_count.to_s.color(:green) end end end diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake index 84aa312002..4fd48cccb1 100644 --- a/lib/tasks/gitlab/update_gitignore.rake +++ b/lib/tasks/gitlab/update_gitignore.rake @@ -2,14 +2,14 @@ namespace :gitlab do desc "GitLab | Update gitignore" task :update_gitignore do unless clone_gitignores - puts "Cloning the gitignores failed".red + puts "Cloning the gitignores failed".color(:red) return end remove_unneeded_files(gitignore_directory) remove_unneeded_files(global_directory) - puts "Done".green + puts "Done".color(:green) end def clone_gitignores diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake index cc0f668474..f467cc0ee2 100644 --- a/lib/tasks/gitlab/web_hook.rake +++ b/lib/tasks/gitlab/web_hook.rake @@ -12,9 +12,9 @@ namespace :gitlab do print "- #{project.name} ... " web_hook = project.hooks.new(url: web_hook_url) if web_hook.save - puts "added".green + puts "added".color(:green) else - print "failed".red + print "failed".color(:red) puts " [#{web_hook.errors.full_messages.to_sentence}]" end end @@ -57,7 +57,7 @@ namespace :gitlab do if namespace Project.in_namespace(namespace.id) else - puts "Namespace not found: #{namespace_path}".red + puts "Namespace not found: #{namespace_path}".color(:red) exit 2 end end diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake index d258c6fd08..4f2486157b 100644 --- a/lib/tasks/migrate/migrate_iids.rake +++ b/lib/tasks/migrate/migrate_iids.rake @@ -1,6 +1,6 @@ desc "GitLab | Build internal ids for issues and merge requests" task migrate_iids: :environment do - puts 'Issues'.yellow + puts 'Issues'.color(:yellow) Issue.where(iid: nil).find_each(batch_size: 100) do |issue| begin issue.set_iid @@ -15,7 +15,7 @@ task migrate_iids: :environment do end puts 'done' - puts 'Merge Requests'.yellow + puts 'Merge Requests'.color(:yellow) MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr| begin mr.set_iid @@ -30,7 +30,7 @@ task migrate_iids: :environment do end puts 'done' - puts 'Milestones'.yellow + puts 'Milestones'.color(:yellow) Milestone.where(iid: nil).find_each(batch_size: 100) do |m| begin m.set_iid diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index 01d23b89bb..da255f5464 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -52,7 +52,7 @@ def run_spinach_tests(tags) tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) puts '' - puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red + puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red) puts '' sleep(3) success = run_spinach_command(tests) From 4cd111e70b2dd597dfc3452afed016016136ab8c Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 3 Jun 2016 10:37:43 -0600 Subject: [PATCH 179/507] Add CHANGELOG entry. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 7215a919d7..9e65dfd814 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,7 @@ v 8.9.0 (unreleased) - Reduce number of queries needed to render issue labels in the sidebar - Improve error handling importing projects - Put project Files and Commits tabs under Code tab + - Replace Colorize with Rainbow for coloring console output in Rake tasks. v 8.8.4 - Fix todos page throwing errors when you have a project pending deletion From 01e1139f68066e46b86ef92749513377eee63f5b Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 12 May 2016 20:50:49 +0200 Subject: [PATCH 180/507] Workhorse to serve raw diffs --- .../projects/merge_requests_controller.rb | 13 ++++++++++--- app/models/merge_request.rb | 7 ------- features/project/merge_requests.feature | 12 ------------ lib/gitlab/workhorse.rb | 17 +++++++++++++++-- .../projects/merge_requests_controller_spec.rb | 9 +++------ 5 files changed, 28 insertions(+), 30 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3142fe5c76..fe811f30c0 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -57,9 +57,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html - format.json { render json: @merge_request } - format.diff { render text: @merge_request.to_diff } - format.patch { render text: @merge_request.to_patch } + format.json { render json: @merge_request } + format.patch { render text: @merge_request.to_patch } + format.diff do + headers.store(*Gitlab::Workhorse.send_git_diff(@project.repository, + @merge_request.diff_base_commit.id, + @merge_request.last_commit.id)) + headers['Content-Disposition'] = 'inline' + + head :ok + end end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 722c258244..2250536365 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -313,13 +313,6 @@ class MergeRequest < ActiveRecord::Base ) end - # Returns the raw diff for this merge request - # - # see "git diff" - def to_diff - target_project.repository.diff_text(diff_base_commit.sha, source_sha) - end - # Returns the commit as a series of email patches. # # see "git format-patch" diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index ecda4ea824..cc04ea7fe4 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -76,18 +76,6 @@ Feature: Project Merge Requests And I submit new merge request "Wiki Feature" Then I should see merge request "Wiki Feature" - Scenario: I download a diff on a public merge request - Given public project "Community" - And "John Doe" owns public project "Community" - And project "Community" has "Bug CO-01" open merge request with diffs inside - Given I logout directly - And I visit merge request page "Bug CO-01" - And I click on "Email Patches" - Then I should see a patch diff - And I visit merge request page "Bug CO-01" - And I click on "Plain Diff" - Then I should see a patch diff - @javascript Scenario: I comment on a merge request Given I visit merge request page "Bug NS-04" diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index c3ddd4c268..7f6ef71b30 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -29,9 +29,22 @@ module Gitlab "git-archive:#{encode(params)}", ] end - + + def send_git_diff(repository, from, to) + params = { + 'RepoPath' => repository.path_to_repo, + 'ShaFrom' => from, + 'ShaTo' => to + } + + [ + SEND_DATA_HEADER, + "git-diff:#{encode(params)}" + ] + end + protected - + def encode(hash) Base64.urlsafe_encode64(JSON.dump(hash)) end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 8499bf07e9..368fd49892 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -84,17 +84,14 @@ describe Projects::MergeRequestsController do end describe "as diff" do - include_examples "export merge as", :diff - let(:format) { :diff } - - it "should really only be a git diff" do + it "triggers workhorse to serve the request" do get(:show, namespace_id: project.namespace.to_param, project_id: project.to_param, id: merge_request.iid, - format: format) + format: :diff) - expect(response.body).to start_with("diff --git") + expect(response.headers['Gitlab-Workhorse-Send-Data']).to start_with("git-diff:") end end From d47218fb9732841956a5511afbde38ff731ab106 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 3 Jun 2016 18:41:01 +0200 Subject: [PATCH 181/507] Bump workhorse version to v0.7.5 --- CHANGELOG | 1 + GITLAB_WORKHORSE_VERSION | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7215a919d7..214758f38e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -162,6 +162,7 @@ v 8.7.6 - Fix import from GitLab.com to a private instance failure. !4181 - Fix external imports not finding the import data. !4106 - Fix notification delay when changing status of an issue + - Bump Workhorse to 0.7.5 so it can serve raw diffs v 8.7.5 - Fix relative links in wiki pages. !4050 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 0a1ffad4b4..8bd6ba8c5c 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.7.4 +0.7.5 From 317cfcbd4dd5891409b5362d9ce24faa66719b44 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 27 May 2016 15:05:46 +0100 Subject: [PATCH 182/507] Fixed issue with search autocomplete not allow arrow key navigation Closes #15649 --- app/assets/javascripts/gl_dropdown.js.coffee | 17 +++++++++++------ .../javascripts/search_autocomplete.js.coffee | 13 ++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index b3f1dc969b..d263faa287 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -470,20 +470,25 @@ class GitLabDropdown return selectedObject - selectRowAtIndex: (index) -> - selector = ".dropdown-content li:not(.divider):eq(#{index}) a" + selectRowAtIndex: (e, index) -> + selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a" if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one #{selector}" # simulate a click on the first link - $(selector, @dropdown).trigger "click" + $el = $(selector, @dropdown) + + if $el.length + e.preventDefault() + e.stopImmediatePropagation() + $(selector, @dropdown)[0].click() addArrowKeyEvent: -> ARROW_KEY_CODES = [38, 40] $input = @dropdown.find(".dropdown-input-field") - selector = '.dropdown-content li:not(.divider)' + selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)' if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one #{selector}" @@ -511,8 +516,8 @@ class GitLabDropdown return false - if currentKeyCode is 13 - @selectRowAtIndex if currentIndex < 0 then 0 else currentIndex + if currentKeyCode is 13 and currentIndex isnt -1 + @selectRowAtIndex e, currentIndex removeArrayKeyEvent: -> $('body').off 'keydown' diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 2122e80f57..5eb915a51e 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -156,11 +156,14 @@ class @SearchAutocomplete # No need to enable anything if user is not logged in return if !gon.current_user_id - _this = @ - @loadingSuggestions = false + unless @dropdown.hasClass('open') + _this = @ + @loadingSuggestions = false - @dropdown.addClass('open') - @searchInput.removeClass('disabled') + @dropdown + .addClass('open') + .trigger('shown.bs.dropdown') + @searchInput.removeClass('disabled') onSearchInputKeyDown: => # Saves last length of the entered text @@ -191,7 +194,7 @@ class @SearchAutocomplete @disableAutocomplete() else # We should display the menu only when input is not empty - @enableAutocomplete() + @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER @wrap.toggleClass 'has-value', !!e.target.value From e696795a42ccb69d0d29300db7a12c3ee3b1fd35 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 27 May 2016 15:25:42 +0100 Subject: [PATCH 183/507] CHANGELOG item --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 7215a919d7..899543fdae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -42,6 +42,7 @@ v 8.8.4 v 8.8.4 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds + - Fix issue with arrow keys not working in search autocomplete dropdown v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 From 2e8f039ae91767b912da2511d7697d7993293534 Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Fri, 3 Jun 2016 18:56:26 +0100 Subject: [PATCH 184/507] Document the API endpoint for gathering a build log * Resolves #18015 --- doc/api/builds.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/doc/api/builds.md b/doc/api/builds.md index 4c0a47d1ea..5669bd0cdd 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -278,6 +278,30 @@ Response: [ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893 +## Get a trace file + +Get a trace of a specific build of a project + +``` +GET /projects/:id/builds/:build_id/trace +``` + +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| id | integer | yes | The ID of a project | +| build_id | integer | yes | The ID of a build | + +``` +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace" +``` + +Response: + +| Status | Description | +|-----------|-----------------------------------| +| 200 | Serves the trace file | +| 404 | Build not found or no trace file | + ## Cancel a build Cancel a single build of a project From f9cc619c7c705619e85f6f1af4fc03f28e52c525 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 3 Jun 2016 12:32:42 -0600 Subject: [PATCH 185/507] The spritesheet should only load when an award emoji picker is opened. --- app/views/award_emoji/_awards_block.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index f1f93c1375..e9302c3975 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -2,7 +2,7 @@ .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } - = emoji_icon(emoji) + = emoji_icon(emoji, sprite: false) %span.award-control-text.js-counter = awards.count From d2b708ac43b0810ea2ce4de196ce46692e536027 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 3 Jun 2016 20:54:33 +0200 Subject: [PATCH 186/507] Extract CI config YAML parser to a separate class With this approach it would be easier to add different sources of configuration, that we do not necessairly have to be in YAML format. --- lib/gitlab/ci/config.rb | 8 ++-- lib/gitlab/ci/config/parser.rb | 25 +++++++++++ spec/lib/gitlab/ci/config/parser_spec.rb | 54 ++++++++++++++++++++++++ spec/lib/gitlab/ci/config_spec.rb | 14 +++++- 4 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 lib/gitlab/ci/config/parser.rb create mode 100644 spec/lib/gitlab/ci/config/parser_spec.rb diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 8f88ccf5bf..0baefa70f6 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -4,13 +4,13 @@ module Gitlab class ParserError < StandardError; end def initialize(config) - @config = YAML.safe_load(config, [Symbol], [], true) + parser = Parser.new(config) - unless @config.is_a?(Hash) - raise ParserError, 'YAML should be a hash' + unless parser.valid? + raise ParserError, 'Invalid configuration format!' end - @config = @config.deep_symbolize_keys + @config = parser.parse end def to_hash diff --git a/lib/gitlab/ci/config/parser.rb b/lib/gitlab/ci/config/parser.rb new file mode 100644 index 0000000000..6e1b7ec826 --- /dev/null +++ b/lib/gitlab/ci/config/parser.rb @@ -0,0 +1,25 @@ +module Gitlab + module Ci + class Config + class Parser + class FormatError < StandardError; end + + def initialize(config) + @config = YAML.safe_load(config, [Symbol], [], true) + end + + def valid? + @config.is_a?(Hash) + end + + def parse + unless valid? + raise FormatError, 'Invalid configuration format' + end + + @config.deep_symbolize_keys + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/parser_spec.rb b/spec/lib/gitlab/ci/config/parser_spec.rb new file mode 100644 index 0000000000..b35e66cde5 --- /dev/null +++ b/spec/lib/gitlab/ci/config/parser_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Parser do + let(:parser) { described_class.new(yml) } + + context 'when yaml syntax is correct' do + let(:yml) { 'image: ruby:2.2' } + + describe '#valid?' do + it 'returns true' do + expect(parser.valid?).to be true + end + end + + describe '#parse' do + it 'returns a hash' do + expect(parser.parse).to be_a Hash + end + + it 'returns a valid hash' do + expect(parser.parse).to eq(image: 'ruby:2.2') + end + end + end + + context 'when yaml syntax is incorrect' do + let(:yml) { '// incorrect' } + + describe '#valid?' do + it 'returns false' do + expect(parser.valid?).to be false + end + end + + describe '#parse' do + it 'raises error' do + expect { parser.parse }.to raise_error( + Gitlab::Ci::Config::Parser::FormatError, + 'Invalid configuration format' + ) + end + end + end + + context 'when yaml config is empty' do + let(:yml) { '' } + + describe '#valid?' do + it 'returns false' do + expect(parser.valid?).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 6e25170671..691c1ee8ba 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Ci::Config do described_class.new(yml) end - context 'when yml config is valid' do + context 'when config is valid' do let(:yml) do <<-EOS image: ruby:2.2 @@ -30,5 +30,17 @@ describe Gitlab::Ci::Config do expect(config.to_hash).to eq hash end end + + context 'when config is invalid' do + let(:yml) { '// invalid' } + + describe '.new' do + it 'raises error' do + expect { config }.to raise_error( + Gitlab::Ci::Config::ParserError, /Invalid configuration format/ + ) + end + end + end end end From 8450fe3074140b86e7d2e5a85d2cdb65051906ee Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 3 Jun 2016 15:49:34 -0300 Subject: [PATCH 187/507] Add index to notification settings --- CHANGELOG | 1 + ...0603180330_remove_duplicated_notification_settings.rb | 7 +++++++ .../20160603182247_add_index_to_notification_settings.rb | 9 +++++++++ lib/gitlab/database/migration_helpers.rb | 6 +++++- 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160603180330_remove_duplicated_notification_settings.rb create mode 100644 db/migrate/20160603182247_add_index_to_notification_settings.rb diff --git a/CHANGELOG b/CHANGELOG index 7215a919d7..bcb7e290ed 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -34,6 +34,7 @@ v 8.9.0 (unreleased) - Cache project build count in sidebar nav - Reduce number of queries needed to render issue labels in the sidebar - Improve error handling importing projects + - Remove duplicated notification settings - Put project Files and Commits tabs under Code tab v 8.8.4 diff --git a/db/migrate/20160603180330_remove_duplicated_notification_settings.rb b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb new file mode 100644 index 0000000000..c2fcac4c53 --- /dev/null +++ b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb @@ -0,0 +1,7 @@ +class RemoveDuplicatedNotificationSettings < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM notification_settings WHERE id NOT IN ( SELECT min_id from (SELECT MIN(id) as min_id FROM notification_settings GROUP BY user_id, source_type, source_id) as dups ) + SQL + end +end diff --git a/db/migrate/20160603182247_add_index_to_notification_settings.rb b/db/migrate/20160603182247_add_index_to_notification_settings.rb new file mode 100644 index 0000000000..06462042b0 --- /dev/null +++ b/db/migrate/20160603182247_add_index_to_notification_settings.rb @@ -0,0 +1,9 @@ +class AddIndexToNotificationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def change + add_concurrent_index :notification_settings, [:user_id, :source_id, :source_type], { unique: true, name: "index_notifications_on_user_id_and_source_id_and_source_type" } + end +end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index fd14234c55..b88e50748f 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -19,7 +19,11 @@ module Gitlab end if Database.postgresql? - args << { algorithm: :concurrently } + if args[2].present? + args[2].merge!({ algorithm: :concurrently }) + else + args << { algorithm: :concurrently } + end end add_index(*args) From d8e90d03b7b8dddeca54245b6f2662aa39b5cda8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 21:53:04 +0200 Subject: [PATCH 188/507] Generate knapsack reports --- .gitlab-ci.yml | 1 + app/controllers/projects/artifacts_controller.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6fc410b4b5..9af4b5fc7a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,6 +52,7 @@ prepare: - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} - export KNAPSACK_REPORT_PATH=knapsack/${JOB_NAME}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_GENERATE_REPORT=true - cp knapsack/${JOB_NAME}_report.json ${KNAPSACK_REPORT_PATH} - knapsack ${JOB_NAME[0]} artifacts: diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index cfea126651..832d7deb57 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController private def build - @build ||= project.builds.unscoped.find_by!(id: params[:build_id]) + @build ||= project.builds.find_by!(id: params[:build_id]) end def artifacts_file From ea731cac08efb8b2a4edbc7c76523ae5a5b17070 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 22:54:25 +0200 Subject: [PATCH 189/507] Preserve knapsack state --- .gitlab-ci.yml | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9af4b5fc7a..e331984365 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ before_script: - source ./scripts/prepare_build.sh - cp config/gitlab.yml.example config/gitlab.yml - bundle --version - - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" + - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' - retry gem install knapsack - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' @@ -31,18 +31,26 @@ stages: - post-test - notifications -prepare: +precompile: stage: prepare variables: USE_DB: "false" script: - bundle exec rake assets:precompile - - mkdir -p knapsack/ - - echo "{}" > knapsack/rspec_report.json - - echo "{}" > knapsack/spinach_report.json artifacts: paths: - public/assets/ + +.knapsack_state: &knapsack_state + variables: + USE_DB: "false" + USE_BUNDLE_INSTALL: "false" + cache: + key: "knapsack" + paths: + - knapsack/ + artifacts: + paths: - knapsack/ .knapsack: &knapsack @@ -59,21 +67,21 @@ prepare: paths: - knapsack/ -post-tests: +knapsack: + <<: *knapsack_state + stage: prepare + script: + - mkdir -p knapsack/ + - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json' + - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json' + +update-knapsack: + <<: *knapsack_state stage: post-test - variables: - USE_DB: "false" script: - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json - rm -f knapsack/*_node_*.json - cache: - key: "knapsack" - paths: - - knapsack/ - artifacts: - paths: - - knapsack/ .exec: &exec stage: test From ccc8d419e65f19cfa8ea60622b527299de55d533 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 3 Jun 2016 15:00:04 -0600 Subject: [PATCH 190/507] Add confidential issue notice in comment box. --- app/assets/stylesheets/pages/note_form.scss | 33 +++++++++++++++++++++ app/views/projects/_md_preview.html.haml | 6 ++++ 2 files changed, 39 insertions(+) diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 7fa13e66b4..a6765fbc7c 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -87,6 +87,39 @@ } } +.md-header .nav-links { + display: flex; + display: -webkit-flex; + flex-flow: row wrap; + -webkit-flex-flow: row wrap; + width: 100%; + + .pull-right { + // Flexbox quirk to make sure right-aligned items stay right-aligned. + margin-left: auto; + } +} + +.confidential-issue-warning { + background-color: $gray-normal; + border-radius: 3px; + padding: 3px 12px; + margin: auto; + margin-top: 0; + text-align: center; + font-size: 13px; + + @media (max-width: $screen-md-min) { + // On smaller devices the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + -webkit-order: 4; + margin: 6px auto; + width: 100%; + } +} + .discussion-form { padding: $gl-padding-top $gl-padding; background-color: $white-light; diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 81afea2c60..59a952dd66 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -7,6 +7,12 @@ %li %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview + + - if @issue.confidential? + %li.confidential-issue-warning + = icon('warning') + %span This is a confidential issue. Your comment will not be visible to the public. + %li.pull-right %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } Go full screen From 9a201adddf0f5bdf10ff5cda65f6d58ef904df4b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 23:03:58 +0200 Subject: [PATCH 191/507] USE_BUNDLE_INSTALL --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e331984365..dc04f3f298 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,7 @@ variables: RAILS_ENV: "test" SIMPLECOV: "true" USE_DB: "true" + USE_BUNDLE_INSTALL: "true" before_script: - source ./scripts/prepare_build.sh From 82f7831d734805188c84e687aced459564549325 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 23:05:15 +0200 Subject: [PATCH 192/507] Don't start services if they are not needed --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dc04f3f298..9774602cbd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,6 +34,7 @@ stages: precompile: stage: prepare + services: [] variables: USE_DB: "false" script: @@ -43,6 +44,7 @@ precompile: - public/assets/ .knapsack_state: &knapsack_state + services: [] variables: USE_DB: "false" USE_BUNDLE_INSTALL: "false" From 5b0316eab51546addea9cde74b710e9742fc95b8 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 3 Jun 2016 15:10:52 -0600 Subject: [PATCH 193/507] Add Changelog entry. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d54cac7f93..8c0a1d42d3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,6 +36,7 @@ v 8.9.0 (unreleased) - Improve error handling importing projects - Put project Files and Commits tabs under Code tab - Replace Colorize with Rainbow for coloring console output in Rake tasks. + - An indicator is now displayed at the top of the comment field for confidential issues. v 8.8.4 - Fix todos page throwing errors when you have a project pending deletion From 2e6f816d36d578014c7869ae61e65414fdeb1814 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Jun 2016 23:11:48 +0200 Subject: [PATCH 194/507] Trigger build From 1f608ac4614f57130992916931ad10f4d5fd9d50 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Sat, 4 Jun 2016 00:02:40 +0200 Subject: [PATCH 195/507] Remove 'unscoped' from project builds selection --- .../projects/artifacts_controller.rb | 2 +- app/controllers/projects/builds_controller.rb | 2 +- spec/features/builds_spec.rb | 168 ++++++++++++++---- 3 files changed, 138 insertions(+), 34 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index cfea126651..832d7deb57 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController private def build - @build ||= project.builds.unscoped.find_by!(id: params[:build_id]) + @build ||= project.builds.find_by!(id: params[:build_id]) end def artifacts_file diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index bb1f6c5e98..db3ae58605 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -81,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController private def build - @build ||= project.builds.unscoped.find_by!(id: params[:id]) + @build ||= project.builds.find_by!(id: params[:id]) end def build_path(build) diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 7a05d30e8b..e268d76755 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -7,6 +7,7 @@ describe "Builds" do login_as(:user) @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit + @build2 = FactoryGirl.create :ci_build @project = @commit.project @project.team << [@user, :developer] end @@ -66,13 +67,24 @@ describe "Builds" do end describe "GET /:project/builds/:id" do - before do - visit namespace_project_build_path(@project.namespace, @project, @build) + context "Build from project" do + before do + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content @commit.sha[0..7] } + it { expect(page).to have_content @commit.git_commit_message } + it { expect(page).to have_content @commit.git_author_name } end - it { expect(page).to have_content @commit.sha[0..7] } - it { expect(page).to have_content @commit.git_commit_message } - it { expect(page).to have_content @commit.git_author_name } + context "Build from other project" do + before do + visit namespace_project_build_path(@project.namespace, @project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end context "Download artifacts" do before do @@ -103,51 +115,143 @@ describe "Builds" do end describe "POST /:project/builds/:id/cancel" do - before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) - click_link "Cancel" + context "Build from project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link "Cancel" + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content 'Retry' } end - it { expect(page).to have_content 'canceled' } - it { expect(page).to have_content 'Retry' } + context "Build from other project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + page.driver.post(cancel_namespace_project_build_path(@project.namespace, @project, @build2)) + end + + it { expect(page.status_code).to eq(404) } + end end describe "POST /:project/builds/:id/retry" do - before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) - click_link "Cancel" - click_link 'Retry' + context "Build from project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link 'Cancel' + click_link 'Retry' + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content 'pending' } + it { expect(page).to have_content 'Cancel' } end - it { expect(page).to have_content 'pending' } - it { expect(page).to have_content 'Cancel' } + context "Build from other project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link 'Cancel' + page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2)) + end + + it { expect(page.status_code).to eq(404) } + end end describe "GET /:project/builds/:id/download" do - before do - @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@project.namespace, @project, @build) - page.within('.artifacts') { click_link 'Download' } + context "Build from project" do + before do + @build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(@project.namespace, @project, @build) + page.within('.artifacts') { click_link 'Download' } + end + + it { expect(page.status_code).to eq(200) } + it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } end - it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } + context "Build from other project" do + before do + @build2.update_attributes(artifacts_file: artifacts_file) + visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end end describe "GET /:project/builds/:id/raw" do - before do - Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build.run! - @build.trace = 'BUILD TRACE' - visit namespace_project_build_path(@project.namespace, @project, @build) + context "Build from project" do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + @build.trace = 'BUILD TRACE' + visit namespace_project_build_path(@project.namespace, @project, @build) + page.within('.build-controls') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(200) + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + end end - it 'sends the right headers' do - page.within('.build-controls') { click_link 'Raw' } + context "Build from other project" do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build2.run! + @build2.trace = 'BUILD TRACE' + visit raw_namespace_project_build_path(@project.namespace, @project, @build2) + puts page.status_code + puts current_url + end - expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') - expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + it 'sends the right headers' do + expect(page.status_code).to eq(404) + end + end + end + + describe "GET /:project/builds/:id/trace.json" do + context "Build from project" do + before do + visit trace_namespace_project_build_path(@project.namespace, @project, @build, format: :json) + end + + it { expect(page.status_code).to eq(200) } + end + + context "Build from other project" do + before do + visit trace_namespace_project_build_path(@project.namespace, @project, @build2, format: :json) + end + + it { expect(page.status_code).to eq(404) } + end + end + + describe "GET /:project/builds/:id/status" do + context "Build from project" do + before do + visit status_namespace_project_build_path(@project.namespace, @project, @build) + end + + it { expect(page.status_code).to eq(200) } + end + + context "Build from other project" do + before do + visit status_namespace_project_build_path(@project.namespace, @project, @build2) + end + + it { expect(page.status_code).to eq(404) } end end end From dbccacdb82bc40ce7f88e1e5027a0c2820f08349 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 22 Apr 2016 16:14:28 -0500 Subject: [PATCH 196/507] Ability to assign a label to multiple issues --- app/assets/javascripts/labels_select.js.coffee | 4 ++++ app/controllers/projects/issues_controller.rb | 3 ++- app/views/shared/issuable/_filter.html.haml | 4 ++++ app/views/shared/issuable/_label_dropdown.html.haml | 10 +++++++++- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 995fd76860..b4ee41a850 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -12,6 +12,7 @@ class @LabelsSelect newColorField = $('#new_label_color') showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') + useId = $dropdown.data('use-id') defaultLabel = $dropdown.data('default-label') abilityName = $dropdown.data('ability-name') $selectbox = $dropdown.closest('.selectbox') @@ -284,6 +285,9 @@ class @LabelsSelect multiSelect: $dropdown.hasClass 'js-multiselect' clicked: (label) -> + if $dropdown.hasClass('js-filter-bulk-update') + return + page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is 'projects:merge_requests:index' diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 5f3745d94b..4ec8d31986 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -216,7 +216,8 @@ class Projects::IssuesController < Projects::ApplicationController :issues_ids, :assignee_id, :milestone_id, - :state_event + :state_event, + :label_ids ) end end diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index cedff4af2e..c4324809fb 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -44,6 +44,10 @@ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", classes: ' js-filter-bulk-update js-multiselect ', extra_options: false, show_footer: false, extra_options: false, data_options: { field_name: "update[label_ids]", show_no: false, show_any: false, use_id: true } + = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 61fd1e9c33..0091e4f49f 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,9 +1,17 @@ +- extra_options = local_assigns.fetch(:extra_options, true) +- show_footer = local_assigns.fetch(:show_footer, true) +- data_options = local_assigns.fetch(:data_options, {}) +- classes = local_assigns.fetch(:classes, '') +- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"} +- dropdown_data.merge!(data_options) +- classes << ' js-extra-options ' if extra_options + - if params[:label_name].present? - if params[:label_name].respond_to?('any?') - params[:label_name].each do |label| = hidden_field_tag "label_name[]", label, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect{class: classes, type: "button", data: dropdown_data} %span.dropdown-toggle-text = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') From da29ad4b9d8db9a035791ee7e46b8fdc237d5edf Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 25 Apr 2016 17:09:13 -0500 Subject: [PATCH 197/507] Add label_ids to bulk_update_params --- app/controllers/projects/issues_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4ec8d31986..5897418ed5 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -217,7 +217,7 @@ class Projects::IssuesController < Projects::ApplicationController :assignee_id, :milestone_id, :state_event, - :label_ids + label_ids: [] ) end end From af30c87cf28c589bb080ead96938ac77e433b0fb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 25 Apr 2016 17:10:12 -0500 Subject: [PATCH 198/507] Remove unnecesary assignment --- app/assets/javascripts/labels_select.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index b4ee41a850..b8205565db 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -12,7 +12,6 @@ class @LabelsSelect newColorField = $('#new_label_color') showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') - useId = $dropdown.data('use-id') defaultLabel = $dropdown.data('default-label') abilityName = $dropdown.data('ability-name') $selectbox = $dropdown.closest('.selectbox') From 243e51325536ca8468a9d2692cc68aa5f466b5e3 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 25 Apr 2016 17:11:17 -0500 Subject: [PATCH 199/507] Add .js-filter-submit when needed --- app/views/shared/issuable/_filter.html.haml | 2 +- app/views/shared/issuable/_label_dropdown.html.haml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index c4324809fb..0ca4b9a681 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -46,7 +46,7 @@ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown", classes: ' js-filter-bulk-update js-multiselect ', extra_options: false, show_footer: false, extra_options: false, data_options: { field_name: "update[label_ids]", show_no: false, show_any: false, use_id: true } + = render "shared/issuable/label_dropdown", classes: ' js-filter-bulk-update js-multiselect ', extra_options: false, filter_submit: false, show_footer: false, extra_options: false, data_options: { field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 0091e4f49f..5e25d83866 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,17 +1,19 @@ - extra_options = local_assigns.fetch(:extra_options, true) +- filter_submit = local_assigns.fetch(:filter_submit, true) - show_footer = local_assigns.fetch(:show_footer, true) - data_options = local_assigns.fetch(:data_options, {}) - classes = local_assigns.fetch(:classes, '') - dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"} - dropdown_data.merge!(data_options) - classes << ' js-extra-options ' if extra_options +- classes << ' js-filter-submit ' if filter_submit - if params[:label_name].present? - if params[:label_name].respond_to?('any?') - params[:label_name].each do |label| = hidden_field_tag "label_name[]", label, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect{class: classes, type: "button", data: dropdown_data} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes, type: "button", data: dropdown_data} %span.dropdown-toggle-text = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') From 4ab6bfcef50b5e3a8c879ad06161bbc8b56bfb9a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 26 Apr 2016 13:05:21 -0500 Subject: [PATCH 200/507] Set indeterminated items --- app/assets/javascripts/gl_dropdown.js.coffee | 14 +++++++- .../javascripts/labels_select.js.coffee | 32 +++++++++++++++++-- .../stylesheets/framework/dropdowns.scss | 11 +++++-- app/helpers/issuables_helper.rb | 5 +++ app/views/projects/issues/_issue.html.haml | 2 +- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index d263faa287..7a23a3cf88 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -11,6 +11,8 @@ class GitLabDropdownFilter $inputContainer = @input.parent() $clearButton = $inputContainer.find('.js-dropdown-input-clear') + @indeterminatedIds = [] + # Clear click $clearButton.on 'click', (e) => e.preventDefault() @@ -298,6 +300,13 @@ class GitLabDropdown opened: => @addArrowKeyEvent() + if @options.setIndeterminatedIds + @options.setIndeterminatedIds.call(@) + + # Makes indeterminated items effective + if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') + @parseData @fullData + contentHtml = $('.dropdown-content', @dropdown).html() if @remote && contentHtml is "" @remote.execute() @@ -309,6 +318,9 @@ class GitLabDropdown hidden: (e) => @removeArrayKeyEvent() + + return if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') + if @options.filterable @dropdown .find(".dropdown-input-field") @@ -358,7 +370,7 @@ class GitLabDropdown if @options.renderRow # Call the render function - html = @options.renderRow(data) + html = @options.renderRow.call(@options, data, @) else if not selected value = if @options.id then @options.id(data) else data.id diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index b8205565db..0aedc7a5f0 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -1,5 +1,7 @@ class @LabelsSelect constructor: -> + _this = @ + $('.js-label-select').each (i, dropdown) -> $dropdown = $(dropdown) projectId = $dropdown.data('project-id') @@ -196,15 +198,25 @@ class @LabelsSelect callback data - renderRow: (label) -> + renderRow: (label, instance) -> + selectedClass = [] removesAll = label.id is 0 or not label.id? - selectedClass = [] + if $dropdown.hasClass('js-filter-bulk-update') + indeterminated = instance.indeterminatedIds + if indeterminated.indexOf(label.id) isnt -1 + selectedClass.push 'indeterminated' + if $form.find("input[type='hidden']\ [name='#{$dropdown.data('fieldName')}']\ [value='#{this.id(label)}']").length selectedClass.push 'is-active' + index = selectedClass.indexOf('indeterminated') + + if index isnt -1 + selectedClass.splice(index, 1) + if $dropdown.hasClass('js-multiselect') and removesAll selectedClass.push 'dropdown-clear-active' @@ -264,6 +276,8 @@ class @LabelsSelect label.id hidden: -> + return if $dropdown.hasClass('js-filter-bulk-update') + page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is 'projects:merge_requests:index' @@ -301,4 +315,18 @@ class @LabelsSelect return else saveLabelData() + + setIndeterminatedIds: -> + if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') + console.log 'options.setIndeterminatedIds' + @indeterminatedIds = _this.getIndeterminatedIds() ) + + getIndeterminatedIds: -> + label_ids = [] + + $('.selected_issue:checked').each (i, el) -> + issue_id = $(el).data('id') + label_ids.push $("#issue_#{issue_id}").data('labels') + + _.flatten(label_ids) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 93c63c6984..a77c19bf2a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -232,9 +232,8 @@ a { padding-left: 25px; - &.is-active { + &.indeterminated, &.is-active{ &::before { - content: "\f00c"; position: absolute; left: 5px; top: 50%; @@ -246,6 +245,14 @@ -moz-osx-font-smoothing: grayscale; } } + + &.indeterminated::before { + content: "\f068"; + } + + &.is-active::before { + content: "\f00c"; + } } } diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index fe84ee3de4..b1ff95f620 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -97,4 +97,9 @@ module IssuablesHelper end end + def label_ids(issuable) + return nil if !issuable.labels.any? + issuable.labels.pluck :id + end + end diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 4701429215..ebaa34f4ff 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,4 +1,4 @@ -%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue) } +%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), :'data-labels' => label_ids(issue) } - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" From 6974d970b40af6b01e14941314a09d250d87c8d3 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 26 Apr 2016 14:34:19 -0500 Subject: [PATCH 201/507] typo --- app/assets/javascripts/gl_dropdown.js.coffee | 8 ++++---- .../javascripts/labels_select.js.coffee | 19 +++++++------------ .../stylesheets/framework/dropdowns.scss | 4 ++-- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 7a23a3cf88..989ab3023c 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -11,7 +11,7 @@ class GitLabDropdownFilter $inputContainer = @input.parent() $clearButton = $inputContainer.find('.js-dropdown-input-clear') - @indeterminatedIds = [] + @indeterminateIds = [] # Clear click $clearButton.on 'click', (e) => @@ -300,10 +300,10 @@ class GitLabDropdown opened: => @addArrowKeyEvent() - if @options.setIndeterminatedIds - @options.setIndeterminatedIds.call(@) + if @options.setIndeterminateIds + @options.setIndeterminateIds.call(@) - # Makes indeterminated items effective + # Makes indeterminate items effective if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') @parseData @fullData diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 0aedc7a5f0..da2c85967d 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -203,20 +203,15 @@ class @LabelsSelect removesAll = label.id is 0 or not label.id? if $dropdown.hasClass('js-filter-bulk-update') - indeterminated = instance.indeterminatedIds - if indeterminated.indexOf(label.id) isnt -1 - selectedClass.push 'indeterminated' + indeterminate = instance.indeterminateIds + if indeterminate.indexOf(label.id) isnt -1 + selectedClass.push 'indeterminate' if $form.find("input[type='hidden']\ [name='#{$dropdown.data('fieldName')}']\ [value='#{this.id(label)}']").length selectedClass.push 'is-active' - index = selectedClass.indexOf('indeterminated') - - if index isnt -1 - selectedClass.splice(index, 1) - if $dropdown.hasClass('js-multiselect') and removesAll selectedClass.push 'dropdown-clear-active' @@ -316,13 +311,13 @@ class @LabelsSelect else saveLabelData() - setIndeterminatedIds: -> + setIndeterminateIds: -> if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') - console.log 'options.setIndeterminatedIds' - @indeterminatedIds = _this.getIndeterminatedIds() + console.log 'options.setIndeterminateIds' + @indeterminateIds = _this.getIndeterminateIds() ) - getIndeterminatedIds: -> + getIndeterminateIds: -> label_ids = [] $('.selected_issue:checked').each (i, el) -> diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a77c19bf2a..6ef0405c7e 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -232,7 +232,7 @@ a { padding-left: 25px; - &.indeterminated, &.is-active{ + &.indeterminate, &.is-active{ &::before { position: absolute; left: 5px; @@ -246,7 +246,7 @@ } } - &.indeterminated::before { + &.indeterminate::before { content: "\f068"; } From eccc1f5911ce686ca6d5ce32c7f25aa1f905609a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 26 Apr 2016 14:57:53 -0500 Subject: [PATCH 202/507] Update classname for indeterminate state --- app/assets/javascripts/labels_select.js.coffee | 2 +- app/assets/stylesheets/framework/dropdowns.scss | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index da2c85967d..6a3c8de300 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -205,7 +205,7 @@ class @LabelsSelect if $dropdown.hasClass('js-filter-bulk-update') indeterminate = instance.indeterminateIds if indeterminate.indexOf(label.id) isnt -1 - selectedClass.push 'indeterminate' + selectedClass.push 'is-indeterminate' if $form.find("input[type='hidden']\ [name='#{$dropdown.data('fieldName')}']\ diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 6ef0405c7e..d18d00c1e5 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -232,7 +232,7 @@ a { padding-left: 25px; - &.indeterminate, &.is-active{ + &.is-indeterminate, &.is-active{ &::before { position: absolute; left: 5px; @@ -246,7 +246,7 @@ } } - &.indeterminate::before { + &.is-indeterminate::before { content: "\f068"; } From d8aaf018ac318aefa126f35bf0110da913441735 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 26 Apr 2016 16:50:48 -0500 Subject: [PATCH 203/507] Handle unchecking of indeterminate items --- app/assets/javascripts/gl_dropdown.js.coffee | 32 ++++++++++++++----- .../javascripts/labels_select.js.coffee | 9 ++++-- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 989ab3023c..1e14486174 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -144,6 +144,7 @@ class GitLabDropdown LOADING_CLASS = "is-loading" PAGE_TWO_CLASS = "is-page-two" ACTIVE_CLASS = "is-active" + INDETERMINATE_CLASS = "is-indeterminate" currentIndex = -1 FILTER_INPUT = '.dropdown-input .dropdown-input-field' @@ -319,8 +320,6 @@ class GitLabDropdown hidden: (e) => @removeArrayKeyEvent() - return if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') - if @options.filterable @dropdown .find(".dropdown-input-field") @@ -455,6 +454,17 @@ class GitLabDropdown $(@el).find(".dropdown-toggle-text").text @options.toggleLabel else selectedObject + else if el.hasClass(INDETERMINATE_CLASS) + el.addClass ACTIVE_CLASS + el.removeClass INDETERMINATE_CLASS + + if !value? + field.remove() + + if !field.length and fieldName + @addInput(fieldName, value) + + return selectedObject else if not @options.multiSelect or el.hasClass('dropdown-clear-active') @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @@ -471,17 +481,23 @@ class GitLabDropdown $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) if value? if !field.length and fieldName - # Create hidden input for form - input = "" - if @options.inputId? - input = $(input) - .attr('id', @options.inputId) - @dropdown.before input + @addInput(fieldName, value) else field.val value return selectedObject + addInput: (fieldName, value)-> + # Create hidden input for form + input = "" + if @options.inputId? + input = $(input) + .attr('id', @options.inputId) + @dropdown.before input + + removeInputs: -> + @dropdown.parent().find('input[type="hidden"]').remove() + selectRowAtIndex: (e, index) -> selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a" diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 6a3c8de300..000d47c6ce 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -271,8 +271,6 @@ class @LabelsSelect label.id hidden: -> - return if $dropdown.hasClass('js-filter-bulk-update') - page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is 'projects:merge_requests:index' @@ -289,7 +287,12 @@ class @LabelsSelect else if $dropdown.hasClass('js-filter-submit') $dropdown.closest('form').submit() else - saveLabelData() + if not $dropdown.hasClass 'js-filter-bulk-update' + saveLabelData() + + if $dropdown.hasClass('js-filter-bulk-update') + @removeInputs() + $dropdown.parent().find('.is-active, .is-indeterminate').removeClass() multiSelect: $dropdown.hasClass 'js-multiselect' clicked: (label) -> From 02fc7adf11e3675818399ffa97571a38efd9cac1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 26 Apr 2016 22:16:18 -0500 Subject: [PATCH 204/507] Remove console.log --- app/assets/javascripts/labels_select.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 000d47c6ce..8c259d45b9 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -316,7 +316,6 @@ class @LabelsSelect setIndeterminateIds: -> if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') - console.log 'options.setIndeterminateIds' @indeterminateIds = _this.getIndeterminateIds() ) From 558ef36f6ce5b1acb86ce5a1e38ab078fe96bcc2 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 26 Apr 2016 22:29:01 -0500 Subject: [PATCH 205/507] Do not remove inputs when dropdown menu hides --- app/assets/javascripts/labels_select.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 8c259d45b9..9f845078b7 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -291,7 +291,6 @@ class @LabelsSelect saveLabelData() if $dropdown.hasClass('js-filter-bulk-update') - @removeInputs() $dropdown.parent().find('.is-active, .is-indeterminate').removeClass() multiSelect: $dropdown.hasClass 'js-multiselect' From e250e8571b27ea6abdb4db4ed5e1695485c84f34 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 26 Apr 2016 23:55:58 -0500 Subject: [PATCH 206/507] Bulk assignment tests --- .../issues/bulk_assigment_labels_spec.rb | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 spec/features/issues/bulk_assigment_labels_spec.rb diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assigment_labels_spec.rb new file mode 100644 index 0000000000..417c4cf09b --- /dev/null +++ b/spec/features/issues/bulk_assigment_labels_spec.rb @@ -0,0 +1,109 @@ +require 'rails_helper' + +feature 'Issues > Labels bulk assignment', feature: true do + include WaitForAjax + + let(:user) { create(:user) } + let!(:project) { create(:project) } + let!(:issue1) { create(:issue, project: project, title: "Issue 1") } + let!(:issue2) { create(:issue, project: project, title: "Issue 2") } + + before do + create(:label, project: project, title: 'bug') + create(:label, project: project, title: 'feature') + end + + context 'as a allowed user', js: true do + before do + project.team << [user, :master] + login_as user + + visit namespace_project_issues_path(project.namespace, project) + end + + context 'can bulk assign a label' do + context 'to all issues' do + before do + check 'check_all_issues' + open_labels_dropdown ['bug'] + click_button 'Update issues' + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'bug' + end + end + + context 'to a issue' do + before do + check "selected_issue_#{issue1.id}" + open_labels_dropdown ['bug'] + click_button 'Update issues' + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + end + end + end + + context 'can bulk assign multiple labels' do + context 'to all issues' do + before do + check 'check_all_issues' + open_labels_dropdown ['bug', 'feature'] + click_button 'Update issues' + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'feature' + end + end + + context 'to a issue' do + before do + check "selected_issue_#{issue1.id}" + open_labels_dropdown ['bug', 'feature'] + click_button 'Update issues' + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + end + end + end + end + + context 'as a guest' do + before do + login_as user + + visit namespace_project_issues_path(project.namespace, project) + end + + context 'cannot bulk assign labels' do + it do + expect(page).not_to have_css '.check_all_issues' + expect(page).not_to have_css '.issue-check' + end + end + end + + def open_labels_dropdown(items = []) + page.within('.issues_bulk_update') do + click_button 'Label' + wait_for_ajax + items.map do |item| + click_link item + end + end + end +end \ No newline at end of file From 112f6a1e6d2e12e4c7f507a8fc0adb996e4a25b0 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 27 Apr 2016 11:07:09 -0500 Subject: [PATCH 207/507] Add empty line to end of file --- spec/features/issues/bulk_assigment_labels_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assigment_labels_spec.rb index 417c4cf09b..b99729ffd0 100644 --- a/spec/features/issues/bulk_assigment_labels_spec.rb +++ b/spec/features/issues/bulk_assigment_labels_spec.rb @@ -106,4 +106,4 @@ feature 'Issues > Labels bulk assignment', feature: true do end end end -end \ No newline at end of file +end From 7712aaf2a757835657f4e6b1af3349b0a8974fc9 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 27 Apr 2016 11:09:29 -0500 Subject: [PATCH 208/507] Add space --- app/assets/stylesheets/framework/dropdowns.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d18d00c1e5..28634d0c59 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -232,7 +232,7 @@ a { padding-left: 25px; - &.is-indeterminate, &.is-active{ + &.is-indeterminate, &.is-active { &::before { position: absolute; left: 5px; From 5a474ff8dafcd238f8bc3d93a34ff2093d0332a6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 27 Apr 2016 11:24:11 -0500 Subject: [PATCH 209/507] Ensure we are clicking a visible dropdown --- features/steps/project/issues/filter_labels.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index d82c685691..d34fa69478 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -29,7 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps end step 'I click link "bug"' do - page.find('.js-label-select').click + page.find('.js-label-select', visible: true).click sleep 0.5 execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") end From e42f88ca3783cc848137a04ed0c21f9cb3c0b7a7 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 27 Apr 2016 18:07:25 -0500 Subject: [PATCH 210/507] Restore dropdown state when unchecking all issues --- app/assets/javascripts/labels_select.js.coffee | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 9f845078b7..52c1e8a469 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -318,6 +318,20 @@ class @LabelsSelect @indeterminateIds = _this.getIndeterminateIds() ) + @bindEvents() + + bindEvents: -> + $('body').on 'change', '.selected_issue', @onSelectCheckboxIssue + + onSelectCheckboxIssue: -> + return if $('.selected_issue:checked').length + + # Remove inputs + $('.issues_bulk_update .labels-filter input[type="hidden"]').remove() + + # Also restore button text + $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label') + getIndeterminateIds: -> label_ids = [] From 81a21c945eb8a6c2f0af7bb074a1aec389fe9394 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 29 Apr 2016 17:38:07 +0100 Subject: [PATCH 211/507] WIP: allow adding and removing labels in bulk --- app/controllers/projects/issues_controller.rb | 4 ++- app/services/issuable_base_service.rb | 35 ++++++++++++++++--- app/services/issues/bulk_update_service.rb | 6 ++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 5897418ed5..855b4cfa09 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -217,7 +217,9 @@ class Projects::IssuesController < Projects::ApplicationController :assignee_id, :milestone_id, :state_event, - label_ids: [] + label_ids: [], + add_label_ids: [], + remove_label_ids: [] ) end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 2b16089df1..969c87a6ed 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -45,6 +45,8 @@ class IssuableBaseService < BaseService unless can?(current_user, ability, project) params.delete(:milestone_id) + params.delete(:add_label_ids) + params.delete(:remove_label_ids) params.delete(:label_ids) params.delete(:assignee_id) end @@ -67,10 +69,35 @@ class IssuableBaseService < BaseService end def filter_labels - return if params[:label_ids].to_a.empty? + if params[:add_label_ids].present? || params[:remove_label_ids].present? + params.delete(:label_ids) - params[:label_ids] = - project.labels.where(id: params[:label_ids]).pluck(:id) + filter_labels_by_name([:add_label_ids, :remove_label_ids]) + else + filter_labels_by_name([:label_ids]) + end + end + + def filter_labels_by_name(keys) + keys.each do |key| + return if params[key].to_a.empty? + + params[key] = project.labels.where(id: params[key]).pluck(:id) + end + end + + def update_issuable(issuable, attributes) + issuable.with_transaction_returning_status do + add_label_ids = attributes.delete(:add_label_ids) + remove_label_ids = attributes.delete(:remove_label_ids) + + issuable.label_ids |= add_label_ids if add_label_ids + issuable.label_ids -= remove_label_ids if remove_label_ids + + issuable.assign_attributes(attributes) + + issuable.save + end end def update(issuable) @@ -78,7 +105,7 @@ class IssuableBaseService < BaseService filter_params old_labels = issuable.labels.to_a - if params.present? && issuable.update_attributes(params.merge(updated_by: current_user)) + if params.present? && update_issuable(issuable, params.merge(updated_by: current_user)) issuable.reset_events_cache handle_common_system_notes(issuable, old_labels: old_labels) handle_changes(issuable, old_labels: old_labels) diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index de8387c490..2772add1d0 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -4,9 +4,9 @@ module Issues issues_ids = params.delete(:issues_ids).split(",") issue_params = params - issue_params.delete(:state_event) unless issue_params[:state_event].present? - issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present? - issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present? + [:state_event, :milestone_id, :assignee_id, :label_ids, :add_label_ids, :remove_label_ids].each do |key| + issue_params.delete(key) unless issue_params[key].present? + end issues = Issue.where(id: issues_ids) issues.each do |issue| From ac40843c94f369cad07f98a2403cd06db041f986 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 2 May 2016 16:25:21 +0100 Subject: [PATCH 212/507] fixup! WIP: allow adding and removing labels in bulk --- .../issues/bulk_update_service_spec.rb | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 454d584949..09a1415abf 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -111,4 +111,152 @@ describe Issues::BulkUpdateService, services: true do end end + describe 'updating labels' do + def create_issue_with_labels(labels) + create(:issue, project: project) { |issue| issue.labels = labels } + end + + let(:user) { create(:user) } + let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute } + let(:label_1) { create(:label, project: project) } + let(:label_2) { create(:label, project: project) } + let(:label_3) { create(:label, project: project) } + + let(:issue_all_labels) { create_issue_with_labels([label_1, label_2, label_3]) } + let(:issue_labels_1_and_2) { create_issue_with_labels([label_1, label_2]) } + let(:issue_labels_1_and_3) { create_issue_with_labels([label_1, label_3]) } + let(:issue_no_labels) { create(:issue, project: project) } + let(:issues) { [issue_all_labels, issue_labels_1_and_2, issue_labels_1_and_3, issue_no_labels] } + + let(:labels) { [] } + let(:add_labels) { [] } + let(:remove_labels) { [] } + + let(:params) do + { + label_ids: labels.map(&:id), + add_label_ids: add_labels.map(&:id), + remove_label_ids: remove_labels.map(&:id), + issues_ids: issues.map(&:id).join(',') + } + end + + before { Issues::BulkUpdateService.new(project, user, params).execute } + + context 'when label_ids are passed' do + let(:issues) { [issue_all_labels, issue_no_labels] } + let(:labels) { [label_1, label_2] } + + it 'updates the labels of all issues passed to the labels passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id))) + end + + it 'does not update issues not passed in' do + expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + end + end + + context 'when add_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_labels_1_and_3, issue_no_labels] } + let(:add_labels) { [label_1, label_2, label_3] } + + it 'adds those label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) + end + + it 'does not update issues not passed in' do + expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + end + end + + context 'when remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_labels_1_and_3, issue_no_labels] } + let(:remove_labels) { [label_1, label_2, label_3] } + + it 'removes those label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + + it 'does not update issues not passed in' do + expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_labels_1_and_3, issue_no_labels] } + let(:add_labels) { [label_1] } + let(:remove_labels) { [label_3] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(label_1.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_3.id) + end + + it 'does not update issues not passed in' do + expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + end + end + + context 'when add_label_ids and label_ids are passed' do + let(:issues) { [issue_all_labels, issue_labels_1_and_2, issue_labels_1_and_3] } + let(:labels) { [label_3] } + let(:add_labels) { [label_2] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(label_2.id)) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(label_1.id)) + end + + it 'does not update issues not passed in' do + expect(issue_no_labels.label_ids).to be_empty + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:issues) { [issue_no_labels, issue_labels_1_and_2] } + let(:labels) { [label_3] } + let(:remove_labels) { [label_2] } + + it 'remove the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_2.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_3.id) + end + + it 'does not update issues not passed in' do + expect(issue_all_labels.label_ids).to contain_exactly(label_1.id, label_2.id, label_3.id) + end + end + + context 'when add_label_ids, remove_label_ids, and label_ids are passed' do + let(:issues) { [issue_labels_1_and_3, issue_no_labels] } + let(:labels) { [label_2] } + let(:add_labels) { [label_1] } + let(:remove_labels) { [label_3] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(label_1.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_3.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_2.id) + end + + it 'does not update issues not passed in' do + expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + end + end + end end From 0fcf6e109454e3a3ac6b8179ca241bad58975136 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 2 May 2016 17:36:25 +0100 Subject: [PATCH 213/507] Tidy up BulkUpdateService specs 1. Don't use instance variables, use `let` instead. 2. Add descriptions for all specs. 3. Share variables where possible. 4. Give labels more vivid names than 1, 2, and 3. 5. Remove deprecation warnings by passing issue IDs as '1,2,3' instead of an array, as that's how they're passed by the front-end. (The deprecation warning is for passing a nested array, which is what happens if an actual array is passed, as: `[1, 2, 3].split(',') == [[1, 2, 3]]` --- .../issues/bulk_update_service_spec.rb | 228 +++++++++--------- 1 file changed, 111 insertions(+), 117 deletions(-) diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 09a1415abf..91d0863695 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -1,36 +1,28 @@ require 'spec_helper' describe Issues::BulkUpdateService, services: true do - let(:issue) { create(:issue, project: @project) } + let(:user) { create(:user) } + let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute } - before do - @user = create :user - opts = { - name: "GitLab", - namespace: @user.namespace - } - @project = Projects::CreateService.new(@user, opts).execute - end - - describe :close_issue do + let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute } before do @issues = create_list(:issue, 5, project: @project) @params = { state_event: 'close', - issues_ids: @issues.map(&:id).join(",") + issues_ids: issues.map(&:id).join(',') } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute + it 'succeeds and returns the correct number of issues updated' do expect(result[:success]).to be_truthy - expect(result[:count]).to eq(@issues.count) - - expect(@project.issues.opened).to be_empty - expect(@project.issues.closed).not_to be_empty + expect(result[:count]).to eq(issues.count) end + it 'closes all the issues passed' do + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty + end end describe :reopen_issues do @@ -38,95 +30,99 @@ describe Issues::BulkUpdateService, services: true do @issues = create_list(:closed_issue, 5, project: @project) @params = { state_event: 'reopen', - issues_ids: @issues.map(&:id).join(",") + issues_ids: issues.map(&:id).join(',') } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute + it 'succeeds and returns the correct number of issues updated' do expect(result[:success]).to be_truthy - expect(result[:count]).to eq(@issues.count) - - expect(@project.issues.closed).to be_empty - expect(@project.issues.opened).not_to be_empty + expect(result[:count]).to eq(issues.count) end - end - - describe :update_assignee do - - before do - @new_assignee = create :user - @params = { - issues_ids: issue.id.to_s, - assignee_id: @new_assignee.id - } - end - - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) - - expect(@project.issues.first.assignee).to eq(@new_assignee) - end - - it 'allows mass-unassigning' do - @project.issues.first.update_attribute(:assignee, @new_assignee) - expect(@project.issues.first.assignee).not_to be_nil - - @params[:assignee_id] = -1 - - Issues::BulkUpdateService.new(@project, @user, @params).execute - expect(@project.issues.first.assignee).to be_nil - end - - it 'does not unassign when assignee_id is not present' do - @project.issues.first.update_attribute(:assignee, @new_assignee) - expect(@project.issues.first.assignee).not_to be_nil - - @params[:assignee_id] = '' - - Issues::BulkUpdateService.new(@project, @user, @params).execute - expect(@project.issues.first.assignee).not_to be_nil + it 'reopens all the issues passed' do + expect(project.issues.closed).to be_empty + expect(project.issues.opened).not_to be_empty end end - describe :update_milestone do + describe 'updating assignee' do + let(:issue) do + create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) } + end - before do - @milestone = create(:milestone, project: @project) - @params = { - issues_ids: issue.id.to_s, - milestone_id: @milestone.id + let(:params) do + { + assignee_id: assignee_id, + issues_ids: issue.id.to_s } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute + context 'when the new assignee ID is a valid user' do + let(:new_assignee) { create(:user) } + let(:assignee_id) { new_assignee.id } + + it 'succeeds' do + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end + + it 'updates the assignee to the use ID passed' do + expect(issue.reload.assignee).to eq(new_assignee) + end + end + + context 'when the new assignee ID is -1' do + let(:assignee_id) { -1 } + + it 'unassigns the issues' do + expect(issue.reload.assignee).to be_nil + end + end + + context 'when the new assignee ID is not present', focus: true do + let(:assignee_id) { nil } + + it 'does not unassign' do + expect(issue.reload.assignee).to eq(user) + end + end + end + + describe 'updating milestones' do + let(:issue) { create(:issue, project: project) } + let(:milestone) { create(:milestone, project: project) } + + let(:params) do + { + issues_ids: issue.id.to_s, + milestone_id: milestone.id + } + end + + it 'succeeds' do expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) + end - expect(@project.issues.first.milestone).to eq(@milestone) + it 'updates the issue milestone' do + expect(project.issues.first.milestone).to eq(milestone) end end describe 'updating labels' do def create_issue_with_labels(labels) - create(:issue, project: project) { |issue| issue.labels = labels } + create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) } end - let(:user) { create(:user) } - let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute } - let(:label_1) { create(:label, project: project) } - let(:label_2) { create(:label, project: project) } - let(:label_3) { create(:label, project: project) } + let(:bug) { create(:label, project: project) } + let(:regression) { create(:label, project: project) } + let(:merge_requests) { create(:label, project: project) } - let(:issue_all_labels) { create_issue_with_labels([label_1, label_2, label_3]) } - let(:issue_labels_1_and_2) { create_issue_with_labels([label_1, label_2]) } - let(:issue_labels_1_and_3) { create_issue_with_labels([label_1, label_3]) } + let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } + let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) } + let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) } let(:issue_no_labels) { create(:issue, project: project) } - let(:issues) { [issue_all_labels, issue_labels_1_and_2, issue_labels_1_and_3, issue_no_labels] } + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } let(:labels) { [] } let(:add_labels) { [] } @@ -141,76 +137,74 @@ describe Issues::BulkUpdateService, services: true do } end - before { Issues::BulkUpdateService.new(project, user, params).execute } - context 'when label_ids are passed' do let(:issues) { [issue_all_labels, issue_no_labels] } - let(:labels) { [label_1, label_2] } + let(:labels) { [bug, regression] } it 'updates the labels of all issues passed to the labels passed' do expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id))) end it 'does not update issues not passed in' do - expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) end end context 'when add_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_labels_1_and_3, issue_no_labels] } - let(:add_labels) { [label_1, label_2, label_3] } + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug, regression, merge_requests] } it 'adds those label IDs to all issues passed' do expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) end it 'does not update issues not passed in' do - expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) end end context 'when remove_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_labels_1_and_3, issue_no_labels] } - let(:remove_labels) { [label_1, label_2, label_3] } + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:remove_labels) { [bug, regression, merge_requests] } it 'removes those label IDs from all issues passed' do expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) end it 'does not update issues not passed in' do - expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) end end context 'when add_label_ids and remove_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_labels_1_and_3, issue_no_labels] } - let(:add_labels) { [label_1] } - let(:remove_labels) { [label_3] } + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(label_1.id)) + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) end it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_3.id) + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) end it 'does not update issues not passed in' do - expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) end end context 'when add_label_ids and label_ids are passed' do - let(:issues) { [issue_all_labels, issue_labels_1_and_2, issue_labels_1_and_3] } - let(:labels) { [label_3] } - let(:add_labels) { [label_2] } + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } + let(:labels) { [merge_requests] } + let(:add_labels) { [regression] } it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(label_2.id)) + expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) end it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(label_1.id)) + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) end it 'does not update issues not passed in' do @@ -219,43 +213,43 @@ describe Issues::BulkUpdateService, services: true do end context 'when remove_label_ids and label_ids are passed' do - let(:issues) { [issue_no_labels, issue_labels_1_and_2] } - let(:labels) { [label_3] } - let(:remove_labels) { [label_2] } + let(:issues) { [issue_no_labels, issue_bug_and_regression] } + let(:labels) { [merge_requests] } + let(:remove_labels) { [regression] } it 'remove the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_2.id) + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) end it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_3.id) + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) end it 'does not update issues not passed in' do - expect(issue_all_labels.label_ids).to contain_exactly(label_1.id, label_2.id, label_3.id) + expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) end end context 'when add_label_ids, remove_label_ids, and label_ids are passed' do - let(:issues) { [issue_labels_1_and_3, issue_no_labels] } - let(:labels) { [label_2] } - let(:add_labels) { [label_1] } - let(:remove_labels) { [label_3] } + let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } + let(:labels) { [regression] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(label_1.id)) + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) end it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_3.id) + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) end it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(label_2.id) + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) end it 'does not update issues not passed in' do - expect(issue_labels_1_and_2.label_ids).to contain_exactly(label_1.id, label_2.id) + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) end end end From 116f5f26ea8e8f824caa89d1cb407b1082e32399 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 3 May 2016 17:03:53 -0500 Subject: [PATCH 214/507] Fix statement --- app/services/issuable_base_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 969c87a6ed..fbe9b7d7f1 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -80,7 +80,7 @@ class IssuableBaseService < BaseService def filter_labels_by_name(keys) keys.each do |key| - return if params[key].to_a.empty? + next if params[key].to_a.empty? params[key] = project.labels.where(id: params[key]).pluck(:id) end From 5d7445198fd830c093d5242bc5651d68b07bb079 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 5 May 2016 16:42:34 -0500 Subject: [PATCH 215/507] Only filter asynchronously if option remote is true. We need to update the results right away when we do bulk assignment. --- app/assets/javascripts/gl_dropdown.js.coffee | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 1e14486174..e98a8a561d 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -37,20 +37,20 @@ class GitLabDropdownFilter if keyCode is 13 return false - clearTimeout timeout - timeout = setTimeout => - blur_field = @shouldBlur keyCode - search_text = @input.val() + # Only filter asynchronously only if option remote is set + if @options.remote + clearTimeout timeout + timeout = setTimeout => + blur_field = @shouldBlur keyCode - if blur_field and @filterInputBlur - @input.blur() + if blur_field and @filterInputBlur + @input.blur() - if @options.remote - @options.query search_text, (data) => + @options.query @input.val(), (data) => @options.callback(data) - else - @filter search_text - , 250 + , 250 + else + @filter @input.val() shouldBlur: (keyCode) -> return BLUR_KEYCODES.indexOf(keyCode) >= 0 From 9df4da4bc08b90ff9c8a1d4ab28670e4615616d8 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 5 May 2016 16:44:35 -0500 Subject: [PATCH 216/507] Remove unneeded call to keyup event This was re-rendering the dropdown unnecessarily --- app/assets/javascripts/gl_dropdown.js.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index e98a8a561d..fd51d5ee5f 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -185,9 +185,6 @@ class GitLabDropdown @fullData = data @parseData @fullData - - if @options.filterable - @filterInput.trigger 'keyup' } # Init filterable From 0be26a47f20d0a536e8600a22e61ff7825983414 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 5 May 2016 16:47:50 -0500 Subject: [PATCH 217/507] Do not trigger keyup event if we are persisting state Triggering keyup will re-render the dropdown which is not needed when option persistWhenHide is true --- app/assets/javascripts/gl_dropdown.js.coffee | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index fd51d5ee5f..5897502ed5 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -317,12 +317,17 @@ class GitLabDropdown hidden: (e) => @removeArrayKeyEvent() + $input = @dropdown.find(".dropdown-input-field") + if @options.filterable - @dropdown - .find(".dropdown-input-field") + $input .blur() .val("") - .trigger("keyup") + + # Triggering 'keyup' will re-render the dropdown which is not always required + # specially if we want to keep the state of the dropdown needed for bulk-assignment + if not @options.persistWhenHide + $input.trigger("keyup") if @dropdown.find(".dropdown-toggle-page").length $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS From d09114e4eda026fd695a758a52cf990b6afc23f4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 5 May 2016 16:57:35 -0500 Subject: [PATCH 218/507] Add show_footer parameter for ability to hide footer --- app/views/shared/issuable/_label_dropdown.html.haml | 1 + app/views/shared/issuable/_label_page_default.html.haml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 5e25d83866..bf6fac3430 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -20,5 +20,6 @@ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" } - if can? current_user, :admin_label, @project and @project + = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer } = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index 7f4867417f..e5be3751d2 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -1,10 +1,11 @@ - title = local_assigns.fetch(:title, 'Assign labels') +- show_footer = local_assigns.fetch(:show_footer, true) - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') .dropdown-page-one = dropdown_title(title) = dropdown_filter(filter_placeholder) = dropdown_content - - if @project + - if @project and show_footer = dropdown_footer do %ul.dropdown-footer-list - if can? current_user, :admin_label, @project From 90c2ab02b5a5f6a63e419c3616dc6f5b00c0a68d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 5 May 2016 17:08:23 -0500 Subject: [PATCH 219/507] Add show_create param to toggle label creation option --- app/views/shared/issuable/_label_dropdown.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index bf6fac3430..b095fd1962 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,3 +1,4 @@ +- show_create = local_assigns.fetch(:show_create, true) - extra_options = local_assigns.fetch(:extra_options, true) - filter_submit = local_assigns.fetch(:filter_submit, true) - show_footer = local_assigns.fetch(:show_footer, true) @@ -18,8 +19,7 @@ = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" } - - if can? current_user, :admin_label, @project and @project = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer } + - if can? current_user, :admin_label, @project and @project and show_create = render partial: "shared/issuable/label_page_create" = dropdown_loading From 7ee3fc8080ede0e1c76f02a70cb242b961605a94 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 5 May 2016 17:09:11 -0500 Subject: [PATCH 220/507] Expose label ID --- app/views/projects/labels/_label.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 8bf544b837..a36535458f 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,4 +1,4 @@ -%li{id: dom_id(label)} +%li{id: dom_id(label), :"data-id" => label.id} = render "shared/label_row", label: label .pull-info-right From 151158f1d775de477a767fd93c34197d9349cd1a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 5 May 2016 17:09:35 -0500 Subject: [PATCH 221/507] Expose Issue ID --- app/views/projects/issues/_issue.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index ebaa34f4ff..c22f5e150e 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,4 +1,4 @@ -%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), :'data-labels' => label_ids(issue) } +%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: label_ids(issue), id: issue.id } } - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" From f02ee98697ba2829bf092d662c24e2e553832fe9 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 5 May 2016 17:10:12 -0500 Subject: [PATCH 222/507] Tweaks for bulk assignment --- app/assets/javascripts/labels_select.js.coffee | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 52c1e8a469..eb249e24f6 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -237,13 +237,23 @@ class @LabelsSelect else colorEl = '' + # We need to identify which items are actually labels + labelIdHtml = labelClass = '' + + if label.id + # Add label id only for labels + labelIdHtml = "data-label-id='#{label.id}'" + + # Add class only for labels + labelClass = 'label-item' + "
                                    • - + #{colorEl} #{_.escape(label.title)}
                                    • " - filterable: true + persistWhenHide: $dropdown.data('persistWhenHide') search: fields: ['title'] selectable: true @@ -291,7 +301,9 @@ class @LabelsSelect saveLabelData() if $dropdown.hasClass('js-filter-bulk-update') - $dropdown.parent().find('.is-active, .is-indeterminate').removeClass() + # If we are persisting state we need the classes + if not @options.persistWhenHide + $dropdown.parent().find('.is-active, .is-indeterminate').removeClass() multiSelect: $dropdown.hasClass 'js-multiselect' clicked: (label) -> From f1291b1b9f28f2ee32d7bbc63b7b57db10b03006 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 5 May 2016 17:10:54 -0500 Subject: [PATCH 223/507] Bulk assignment implementation --- app/assets/javascripts/dispatcher.js.coffee | 1 + .../issues-bulk-assignment.js.coffee | 108 ++++++++++++++++++ app/views/shared/issuable/_filter.html.haml | 4 +- 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/issues-bulk-assignment.js.coffee diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index cd418ad244..bae67a2eba 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -17,6 +17,7 @@ class Dispatcher switch page when 'projects:issues:index' Issuable.init() + new IssuableBulkActions() shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' new Issue() diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee new file mode 100644 index 0000000000..da463a586c --- /dev/null +++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee @@ -0,0 +1,108 @@ +class @IssuableBulkActions + constructor: (opts = {}) -> + # Set defaults + { + @container = $('.content') + @form = @getElement('.bulk-update') + @issues = @getElement('.issues-list .issue') + } = opts + + @bindEvents() + + getElement: (selector) -> + @container.find selector + + bindEvents: -> + @form.on 'submit', @onFormSubmit.bind(@) + + onFormSubmit: (e) -> + e.preventDefault() + @submit() + + submit: -> + _this = @ + + xhr = $.ajax + url: @form.attr 'action' + method: @form.attr 'method' + dataType: 'JSON', + data: @getFormDataAsObject() + + xhr.done (response, status, xhr) -> + Turbolinks.visit(location.href) + + xhr.fail -> + console.error 'fail' + + xhr.always -> + _this.onFormSubmitAlways() + + onFormSubmitAlways: -> + @form.find('[type="submit"]').enable() + + getSelectedIssues: -> + @issues.has('.selected_issue:checked') + + getLabelsFromSelection: -> + labels = [] + + @getSelectedIssues().map -> + _labels = $(@).data('labels') + console.log _labels + if _labels + _labels.map (labelId)-> + labels.push(labelId) if labels.indexOf(labelId) is -1 + + labels + + ###* + * Will return only labels that were marked previously and the user has unmarked + * @return {Array} Label IDs + ### + getUnmarkedIndeterminedLabels: -> + result = [] + labelsToKeep = [] + + for el in @getElement('.labels-filter .is-indeterminate') + labelsToKeep.push $(el).data('labelId') + + for id in @getLabelsFromSelection() + # Only the ones that we are not going to keep + result.push(id) if labelsToKeep.indexOf(id) is -1 + + result + + ###* + * Simple form serialization, it will return just what we need + * Returns key/value pairs from form data + ### + getFormDataAsObject: -> + formData = + update: + issues_ids: @form.find('#update_issues_ids').val() + add_label_ids: [] + remove_label_ids: [] + + for id in @getLabelsToApply() + formData.update.add_label_ids.push id + + for id in @getLabelsToRemove() + formData.update.remove_label_ids.push id + + formData + + getLabelsToApply: -> + labelIds = [] + $labels = @form.find('.labels-filter input[name="update[label_ids][]"]') + + for label in $labels + labelIds.push $(label).val() if label + + labelIds + + ###* + * Just an alias of @getUnmarkedIndeterminedLabels + * @return {Array} Array of labels + ### + getLabelsToRemove: -> + @getUnmarkedIndeterminedLabels() diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0ca4b9a681..8758de12f4 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -31,7 +31,7 @@ - if controller.controller_name == 'issues' .issues_bulk_update.hide - = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do + = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do .filter-item.inline = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul @@ -46,7 +46,7 @@ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown", classes: ' js-filter-bulk-update js-multiselect ', extra_options: false, filter_submit: false, show_footer: false, extra_options: false, data_options: { field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = render "shared/issuable/label_dropdown", classes: ' js-filter-bulk-update js-multiselect ', show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] From dbf9df1dd2719acaa0c540efdc37502297f34a92 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 6 May 2016 15:22:44 -0500 Subject: [PATCH 224/507] Remove console.log --- app/assets/javascripts/issues-bulk-assignment.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee index da463a586c..72c4a6aa4b 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js.coffee +++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee @@ -48,7 +48,6 @@ class @IssuableBulkActions @getSelectedIssues().map -> _labels = $(@).data('labels') - console.log _labels if _labels _labels.map (labelId)-> labels.push(labelId) if labels.indexOf(labelId) is -1 From d84f1180d8687c694f64f76a5a91a3dae7d178bf Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 6 May 2016 15:58:21 -0500 Subject: [PATCH 225/507] Bulk assignment tests --- .../issues/bulk_assigment_labels_spec.rb | 158 +++++++++++++----- 1 file changed, 120 insertions(+), 38 deletions(-) diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assigment_labels_spec.rb index b99729ffd0..df080a4c66 100644 --- a/spec/features/issues/bulk_assigment_labels_spec.rb +++ b/spec/features/issues/bulk_assigment_labels_spec.rb @@ -3,80 +3,147 @@ require 'rails_helper' feature 'Issues > Labels bulk assignment', feature: true do include WaitForAjax - let(:user) { create(:user) } - let!(:project) { create(:project) } - let!(:issue1) { create(:issue, project: project, title: "Issue 1") } - let!(:issue2) { create(:issue, project: project, title: "Issue 2") } - - before do - create(:label, project: project, title: 'bug') - create(:label, project: project, title: 'feature') - end + let(:user) { create(:user) } + let!(:project) { create(:project) } + let!(:issue1) { create(:issue, project: project, title: "Issue 1") } + let!(:issue2) { create(:issue, project: project, title: "Issue 2") } + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:feature) { create(:label, project: project, title: 'feature') } context 'as a allowed user', js: true do before do project.team << [user, :master] + login_as user - - visit namespace_project_issues_path(project.namespace, project) end - context 'can bulk assign a label' do - context 'to all issues' do - before do - check 'check_all_issues' - open_labels_dropdown ['bug'] - click_button 'Update issues' + context 'can bulk assign' do + before do + visit namespace_project_issues_path(project.namespace, project) + end + + context 'a label' do + context 'to all issues' do + before do + check 'check_all_issues' + open_labels_dropdown ['bug'] + click_button 'Update issues' + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'bug' + end end - it do - expect(find("#issue_#{issue1.id}")).to have_content 'bug' - expect(find("#issue_#{issue2.id}")).to have_content 'bug' + context 'to a issue' do + before do + check "selected_issue_#{issue1.id}" + open_labels_dropdown ['bug'] + click_button 'Update issues' + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + end end end - context 'to a issue' do + context 'multiple labels' do + context 'to all issues' do + before do + check 'check_all_issues' + open_labels_dropdown ['bug', 'feature'] + click_button 'Update issues' + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'feature' + end + end + + context 'to a issue' do + before do + check "selected_issue_#{issue1.id}" + open_labels_dropdown ['bug', 'feature'] + click_button 'Update issues' + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + end + end + end + end + + context 'can bulk un-assign' do + context 'all labels to all issues' do before do - check "selected_issue_#{issue1.id}" - open_labels_dropdown ['bug'] + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + issue2.labels << feature + + visit namespace_project_issues_path(project.namespace, project) + + check 'check_all_issues' + unmark_labels_in_dropdown ['bug', 'feature'] click_button 'Update issues' end it do - expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).not_to have_content 'feature' expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' end end - end - context 'can bulk assign multiple labels' do - context 'to all issues' do + context 'a label to a issue' do before do - check 'check_all_issues' - open_labels_dropdown ['bug', 'feature'] + issue1.labels << bug + issue2.labels << feature + + visit namespace_project_issues_path(project.namespace, project) + + check_issue issue1 + unmark_labels_in_dropdown ['bug'] click_button 'Update issues' end it do - expect(find("#issue_#{issue1.id}")).to have_content 'bug' - expect(find("#issue_#{issue1.id}")).to have_content 'feature' - expect(find("#issue_#{issue2.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' expect(find("#issue_#{issue2.id}")).to have_content 'feature' end end - context 'to a issue' do + context 'a label and keep the others label' do before do - check "selected_issue_#{issue1.id}" - open_labels_dropdown ['bug', 'feature'] + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + issue2.labels << feature + + visit namespace_project_issues_path(project.namespace, project) + + check_issue issue1 + check_issue issue2 + unmark_labels_in_dropdown ['bug'] click_button 'Update issues' end it do - expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' expect(find("#issue_#{issue1.id}")).to have_content 'feature' expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' - expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'feature' end end end @@ -97,13 +164,28 @@ feature 'Issues > Labels bulk assignment', feature: true do end end - def open_labels_dropdown(items = []) + def open_labels_dropdown(items = [], unmark = false) page.within('.issues_bulk_update') do click_button 'Label' wait_for_ajax items.map do |item| click_link item end + if unmark + items.map do |item| + click_link item + end + end + end + end + + def unmark_labels_in_dropdown(items = []) + open_labels_dropdown(items, true) + end + + def check_issue(issue) + page.within('.issues-list') do + check "selected_issue_#{issue.id}" end end end From 830ccdfd3e1bb6d4b0c333fd2e7931cf65dac4b9 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 9 May 2016 15:53:33 -0500 Subject: [PATCH 226/507] Fix failing specs --- spec/features/issues/update_issues_spec.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index 466a6f7dfa..c109de1cbd 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' feature 'Multiple issue updating from issues#index', feature: true do + include WaitForAjax + let!(:project) { create(:project) } let!(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} @@ -24,9 +26,7 @@ feature 'Multiple issue updating from issues#index', feature: true do it 'should be set to open' do create_closed - visit namespace_project_issues_path(project.namespace, project) - - find('.issues-state-filters a', text: 'Closed').click + visit namespace_project_issues_path(project.namespace, project, state: 'closed') find('#check_all_issues').click find('.js-issue-status').click @@ -61,8 +61,8 @@ feature 'Multiple issue updating from issues#index', feature: true do click_link 'Unassigned' click_update_issues_button - - within first('.issue .controls') do + sleep 1 # needed + page.within first('.issue .controls') do expect(page).to have_no_selector('.author_link') end end @@ -95,7 +95,8 @@ feature 'Multiple issue updating from issues#index', feature: true do find('.dropdown-menu-milestone a', text: "No Milestone").click click_update_issues_button - expect(first('.issue')).not_to have_content milestone.title + sleep 1 # needed + expect(first('.issue')).to_not have_content milestone.title end end @@ -113,5 +114,6 @@ feature 'Multiple issue updating from issues#index', feature: true do def click_update_issues_button find('.update_selected_issues').click + wait_for_ajax end end From 90bf5aa8f8feff5ceeaee1d1a245c4f894a4feca Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 9 May 2016 15:54:12 -0500 Subject: [PATCH 227/507] Add mising params --- app/assets/javascripts/issues-bulk-assignment.js.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee index 72c4a6aa4b..837661aef6 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js.coffee +++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee @@ -78,9 +78,12 @@ class @IssuableBulkActions getFormDataAsObject: -> formData = update: - issues_ids: @form.find('#update_issues_ids').val() - add_label_ids: [] - remove_label_ids: [] + state_event : @form.find('input[name="update[state_event]"]').val() + assignee_id : @form.find('input[name="update[assignee_id]"]').val() + milestone_id : @form.find('input[name="update[milestone_id]"]').val() + issues_ids : @form.find('input[name="update[issues_ids]"]').val() + add_label_ids : [] + remove_label_ids : [] for id in @getLabelsToApply() formData.update.add_label_ids.push id From 73b528f9a34767268af199ea0231e659491b03e4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 9 May 2016 15:54:20 -0500 Subject: [PATCH 228/507] Fix spec --- app/assets/javascripts/issues-bulk-assignment.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee index 837661aef6..c7dbda064e 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js.coffee +++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee @@ -29,7 +29,7 @@ class @IssuableBulkActions data: @getFormDataAsObject() xhr.done (response, status, xhr) -> - Turbolinks.visit(location.href) + location.reload() xhr.fail -> console.error 'fail' From 812ae973db7f6fa72246c975fb4eff6b1a95c886 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 9 May 2016 15:54:46 -0500 Subject: [PATCH 229/507] Respond to .json only --- app/controllers/projects/issues_controller.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 855b4cfa09..4e2d3bebb2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -156,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController def bulk_update result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute - redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) + + respond_to do |format| + format.json do + render json: { notice: "#{result[:count]} issues updated" } + end + end end protected From 15108cbaac059b341eae9d733c5ff8cb1dfbdb28 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 10 May 2016 18:11:16 -0500 Subject: [PATCH 230/507] Fix spec --- spec/features/issues/update_issues_spec.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index c109de1cbd..c6181eed9a 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -42,7 +42,7 @@ feature 'Multiple issue updating from issues#index', feature: true do visit namespace_project_issues_path(project.namespace, project) find('#check_all_issues').click - find('.js-update-assignee').click + click_update_assignee_button find('.dropdown-menu-user-link', text: user.username).click click_update_issues_button @@ -57,14 +57,11 @@ feature 'Multiple issue updating from issues#index', feature: true do visit namespace_project_issues_path(project.namespace, project) find('#check_all_issues').click - find('.js-update-assignee').click + click_update_assignee_button click_link 'Unassigned' click_update_issues_button - sleep 1 # needed - page.within first('.issue .controls') do - expect(page).to have_no_selector('.author_link') - end + expect(find('.issue:first-child .controls')).not_to have_css('.author_link') end end @@ -95,8 +92,7 @@ feature 'Multiple issue updating from issues#index', feature: true do find('.dropdown-menu-milestone a', text: "No Milestone").click click_update_issues_button - sleep 1 # needed - expect(first('.issue')).to_not have_content milestone.title + expect(find('.issue:first-child')).to_not have_content milestone.title end end @@ -112,6 +108,11 @@ feature 'Multiple issue updating from issues#index', feature: true do create(:issue, project: project, milestone: milestone) end + def click_update_assignee_button + find('.js-update-assignee').click + wait_for_ajax + end + def click_update_issues_button find('.update_selected_issues').click wait_for_ajax From 2ee779f17d5fa318bf54e1bab9023bc7f74f0f9d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 10 May 2016 18:22:18 -0500 Subject: [PATCH 231/507] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d54cac7f93..d4885dbe8b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -164,6 +164,7 @@ v 8.7.6 - Fix import from GitLab.com to a private instance failure. !4181 - Fix external imports not finding the import data. !4106 - Fix notification delay when changing status of an issue + - Bulk assign/unassign labels to issues. v 8.7.5 - Fix relative links in wiki pages. !4050 From 5ea01651758beee85003bafd9ebea767090cb9f1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 13 May 2016 00:56:37 -0500 Subject: [PATCH 232/507] Enhancements --- app/assets/javascripts/flash.js.coffee | 2 +- app/assets/javascripts/gl_dropdown.js.coffee | 16 +++++++--------- .../issues-bulk-assignment.js.coffee | 13 ++++++------- .../issues/bulk_assigment_labels_spec.rb | 19 ++++++++++++------- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index 5de012e409..4f73d215b8 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,5 +1,5 @@ class @Flash - constructor: (message, type)-> + constructor: (message, type = 'alert')-> @flash = $(".flash-container") @flash.html("") diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 5897502ed5..4b8e4bb490 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -460,10 +460,10 @@ class GitLabDropdown el.addClass ACTIVE_CLASS el.removeClass INDETERMINATE_CLASS - if !value? + if not value? field.remove() - if !field.length and fieldName + if not field.length and fieldName @addInput(fieldName, value) return selectedObject @@ -491,14 +491,12 @@ class GitLabDropdown addInput: (fieldName, value)-> # Create hidden input for form - input = "" - if @options.inputId? - input = $(input) - .attr('id', @options.inputId) - @dropdown.before input + $input = $("") - removeInputs: -> - @dropdown.parent().find('input[type="hidden"]').remove() + if @options.inputId? + $input.attr('id', @options.inputId) + + @dropdown.before $input selectRowAtIndex: (e, index) -> selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a" diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee index c7dbda064e..40751c81fd 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js.coffee +++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee @@ -32,10 +32,9 @@ class @IssuableBulkActions location.reload() xhr.fail -> - console.error 'fail' + new Flash("Issue update failed") - xhr.always -> - _this.onFormSubmitAlways() + xhr.always @onFormSubmitAlways.bind(@) onFormSubmitAlways: -> @form.find('[type="submit"]').enable() @@ -49,7 +48,7 @@ class @IssuableBulkActions @getSelectedIssues().map -> _labels = $(@).data('labels') if _labels - _labels.map (labelId)-> + _labels.map (labelId) -> labels.push(labelId) if labels.indexOf(labelId) is -1 labels @@ -85,10 +84,10 @@ class @IssuableBulkActions add_label_ids : [] remove_label_ids : [] - for id in @getLabelsToApply() + @getLabelsToApply().map (id) -> formData.update.add_label_ids.push id - for id in @getLabelsToRemove() + @getLabelsToRemove().map (id) -> formData.update.remove_label_ids.push id formData @@ -97,7 +96,7 @@ class @IssuableBulkActions labelIds = [] $labels = @form.find('.labels-filter input[name="update[label_ids][]"]') - for label in $labels + $labels.each (k, label) -> labelIds.push $(label).val() if label labelIds diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assigment_labels_spec.rb index df080a4c66..c58b87281a 100644 --- a/spec/features/issues/bulk_assigment_labels_spec.rb +++ b/spec/features/issues/bulk_assigment_labels_spec.rb @@ -27,7 +27,7 @@ feature 'Issues > Labels bulk assignment', feature: true do before do check 'check_all_issues' open_labels_dropdown ['bug'] - click_button 'Update issues' + update_issues end it do @@ -40,7 +40,7 @@ feature 'Issues > Labels bulk assignment', feature: true do before do check "selected_issue_#{issue1.id}" open_labels_dropdown ['bug'] - click_button 'Update issues' + update_issues end it do @@ -55,7 +55,7 @@ feature 'Issues > Labels bulk assignment', feature: true do before do check 'check_all_issues' open_labels_dropdown ['bug', 'feature'] - click_button 'Update issues' + update_issues end it do @@ -70,7 +70,7 @@ feature 'Issues > Labels bulk assignment', feature: true do before do check "selected_issue_#{issue1.id}" open_labels_dropdown ['bug', 'feature'] - click_button 'Update issues' + update_issues end it do @@ -95,7 +95,7 @@ feature 'Issues > Labels bulk assignment', feature: true do check 'check_all_issues' unmark_labels_in_dropdown ['bug', 'feature'] - click_button 'Update issues' + update_issues end it do @@ -115,7 +115,7 @@ feature 'Issues > Labels bulk assignment', feature: true do check_issue issue1 unmark_labels_in_dropdown ['bug'] - click_button 'Update issues' + update_issues end it do @@ -136,7 +136,7 @@ feature 'Issues > Labels bulk assignment', feature: true do check_issue issue1 check_issue issue2 unmark_labels_in_dropdown ['bug'] - click_button 'Update issues' + update_issues end it do @@ -188,4 +188,9 @@ feature 'Issues > Labels bulk assignment', feature: true do check "selected_issue_#{issue.id}" end end + + def update_issues + click_button 'Update issues' + wait_for_ajax + end end From d78fd6df0c21f56887d1ca76cb5a40c16d3552b0 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 23 May 2016 12:02:53 -0500 Subject: [PATCH 233/507] Update CHANGELOG --- CHANGELOG | 1 + spec/features/issues/update_issues_spec.rb | 2 +- spec/services/issues/bulk_update_service_spec.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d4885dbe8b..29926fad7c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -74,6 +74,7 @@ v 8.8.2 - Fixed issue with enter key selecting wrong option in dropdown. !4210 - When creating a .gitignore file a dropdown with templates will be provided. !4075 - Fix concurrent request when updating build log in browser. !4183 + - Bulk assign/unassign labels to issues. v 8.8.1 - Add documentation for the "Health Check" feature diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index c6181eed9a..ddbd69b289 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -92,7 +92,7 @@ feature 'Multiple issue updating from issues#index', feature: true do find('.dropdown-menu-milestone a', text: "No Milestone").click click_update_issues_button - expect(find('.issue:first-child')).to_not have_content milestone.title + expect(find('.issue:first-child')).not_to have_content milestone.title end end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 91d0863695..c777d475dd 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -79,7 +79,7 @@ describe Issues::BulkUpdateService, services: true do end end - context 'when the new assignee ID is not present', focus: true do + context 'when the new assignee ID is not present' do let(:assignee_id) { nil } it 'does not unassign' do From 165d799fb3ca36768497d964619ceeacf2deeae3 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 31 May 2016 11:14:15 +0100 Subject: [PATCH 234/507] Allow bulk-updating to remove all labels Instead of passing `remove_label_ids`, just pass an empty array for `label_ids` (and don't pass `add_label_ids` or `remove_label_ids`). --- app/services/issues/bulk_update_service.rb | 2 +- spec/services/issues/bulk_update_service_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index 2772add1d0..15825b8168 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -4,7 +4,7 @@ module Issues issues_ids = params.delete(:issues_ids).split(",") issue_params = params - [:state_event, :milestone_id, :assignee_id, :label_ids, :add_label_ids, :remove_label_ids].each do |key| + %i(state_event milestone_id assignee_id add_label_ids remove_label_ids).each do |key| issue_params.delete(key) unless issue_params[key].present? end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index c777d475dd..ad19fe0f15 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -148,6 +148,14 @@ describe Issues::BulkUpdateService, services: true do it 'does not update issues not passed in' do expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) end + + context 'when those label IDs are empty' do + let(:labels) { [] } + + it 'updates the issues passed to have no labels' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + end end context 'when add_label_ids are passed' do From 071ad63630c2f8f9666c0c1fae6b6b3491e3cf9f Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 31 May 2016 12:45:55 +0100 Subject: [PATCH 235/507] Spec label add / delete in UpdateService --- app/services/issuable_base_service.rb | 17 ++++---- spec/services/issues/update_service_spec.rb | 46 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index fbe9b7d7f1..e3dc569152 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -72,18 +72,17 @@ class IssuableBaseService < BaseService if params[:add_label_ids].present? || params[:remove_label_ids].present? params.delete(:label_ids) - filter_labels_by_name([:add_label_ids, :remove_label_ids]) + filter_labels_in_param(:add_label_ids) + filter_labels_in_param(:remove_label_ids) else - filter_labels_by_name([:label_ids]) + filter_labels_in_param(:label_ids) end end - def filter_labels_by_name(keys) - keys.each do |key| - next if params[key].to_a.empty? + def filter_labels_in_param(key) + return if params[key].to_a.empty? - params[key] = project.labels.where(id: params[key]).pluck(:id) - end + params[key] = project.labels.where(id: params[key]).pluck(:id) end def update_issuable(issuable, attributes) @@ -94,7 +93,7 @@ class IssuableBaseService < BaseService issuable.label_ids |= add_label_ids if add_label_ids issuable.label_ids -= remove_label_ids if remove_label_ids - issuable.assign_attributes(attributes) + issuable.assign_attributes(attributes.merge(updated_by: current_user)) issuable.save end @@ -105,7 +104,7 @@ class IssuableBaseService < BaseService filter_params old_labels = issuable.labels.to_a - if params.present? && update_issuable(issuable, params.merge(updated_by: current_user)) + if params.present? && update_issuable(issuable, params) issuable.reset_events_cache handle_common_system_notes(issuable, old_labels: old_labels) handle_changes(issuable, old_labels: old_labels) diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index be19be1715..dacbcd8fb4 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe Issues::UpdateService, services: true do @@ -273,5 +274,50 @@ describe Issues::UpdateService, services: true do end end end + + context 'updating labels' do + let(:label3) { create(:label, project: project) } + let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload } + + context 'when add_label_ids and label_ids are passed' do + let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } } + + it 'ignores the label_ids parameter' do + expect(result.label_ids).not_to include(label.id) + end + + it 'adds the passed labels' do + expect(result.label_ids).to include(label3.id) + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:params) { { label_ids: [], remove_label_ids: [label.id] } } + + before { issue.update_attributes(labels: [label, label3]) } + + it 'ignores the label_ids parameter' do + expect(result.label_ids).not_to be_empty + end + + it 'removes the passed labels' do + expect(result.label_ids).not_to include(label.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } } + + before { issue.update_attributes(labels: [label]) } + + it 'adds the passed labels' do + expect(result.label_ids).to include(label3.id) + end + + it 'removes the passed labels' do + expect(result.label_ids).not_to include(label.id) + end + end + end end end From 228a68458716fe36ece59bd21567e2183b2ca313 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 31 May 2016 11:20:25 -0500 Subject: [PATCH 236/507] Update CHANGELOG --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 29926fad7c..e2ba6bc79d 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.9.0 (unreleased) + - Bulk assign/unassign labels to issues. - Allow enabling wiki page events from Webhook management UI - Make EmailsOnPushWorker use Sidekiq mailers queue - Fix wiki page events' webhook to point to the wiki repository @@ -74,7 +75,6 @@ v 8.8.2 - Fixed issue with enter key selecting wrong option in dropdown. !4210 - When creating a .gitignore file a dropdown with templates will be provided. !4075 - Fix concurrent request when updating build log in browser. !4183 - - Bulk assign/unassign labels to issues. v 8.8.1 - Add documentation for the "Health Check" feature @@ -165,7 +165,6 @@ v 8.7.6 - Fix import from GitLab.com to a private instance failure. !4181 - Fix external imports not finding the import data. !4106 - Fix notification delay when changing status of an issue - - Bulk assign/unassign labels to issues. v 8.7.5 - Fix relative links in wiki pages. !4050 From 22b8b9a7f399abb685e95ea5669beb033d30101b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 31 May 2016 16:11:46 -0500 Subject: [PATCH 237/507] Address feedback --- app/assets/javascripts/gl_dropdown.js.coffee | 4 +++- .../issues-bulk-assignment.js.coffee | 2 +- .../javascripts/labels_select.js.coffee | 21 ++++++++----------- app/helpers/issuables_helper.rb | 6 ------ app/views/projects/issues/_issue.html.haml | 2 +- app/views/projects/labels/_label.html.haml | 2 +- app/views/shared/issuable/_filter.html.haml | 2 +- .../shared/issuable/_label_dropdown.html.haml | 12 +++++------ .../issuable/_label_page_default.html.haml | 7 ++++--- 9 files changed, 26 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 4b8e4bb490..7c7334e9e4 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -491,7 +491,9 @@ class GitLabDropdown addInput: (fieldName, value)-> # Create hidden input for form - $input = $("") + $input = $('').attr('type', 'hidden') + .attr('name', fieldName) + .val(value) if @options.inputId? $input.attr('id', @options.inputId) diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee index 40751c81fd..16d023dd39 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js.coffee +++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee @@ -13,7 +13,7 @@ class @IssuableBulkActions @container.find selector bindEvents: -> - @form.on 'submit', @onFormSubmit.bind(@) + @form.off('submit').on('submit', @onFormSubmit.bind(@)) onFormSubmit: (e) -> e.preventDefault() diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index eb249e24f6..ec74dfaae1 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -199,6 +199,9 @@ class @LabelsSelect callback data renderRow: (label, instance) -> + $li = $('
                                    • ') + $a = $('') + selectedClass = [] removesAll = label.id is 0 or not label.id? @@ -238,21 +241,15 @@ class @LabelsSelect colorEl = '' # We need to identify which items are actually labels - labelIdHtml = labelClass = '' - if label.id - # Add label id only for labels - labelIdHtml = "data-label-id='#{label.id}'" + selectedClass.push('label-item') + $a.attr('data-label-id', label.id) - # Add class only for labels - labelClass = 'label-item' + $a.addClass(selectedClass.join(' ')) + .html("#{colorEl} #{_.escape(label.title)}") - "
                                    • - - #{colorEl} - #{_.escape(label.title)} - -
                                    • " + # Return generated html + $li.html($a).prop('outerHTML') persistWhenHide: $dropdown.data('persistWhenHide') search: fields: ['title'] diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index b1ff95f620..37b93f6314 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -96,10 +96,4 @@ module IssuablesHelper issuable.open? ? :opened : :closed end end - - def label_ids(issuable) - return nil if !issuable.labels.any? - issuable.labels.pluck :id - end - end diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index c22f5e150e..79b1481986 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,4 +1,4 @@ -%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: label_ids(issue), id: issue.id } } +%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } } - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index a36535458f..950f6e284b 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,4 +1,4 @@ -%li{id: dom_id(label), :"data-id" => label.id} +%li{id: dom_id(label), data: { id: label.id } } } = render "shared/label_row", label: label .pull-info-right diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 8758de12f4..380ab465bf 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -46,7 +46,7 @@ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown", classes: ' js-filter-bulk-update js-multiselect ', show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index b095fd1962..d34d28f673 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -3,23 +3,23 @@ - filter_submit = local_assigns.fetch(:filter_submit, true) - show_footer = local_assigns.fetch(:show_footer, true) - data_options = local_assigns.fetch(:data_options, {}) -- classes = local_assigns.fetch(:classes, '') +- classes = local_assigns.fetch(:classes, []) - dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"} - dropdown_data.merge!(data_options) -- classes << ' js-extra-options ' if extra_options -- classes << ' js-filter-submit ' if filter_submit +- classes << 'js-extra-options' if extra_options +- classes << 'js-filter-submit' if filter_submit - if params[:label_name].present? - if params[:label_name].respond_to?('any?') - params[:label_name].each do |label| = hidden_field_tag "label_name[]", label, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes, type: "button", data: dropdown_data} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %span.dropdown-toggle-text = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer } - - if can? current_user, :admin_label, @project and @project and show_create + = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create } + - if show_create and @project and can?(current_user, :admin_label, @project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index e5be3751d2..935a0193a2 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -1,4 +1,5 @@ - title = local_assigns.fetch(:title, 'Assign labels') +- show_create = local_assigns.fetch(:show_create, true) - show_footer = local_assigns.fetch(:show_footer, true) - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') .dropdown-page-one @@ -8,14 +9,14 @@ - if @project and show_footer = dropdown_footer do %ul.dropdown-footer-list - - if can? current_user, :admin_label, @project + - if can?(current_user, :admin_label, @project) %li %a.dropdown-toggle-page{href: "#"} Create new %li = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do - - if can? current_user, :admin_label, @project + - if show_create && @project && can?(current_user, :admin_label, @project) Manage labels - else View labels - = dropdown_loading \ No newline at end of file + = dropdown_loading From 7df4a3c53a654782c6ca2214d8a5bd452b52835b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 31 May 2016 16:33:18 -0500 Subject: [PATCH 238/507] Fix spec --- .../shared/issuable/_label_page_default.html.haml | 2 +- spec/services/issues/bulk_update_service_spec.rb | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index 935a0193a2..4e280c371a 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -6,7 +6,7 @@ = dropdown_title(title) = dropdown_filter(filter_placeholder) = dropdown_content - - if @project and show_footer + - if @project && show_footer = dropdown_footer do %ul.dropdown-footer-list - if can?(current_user, :admin_label, @project) diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index ad19fe0f15..4a689e64dc 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -6,9 +6,10 @@ describe Issues::BulkUpdateService, services: true do let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute } - before do - @issues = create_list(:issue, 5, project: @project) - @params = { + describe :close_issue do + let(:issues) { create_list(:issue, 5, project: project) } + let(:params) do + { state_event: 'close', issues_ids: issues.map(&:id).join(',') } @@ -26,9 +27,9 @@ describe Issues::BulkUpdateService, services: true do end describe :reopen_issues do - before do - @issues = create_list(:closed_issue, 5, project: @project) - @params = { + let(:issues) { create_list(:closed_issue, 5, project: project) } + let(:params) do + { state_event: 'reopen', issues_ids: issues.map(&:id).join(',') } From 94d9efaa8e59a93d7350e2e1f7a4f99ef93a5ab7 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 31 May 2016 19:36:00 -0500 Subject: [PATCH 239/507] Remove extra bracket --- app/views/projects/labels/_label.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 950f6e284b..294fec422c 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,6 +1,5 @@ -%li{id: dom_id(label), data: { id: label.id } } } +%li{ id: dom_id(label), data: { id: label.id } } = render "shared/label_row", label: label - .pull-info-right %span.append-right-20 = link_to_label(label, type: :merge_request) do From e6567bc13c3c92ed581e916306ecfb758e311f19 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 4 Jun 2016 00:24:33 +0200 Subject: [PATCH 240/507] Use gitlab-build-images for precache some of the dependencies --- .gitlab-ci.yml | 34 ++++++++++++---------------------- scripts/prepare_build.sh | 14 -------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9774602cbd..85bf783ace 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,8 @@ -image: "ruby:2.1" +image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.1 services: - mysql:latest - - redis:latest + - redis:alpine cache: key: "ruby21" @@ -112,26 +112,16 @@ rspec 17 20: *knapsack rspec 18 20: *knapsack rspec 19 20: *knapsack -spinach 0 20: *knapsack -spinach 1 20: *knapsack -spinach 2 20: *knapsack -spinach 3 20: *knapsack -spinach 4 20: *knapsack -spinach 5 20: *knapsack -spinach 6 20: *knapsack -spinach 7 20: *knapsack -spinach 8 20: *knapsack -spinach 9 20: *knapsack -spinach 10 20: *knapsack -spinach 11 20: *knapsack -spinach 12 20: *knapsack -spinach 13 20: *knapsack -spinach 14 20: *knapsack -spinach 15 20: *knapsack -spinach 16 20: *knapsack -spinach 17 20: *knapsack -spinach 18 20: *knapsack -spinach 19 20: *knapsack +spinach 0 10: *knapsack +spinach 1 10: *knapsack +spinach 2 10: *knapsack +spinach 3 10: *knapsack +spinach 4 10: *knapsack +spinach 5 10: *knapsack +spinach 6 10: *knapsack +spinach 7 10: *knapsack +spinach 8 10: *knapsack +spinach 9 10: *knapsack teaspoon: *exec rubocop: *exec diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 247383aa46..9540d7d128 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -12,20 +12,6 @@ retry() { } if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then - mkdir -p vendor - - # Install phantomjs package - pushd vendor - if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then - wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb - fi - dpkg -i phantomjs_1.9.8-0jessie_amd64.deb - popd - - # Try to install packages - retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ - libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip' - cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml sed -i 's/password:.*/password:/g' config/database.yml From 1faed033ce12fb5a7190a3393e43cbea0154cb8a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 4 Jun 2016 20:43:18 +0200 Subject: [PATCH 241/507] Disable Rails/UniqBeforePluck rubocop cop --- .rubocop.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 3593ae29f2..bbe4e1ece3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1088,6 +1088,9 @@ Rails/TimeZone: Rails/Validation: Enabled: false +Rails/UniqBeforePluck: + Enabled: false + ##################### RSpec ################################## # Check that instances are not being stubbed globally. From f0f8efeb3465d5a6915a680bc96a4cdc9384bd4b Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sun, 5 Jun 2016 12:24:36 -0600 Subject: [PATCH 242/507] Fix error on non-issue pages with comment areas. --- app/views/projects/_md_preview.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 59a952dd66..28a28282fd 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 - - if @issue.confidential? + - if defined?(@issue) && @issue.confidential? %li.confidential-issue-warning = icon('warning') %span This is a confidential issue. Your comment will not be visible to the public. From 90a3c947a712845770dc9fe2266b727092e717cb Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 5 Jun 2016 14:49:25 -0400 Subject: [PATCH 243/507] Fix CHANGELOG for 8.8.4 (unreleased) [ci skip] --- CHANGELOG | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e2ba6bc79d..ecce18af06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,13 +38,11 @@ v 8.9.0 (unreleased) - Put project Files and Commits tabs under Code tab - Replace Colorize with Rainbow for coloring console output in Rake tasks. -v 8.8.4 - - Fix todos page throwing errors when you have a project pending deletion - - Reduce number of SQL queries when rendering user references - v 8.8.4 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds - Fix issue with arrow keys not working in search autocomplete dropdown + - Fix todos page throwing errors when you have a project pending deletion + - Reduce number of SQL queries when rendering user references v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 From 86df113907907b282de226e86b27e1136432563e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 31 May 2016 18:22:33 +0100 Subject: [PATCH 244/507] Upgraded jQuery to version 2 Closes #12440 --- app/assets/javascripts/application.js.coffee | 2 +- vendor/assets/javascripts/task_list.js.coffee | 258 ++++++++++++++++++ 2 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 vendor/assets/javascripts/task_list.js.coffee diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 18c1aa0d4e..0bf99bb1f4 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -4,7 +4,7 @@ # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # the compiled file. # -#= require jquery +#= require jquery2 #= require jquery-ui/autocomplete #= require jquery-ui/datepicker #= require jquery-ui/draggable diff --git a/vendor/assets/javascripts/task_list.js.coffee b/vendor/assets/javascripts/task_list.js.coffee new file mode 100644 index 0000000000..584751af8e --- /dev/null +++ b/vendor/assets/javascripts/task_list.js.coffee @@ -0,0 +1,258 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 GitHub, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# TaskList Behavior +# +#= provides tasklist:enabled +#= provides tasklist:disabled +#= provides tasklist:change +#= provides tasklist:changed +# +# +# Enables Task List update behavior. +# +# ### Example Markup +# +#
                                      +#
                                        +#
                                      • +# +# text +#
                                      • +#
                                      +#
                                      +# +#
                                      +#
                                      +# +# ### Specification +# +# TaskLists MUST be contained in a `(div).js-task-list-container`. +# +# TaskList Items SHOULD be an a list (`UL`/`OL`) element. +# +# Task list items MUST match `(input).task-list-item-checkbox` and MUST be +# `disabled` by default. +# +# TaskLists MUST have a `(textarea).js-task-list-field` form element whose +# `value` attribute is the source (Markdown) to be udpated. The source MUST +# follow the syntax guidelines. +# +# TaskList updates trigger `tasklist:change` events. If the change is +# successful, `tasklist:changed` is fired. The change can be canceled. +# +# jQuery is required. +# +# ### Methods +# +# `.taskList('enable')` or `.taskList()` +# +# Enables TaskList updates for the container. +# +# `.taskList('disable')` +# +# Disables TaskList updates for the container. +# +## ### Events +# +# `tasklist:enabled` +# +# Fired when the TaskList is enabled. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** No +# * **Target** `.js-task-list-container` +# +# `tasklist:disabled` +# +# Fired when the TaskList is disabled. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** No +# * **Target** `.js-task-list-container` +# +# `tasklist:change` +# +# Fired before the TaskList item change takes affect. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** Yes +# * **Target** `.js-task-list-field` +# +# `tasklist:changed` +# +# Fired once the TaskList item change has taken affect. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** No +# * **Target** `.js-task-list-field` +# +# ### NOTE +# +# Task list checkboxes are rendered as disabled by default because rendered +# user content is cached without regard for the viewer. + +incomplete = "[ ]" +complete = "[x]" + +# Escapes the String for regular expression matching. +escapePattern = (str) -> + str. + replace(/([\[\]])/g, "\\$1"). # escape square brackets + replace(/\s/, "\\s"). # match all white space + replace("x", "[xX]") # match all cases + +incompletePattern = /// + #{escapePattern(incomplete)} +/// +completePattern = /// + #{escapePattern(complete)} +/// + +# Pattern used to identify all task list items. +# Useful when you need iterate over all items. +itemPattern = /// + ^ + (?: # prefix, consisting of + \s* # optional leading whitespace + (?:>\s*)* # zero or more blockquotes + (?:[-+*]|(?:\d+\.)) # list item indicator + ) + \s* # optional whitespace prefix + ( # checkbox + #{escapePattern(complete)}| + #{escapePattern(incomplete)} + ) + \s+ # is followed by whitespace + (?! + \(.*?\) # is not part of a [foo](url) link + ) + (?= # and is followed by zero or more links + (?:\[.*?\]\s*(?:\[.*?\]|\(.*?\))\s*)* + (?:[^\[]|$) # and either a non-link or the end of the string + ) +/// + +# Used to filter out code fences from the source for comparison only. +# http://rubular.com/r/x5EwZVrloI +# Modified slightly due to issues with JS +codeFencesPattern = /// + ^`{3} # ``` + (?:\s*\w+)? # followed by optional language + [\S\s] # whitespace + .* # code + [\S\s] # whitespace + ^`{3}$ # ``` +///mg + +# Used to filter out potential mismatches (items not in lists). +# http://rubular.com/r/OInl6CiePy +itemsInParasPattern = /// + ^ + ( + #{escapePattern(complete)}| + #{escapePattern(incomplete)} + ) + .+ + $ +///g + +# Given the source text, updates the appropriate task list item to match the +# given checked value. +# +# Returns the updated String text. +updateTaskListItem = (source, itemIndex, checked) -> + clean = source.replace(/\r/g, '').replace(codeFencesPattern, ''). + replace(itemsInParasPattern, '').split("\n") + index = 0 + result = for line in source.split("\n") + if line in clean && line.match(itemPattern) + index += 1 + if index == itemIndex + line = + if checked + line.replace(incompletePattern, complete) + else + line.replace(completePattern, incomplete) + line + result.join("\n") + +# Updates the $field value to reflect the state of $item. +# Triggers the `tasklist:change` event before the value has changed, and fires +# a `tasklist:changed` event once the value has changed. +updateTaskList = ($item) -> + $container = $item.closest '.js-task-list-container' + $field = $container.find '.js-task-list-field' + index = 1 + $container.find('.task-list-item-checkbox').index($item) + checked = $item.prop 'checked' + + event = $.Event 'tasklist:change' + $field.trigger event, [index, checked] + + unless event.isDefaultPrevented() + $field.val updateTaskListItem($field.val(), index, checked) + $field.trigger 'change' + $field.trigger 'tasklist:changed', [index, checked] + +# When the task list item checkbox is updated, submit the change +$(document).on 'change', '.task-list-item-checkbox', -> + updateTaskList $(this) + +# Enables TaskList item changes. +enableTaskList = ($container) -> + if $container.find('.js-task-list-field').length > 0 + $container. + find('.task-list-item').addClass('enabled'). + find('.task-list-item-checkbox').attr('disabled', null) + $container.addClass('is-task-list-enabled'). + trigger 'tasklist:enabled' + +# Enables a collection of TaskList containers. +enableTaskLists = ($containers) -> + for container in $containers + enableTaskList $(container) + +# Disable TaskList item changes. +disableTaskList = ($container) -> + $container. + find('.task-list-item').removeClass('enabled'). + find('.task-list-item-checkbox').attr('disabled', 'disabled') + $container.removeClass('is-task-list-enabled'). + trigger 'tasklist:disabled' + +# Disables a collection of TaskList containers. +disableTaskLists = ($containers) -> + for container in $containers + disableTaskList $(container) + +$.fn.taskList = (method) -> + $container = $(this).closest('.js-task-list-container') + + methods = + enable: enableTaskLists + disable: disableTaskLists + + methods[method || 'enable']($container) From 515a5aeb3325848de22eec6421b1a0dcaf434e2c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 1 Jun 2016 10:11:30 +0100 Subject: [PATCH 245/507] Fixed JS errors CHANGELOG item --- CHANGELOG | 1 + app/assets/javascripts/due_date_select.js.coffee | 5 +++-- app/assets/javascripts/milestone_select.js.coffee | 4 ++-- app/assets/javascripts/users_select.js.coffee | 2 +- spec/features/issues_spec.rb | 8 ++------ .../graphs/stat_graph_contributors_util_spec.js | 12 ++++++------ 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ecce18af06..6d5663134d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,7 @@ v 8.8.4 (unreleased) - Fix issue with arrow keys not working in search autocomplete dropdown - Fix todos page throwing errors when you have a project pending deletion - Reduce number of SQL queries when rendering user references + - Upgrade to jQuery 2 v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee index 3cc7018517..3d009a96d0 100644 --- a/app/assets/javascripts/due_date_select.js.coffee +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -21,7 +21,7 @@ class @DueDateSelect $dropdown.glDropdown( hidden: -> $selectbox.hide() - $value.removeAttr('style') + $value.css('display', '') ) addDueDate = (isDropdown) -> @@ -42,12 +42,13 @@ class @DueDateSelect type: 'PUT' url: issueUpdateURL data: data + dataType: 'json' beforeSend: -> $loading.fadeIn() if isDropdown $dropdown.trigger('loading.gl.dropdown') $selectbox.hide() - $value.removeAttr('style') + $value.css('display', '') $valueContent.html(mediumDate) $sidebarValue.html(mediumDate) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 345a0e447a..1d061d5edb 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -83,7 +83,7 @@ class @MilestoneSelect $selectbox.hide() # display:block overrides the hide-collapse rule - $value.removeAttr('style') + $value.css('display', '') clicked: (selected) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' @@ -118,7 +118,7 @@ class @MilestoneSelect $dropdown.trigger('loaded.gl.dropdown') $loading.fadeOut() $selectbox.hide() - $value.removeAttr('style') + $value.css('display', '') if data.milestone? data.milestone.namespace = _this.currentProject.namespace data.milestone.path = _this.currentProject.path diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 519618aa61..de0eae58bf 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -149,7 +149,7 @@ class @UsersSelect hidden: (e) -> $selectbox.hide() # display:block overrides the hide-collapse rule - $value.removeAttr('style') + $value.css('display', '') clicked: (user) -> page = $('body').data 'page' diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index a4eed5031c..460d7f82b3 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -365,13 +365,9 @@ describe 'Issues', feature: true do page.within('.assignee') do expect(page).to have_content "#{@user.name}" - end - find('.block.assignee .edit-link').click - sleep 2 # wait for ajax stuff to complete - first('.dropdown-menu-user-link').click - sleep 2 - page.within('.assignee') do + click_link 'Edit' + click_link 'Unassigned' expect(page).to have_content 'No assignee' end diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 5b99244747..56970e22e3 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -9,14 +9,14 @@ describe("ContributorsStatGraphUtil", function () { {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1}, {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3}, {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}] - + var correct_parsed_log = { total: [ {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], by_author: [ - { + { author_name: "Karlo Soriano", author_email: "karlo@email.com", "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} }, @@ -132,8 +132,8 @@ describe("ContributorsStatGraphUtil", function () { total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], by_author:[ - { - author: "Karlo Soriano", + { + author: "Karlo Soriano", "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} }, { @@ -161,11 +161,11 @@ describe("ContributorsStatGraphUtil", function () { it("returns the log by author sorted by specified field", function () { var fake_parsed_log = { total: [ - {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} ], by_author: [ - { + { author_name: "Karlo Soriano", author_email: "karlo@email.com", "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} }, From b75945e9e4718eb7c2b029b7fb7884eb2006fab1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 6 Jun 2016 07:42:12 +0200 Subject: [PATCH 246/507] Fix rubocop offense in awardable specs --- spec/models/concerns/awardable_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index 6851d06836..a371c4a18a 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -38,12 +38,11 @@ describe Issue, "Awardable" do describe "#toggle_award_emoji" do it "adds an emoji if it isn't awarded yet" do - expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by 1 + expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1) end it "toggles already awarded emoji" do - - expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by -1 + expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1) end end end From 23030439c2cf3b3ad48099eb9a4371b8bf55066f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 6 Jun 2016 08:20:55 +0200 Subject: [PATCH 247/507] Rename class that loads CI configuration to Loader --- lib/ci/gitlab_ci_yaml_processor.rb | 2 +- lib/gitlab/ci/config.rb | 10 ++++---- lib/gitlab/ci/config/{parser.rb => loader.rb} | 4 ++-- .../config/{parser_spec.rb => loader_spec.rb} | 24 ++++++++----------- spec/lib/gitlab/ci/config_spec.rb | 2 +- 5 files changed, 19 insertions(+), 23 deletions(-) rename lib/gitlab/ci/config/{parser.rb => loader.rb} (92%) rename spec/lib/gitlab/ci/config/{parser_spec.rb => loader_spec.rb} (56%) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 9a60c5ab84..46a923161c 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -18,7 +18,7 @@ module Ci initial_parsing validate! - rescue Gitlab::Ci::Config::ParserError => e + rescue Gitlab::Ci::Config::LoaderError => e raise ValidationError, e.message end diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 0baefa70f6..5fc4894311 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -1,16 +1,16 @@ module Gitlab module Ci class Config - class ParserError < StandardError; end + class LoaderError < StandardError; end def initialize(config) - parser = Parser.new(config) + loader = Loader.new(config) - unless parser.valid? - raise ParserError, 'Invalid configuration format!' + unless loader.valid? + raise LoaderError, 'Invalid configuration format!' end - @config = parser.parse + @config = loader.load end def to_hash diff --git a/lib/gitlab/ci/config/parser.rb b/lib/gitlab/ci/config/loader.rb similarity index 92% rename from lib/gitlab/ci/config/parser.rb rename to lib/gitlab/ci/config/loader.rb index 6e1b7ec826..ed9cc16702 100644 --- a/lib/gitlab/ci/config/parser.rb +++ b/lib/gitlab/ci/config/loader.rb @@ -1,7 +1,7 @@ module Gitlab module Ci class Config - class Parser + class Loader class FormatError < StandardError; end def initialize(config) @@ -12,7 +12,7 @@ module Gitlab @config.is_a?(Hash) end - def parse + def load unless valid? raise FormatError, 'Invalid configuration format' end diff --git a/spec/lib/gitlab/ci/config/parser_spec.rb b/spec/lib/gitlab/ci/config/loader_spec.rb similarity index 56% rename from spec/lib/gitlab/ci/config/parser_spec.rb rename to spec/lib/gitlab/ci/config/loader_spec.rb index b35e66cde5..6f1a10085d 100644 --- a/spec/lib/gitlab/ci/config/parser_spec.rb +++ b/spec/lib/gitlab/ci/config/loader_spec.rb @@ -1,24 +1,20 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Parser do - let(:parser) { described_class.new(yml) } +describe Gitlab::Ci::Config::Loader do + let(:loader) { described_class.new(yml) } context 'when yaml syntax is correct' do let(:yml) { 'image: ruby:2.2' } describe '#valid?' do it 'returns true' do - expect(parser.valid?).to be true + expect(loader.valid?).to be true end end - describe '#parse' do - it 'returns a hash' do - expect(parser.parse).to be_a Hash - end - + describe '#load' do it 'returns a valid hash' do - expect(parser.parse).to eq(image: 'ruby:2.2') + expect(loader.load).to eq(image: 'ruby:2.2') end end end @@ -28,14 +24,14 @@ describe Gitlab::Ci::Config::Parser do describe '#valid?' do it 'returns false' do - expect(parser.valid?).to be false + expect(loader.valid?).to be false end end - describe '#parse' do + describe '#load' do it 'raises error' do - expect { parser.parse }.to raise_error( - Gitlab::Ci::Config::Parser::FormatError, + expect { loader.load }.to raise_error( + Gitlab::Ci::Config::Loader::FormatError, 'Invalid configuration format' ) end @@ -47,7 +43,7 @@ describe Gitlab::Ci::Config::Parser do describe '#valid?' do it 'returns false' do - expect(parser.valid?).to be false + expect(loader.valid?).to be false end end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 691c1ee8ba..52aafbcaaa 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::Ci::Config do describe '.new' do it 'raises error' do expect { config }.to raise_error( - Gitlab::Ci::Config::ParserError, /Invalid configuration format/ + Gitlab::Ci::Config::LoaderError, /Invalid configuration format/ ) end end From 791cc9138be6ea1783e3c3853370cf0290f4d41e Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Jun 2016 10:08:42 +0530 Subject: [PATCH 248/507] Add a `U2fRegistrations` table/model. - To hold registrations from U2F devices, and to authenticate them. - Previously, `User#two_factor_enabled` was aliased to the `otp_required_for_login` column on `users`. - This commit changes things a bit: - `User#two_factor_enabled` is not a method anymore - `User#two_factor_enabled?` checks both the `otp_required_for_login` column, as well as `U2fRegistration`s - Change all instances of `User#two_factor_enabled` to `User#two_factor_enabled?` - Add the `u2f` gem, and implement registration/authentication at the model level. --- Gemfile | 1 + Gemfile.lock | 2 + app/controllers/application_controller.rb | 4 +- app/helpers/auth_helper.rb | 2 +- app/models/u2f_registration.rb | 40 +++++++++++++ app/models/user.rb | 45 ++++++++++---- ...20160425045124_create_u2f_registrations.rb | 13 ++++ db/schema.rb | 15 ++++- lib/api/entities.rb | 2 +- spec/factories/u2f_registrations.rb | 8 +++ spec/factories/users.rb | 14 ++++- spec/features/admin/admin_users_spec.rb | 10 ++-- spec/models/user_spec.rb | 60 +++++++++++++++++++ 13 files changed, 194 insertions(+), 22 deletions(-) create mode 100644 app/models/u2f_registration.rb create mode 100644 db/migrate/20160425045124_create_u2f_registrations.rb create mode 100644 spec/factories/u2f_registrations.rb diff --git a/Gemfile b/Gemfile index a50d7e632a..1e7c7869cb 100644 --- a/Gemfile +++ b/Gemfile @@ -45,6 +45,7 @@ gem 'akismet', '~> 2.0' gem 'devise-two-factor', '~> 3.0.0' gem 'rqrcode-rails3', '~> 0.1.7' gem 'attr_encrypted', '~> 3.0.0' +gem 'u2f', '~> 0.2.1' # Browser detection gem "browser", '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 1771b919b6..bdf7ab9774 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -747,6 +747,7 @@ GEM simple_oauth (~> 0.1.4) tzinfo (1.2.2) thread_safe (~> 0.1) + u2f (0.2.1) uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) @@ -963,6 +964,7 @@ DEPENDENCIES thin (~> 1.6.1) tinder (~> 1.10.0) turbolinks (~> 2.5.0) + u2f (~> 0.2.1) uglifier (~> 2.7.2) underscore-rails (~> 1.8.0) unf (~> 0.1.4) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c28d1ca9e3..e73b2d0855 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -182,8 +182,8 @@ class ApplicationController < ActionController::Base end def check_2fa_requirement - if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? - redirect_to new_profile_two_factor_auth_path + if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor? + redirect_to profile_two_factor_auth_path end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index b05fa0a14d..cd4d778e50 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -66,7 +66,7 @@ module AuthHelper def two_factor_skippable? current_application_settings.require_two_factor_authentication && - !current_user.two_factor_enabled && + !current_user.two_factor_enabled? && current_application_settings.two_factor_grace_period && !two_factor_grace_period_expired? end diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb new file mode 100644 index 0000000000..00b19686d4 --- /dev/null +++ b/app/models/u2f_registration.rb @@ -0,0 +1,40 @@ +# Registration information for U2F (universal 2nd factor) devices, like Yubikeys + +class U2fRegistration < ActiveRecord::Base + belongs_to :user + + def self.register(user, app_id, json_response, challenges) + u2f = U2F::U2F.new(app_id) + registration = self.new + + begin + response = U2F::RegisterResponse.load_from_json(json_response) + registration_data = u2f.register!(challenges, response) + registration.update(certificate: registration_data.certificate, + key_handle: registration_data.key_handle, + public_key: registration_data.public_key, + counter: registration_data.counter, + user: user) + rescue JSON::ParserError, NoMethodError, ArgumentError + registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.') + rescue U2F::Error => e + registration.errors.add(:base, e.message) + end + + registration + end + + def self.authenticate(user, app_id, json_response, challenges) + response = U2F::SignResponse.load_from_json(json_response) + registration = user.u2f_registrations.find_by_key_handle(response.key_handle) + u2f = U2F::U2F.new(app_id) + + if registration + u2f.authenticate!(challenges, response, Base64.decode64(registration.public_key), registration.counter) + registration.update(counter: response.counter) + true + end + rescue JSON::ParserError, NoMethodError, ArgumentError, U2F::Error + false + end +end diff --git a/app/models/user.rb b/app/models/user.rb index bbc88f7e38..e0987e07e1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -27,7 +27,6 @@ class User < ActiveRecord::Base devise :two_factor_authenticatable, otp_secret_encryption_key: Gitlab::Application.config.secret_key_base - alias_attribute :two_factor_enabled, :otp_required_for_login devise :two_factor_backupable, otp_number_of_backup_codes: 10 serialize :otp_backup_codes, JSON @@ -51,6 +50,7 @@ class User < ActiveRecord::Base has_many :keys, dependent: :destroy has_many :emails, dependent: :destroy has_many :identities, dependent: :destroy, autosave: true + has_many :u2f_registrations, dependent: :destroy # Groups has_many :members, dependent: :destroy @@ -175,8 +175,16 @@ class User < ActiveRecord::Base scope :active, -> { with_state(:active) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } - scope :with_two_factor, -> { where(two_factor_enabled: true) } - scope :without_two_factor, -> { where(two_factor_enabled: false) } + + def self.with_two_factor + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). + where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id]) + end + + def self.without_two_factor + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). + where("u2f.id IS NULL AND otp_required_for_login = ?", false) + end # # Class methods @@ -323,14 +331,29 @@ class User < ActiveRecord::Base end def disable_two_factor! - update_attributes( - two_factor_enabled: false, - encrypted_otp_secret: nil, - encrypted_otp_secret_iv: nil, - encrypted_otp_secret_salt: nil, - otp_grace_period_started_at: nil, - otp_backup_codes: nil - ) + transaction do + update_attributes( + otp_required_for_login: false, + encrypted_otp_secret: nil, + encrypted_otp_secret_iv: nil, + encrypted_otp_secret_salt: nil, + otp_grace_period_started_at: nil, + otp_backup_codes: nil + ) + self.u2f_registrations.destroy_all + end + end + + def two_factor_enabled? + two_factor_otp_enabled? || two_factor_u2f_enabled? + end + + def two_factor_otp_enabled? + self.otp_required_for_login? + end + + def two_factor_u2f_enabled? + self.u2f_registrations.exists? end def namespace_uniq diff --git a/db/migrate/20160425045124_create_u2f_registrations.rb b/db/migrate/20160425045124_create_u2f_registrations.rb new file mode 100644 index 0000000000..93bdd9de2e --- /dev/null +++ b/db/migrate/20160425045124_create_u2f_registrations.rb @@ -0,0 +1,13 @@ +class CreateU2fRegistrations < ActiveRecord::Migration + def change + create_table :u2f_registrations do |t| + t.text :certificate + t.string :key_handle, index: true + t.string :public_key + t.integer :counter + t.references :user, index: true, foreign_key: true + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2890211961..9b991f347a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,6 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 20160530150109) do - # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "pg_trgm" @@ -940,6 +939,19 @@ ActiveRecord::Schema.define(version: 20160530150109) do add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree + create_table "u2f_registrations", force: :cascade do |t| + t.text "certificate" + t.string "key_handle" + t.string "public_key" + t.integer "counter" + t.integer "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree + add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -1047,4 +1059,5 @@ ActiveRecord::Schema.define(version: 20160530150109) do add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree + add_foreign_key "u2f_registrations", "users" end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 1a996846e9..66c138eb90 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -30,7 +30,7 @@ module API expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project - expose :two_factor_enabled + expose :two_factor_enabled?, as: :two_factor_enabled expose :external end diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb new file mode 100644 index 0000000000..df92b07958 --- /dev/null +++ b/spec/factories/u2f_registrations.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :u2f_registration do + certificate { FFaker::BaconIpsum.characters(728) } + key_handle { FFaker::BaconIpsum.characters(86) } + public_key { FFaker::BaconIpsum.characters(88) } + counter 0 + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a9b2148bd2..c6f7869516 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -15,14 +15,26 @@ FactoryGirl.define do end trait :two_factor do + two_factor_via_otp + end + + trait :two_factor_via_otp do before(:create) do |user| - user.two_factor_enabled = true + user.otp_required_for_login = true user.otp_secret = User.generate_otp_secret(32) user.otp_grace_period_started_at = Time.now user.generate_otp_backup_codes! end end + trait :two_factor_via_u2f do + transient { registrations_count 5 } + + after(:create) do |user, evaluator| + create_list(:u2f_registration, evaluator.registrations_count, user: user) + end + end + factory :omniauth_user do transient do extern_uid '123456' diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 96621843b3..b72ad40547 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -19,7 +19,7 @@ describe "Admin::Users", feature: true do describe 'Two-factor Authentication filters' do it 'counts users who have enabled 2FA' do - create(:user, two_factor_enabled: true) + create(:user, :two_factor) visit admin_users_path @@ -29,7 +29,7 @@ describe "Admin::Users", feature: true do end it 'filters by users who have enabled 2FA' do - user = create(:user, two_factor_enabled: true) + user = create(:user, :two_factor) visit admin_users_path click_link '2FA Enabled' @@ -38,7 +38,7 @@ describe "Admin::Users", feature: true do end it 'counts users who have not enabled 2FA' do - create(:user, two_factor_enabled: false) + create(:user) visit admin_users_path @@ -48,7 +48,7 @@ describe "Admin::Users", feature: true do end it 'filters by users who have not enabled 2FA' do - user = create(:user, two_factor_enabled: false) + user = create(:user) visit admin_users_path click_link '2FA Disabled' @@ -173,7 +173,7 @@ describe "Admin::Users", feature: true do describe 'Two-factor Authentication status' do it 'shows when enabled' do - @user.update_attribute(:two_factor_enabled, true) + @user.update_attribute(:otp_required_for_login, true) visit admin_user_path(@user) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 528a79bf22..6ea8bf9bbe 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -121,6 +121,66 @@ describe User, models: true do end end + describe "scopes" do + describe ".with_two_factor" do + it "returns users with 2fa enabled via OTP" do + user_with_2fa = create(:user, :two_factor_via_otp) + user_without_2fa = create(:user) + users_with_two_factor = User.with_two_factor.pluck(:id) + + expect(users_with_two_factor).to include(user_with_2fa.id) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + + it "returns users with 2fa enabled via U2F" do + user_with_2fa = create(:user, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_with_two_factor = User.with_two_factor.pluck(:id) + + expect(users_with_two_factor).to include(user_with_2fa.id) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + + it "returns users with 2fa enabled via OTP and U2F" do + user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_with_two_factor = User.with_two_factor.pluck(:id) + + expect(users_with_two_factor).to eq([user_with_2fa.id]) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + end + + describe ".without_two_factor" do + it "excludes users with 2fa enabled via OTP" do + user_with_2fa = create(:user, :two_factor_via_otp) + user_without_2fa = create(:user) + users_without_two_factor = User.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + + it "excludes users with 2fa enabled via U2F" do + user_with_2fa = create(:user, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_without_two_factor = User.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + + it "excludes users with 2fa enabled via OTP and U2F" do + user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_without_two_factor = User.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + end + end + describe "Respond to" do it { is_expected.to respond_to(:is_admin?) } it { is_expected.to respond_to(:name) } From e5823f3609136dafdee05204cd17436e09985177 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Jun 2016 10:09:58 +0530 Subject: [PATCH 249/507] Update the `browser` gem. - Need the `mobile?` detection (that the new version provides) for the U2F registration/ authentication flow --- Gemfile | 2 +- Gemfile.lock | 6 +++--- app/views/help/_shortcuts.html.haml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 1e7c7869cb..38ff536fd7 100644 --- a/Gemfile +++ b/Gemfile @@ -48,7 +48,7 @@ gem 'attr_encrypted', '~> 3.0.0' gem 'u2f', '~> 0.2.1' # Browser detection -gem "browser", '~> 1.0.0' +gem "browser", '~> 2.0.3' # Extracting information from a git repository # Provide access to Gitlab::Git library diff --git a/Gemfile.lock b/Gemfile.lock index bdf7ab9774..5f1dbd431e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,7 +92,7 @@ GEM sass (~> 3.0) slim (>= 1.3.6, < 4.0) terminal-table (~> 1.4) - browser (1.0.1) + browser (2.0.3) builder (3.2.2) bullet (5.0.0) activesupport (>= 3.0.0) @@ -815,7 +815,7 @@ DEPENDENCIES binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) brakeman (~> 3.2.0) - browser (~> 1.0.0) + browser (~> 2.0.3) bullet bundler-audit byebug @@ -977,4 +977,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.12.4 + 1.12.5 diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 70e88da7aa..01648047ce 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -24,7 +24,7 @@ %td Show/hide this dialog %tr %td.shortcut - - if browser.mac? + - if browser.platform.mac? .key ⌘ shift p - else .key ctrl shift p From 1f713d52d71cc283cb2190cfcdf38155a6fdfeac Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Jun 2016 10:12:39 +0530 Subject: [PATCH 250/507] Render `gon` data in the page `body`, not `head` - Turbolinks caches the `head`, so `gon` updates don't show up unless the user navigates to page directly (by URL) or performs a refresh. - The solution is to render `gon` in the body instead. - Also update the syntax to the new Rails 4 (according to the gon README) syntax. --- app/views/layouts/_head.html.haml | 2 -- app/views/layouts/application.html.haml | 2 ++ app/views/layouts/devise.html.haml | 1 + app/views/layouts/devise_empty.html.haml | 1 + app/views/layouts/errors.html.haml | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index b30fb0a5da..e0ed657919 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -35,8 +35,6 @@ = csrf_meta_tags - = include_gon - - unless browser.safari? %meta{name: 'referrer', content: 'origin-when-cross-origin'} %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index e4d1c773d0..2b86b289bb 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,6 +2,8 @@ %html{ lang: "en"} = render "layouts/head" %body{class: "#{user_application_theme}", 'data-page' => body_data_page} + = Gon::Base.render_data + -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. = yield :scripts_body_top diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index f08cb0a542..3d28eec84e 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body.ui_charcoal.login-page.application.navless + = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index 7c061dd531..6bd427b02a 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body.ui_charcoal.login-page.application.navless + = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 915acc4612..7fbe065df0 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body{class: "#{user_application_theme} application navless"} + = Gon::Base.render_data = render "layouts/header/empty" .container.navless-container = render "layouts/flash" From 128549f10beb406333fa23c1693750c06ff7bc4a Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Jun 2016 10:14:51 +0530 Subject: [PATCH 251/507] Implement U2F registration. - Move the `TwoFactorAuthsController`'s `new` action to `show`, since the page is not used to create a single "two factor auth" anymore. We can have a single 2FA authenticator app, along with any number of U2F devices, in any combination, so the page will be accessed after the first "two factor auth" is created. - Add the `u2f` javascript library, which provides an API to the browser's U2F implementation. - Add tests for the JS components --- app/assets/javascripts/application.js.coffee | 2 + app/assets/javascripts/u2f/error.js.coffee | 13 + app/assets/javascripts/u2f/register.js.coffee | 63 ++ app/assets/javascripts/u2f/util.js.coffee.erb | 15 + app/controllers/application_controller.rb | 11 + .../profiles/two_factor_auths_controller.rb | 45 +- app/views/profiles/accounts/show.html.haml | 25 +- .../profiles/two_factor_auths/new.html.haml | 39 - .../profiles/two_factor_auths/show.html.haml | 69 ++ app/views/u2f/_register.html.haml | 31 + config/routes.rb | 3 +- .../two_factor_auths_controller_spec.rb | 14 +- .../fixtures/u2f/register.html.haml | 1 + .../javascripts/u2f/mock_u2f_device.js.coffee | 15 + spec/javascripts/u2f/register_spec.js.coffee | 57 ++ vendor/assets/javascripts/u2f.js | 748 ++++++++++++++++++ 16 files changed, 1086 insertions(+), 65 deletions(-) create mode 100644 app/assets/javascripts/u2f/error.js.coffee create mode 100644 app/assets/javascripts/u2f/register.js.coffee create mode 100644 app/assets/javascripts/u2f/util.js.coffee.erb delete mode 100644 app/views/profiles/two_factor_auths/new.html.haml create mode 100644 app/views/profiles/two_factor_auths/show.html.haml create mode 100644 app/views/u2f/_register.html.haml create mode 100644 spec/javascripts/fixtures/u2f/register.html.haml create mode 100644 spec/javascripts/u2f/mock_u2f_device.js.coffee create mode 100644 spec/javascripts/u2f/register_spec.js.coffee create mode 100644 vendor/assets/javascripts/u2f.js diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 18c1aa0d4e..a76b111bf0 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -56,9 +56,11 @@ #= require_directory ./commit #= require_directory ./extensions #= require_directory ./lib +#= require_directory ./u2f #= require_directory . #= require fuzzaldrin-plus #= require cropper +#= require u2f window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/app/assets/javascripts/u2f/error.js.coffee b/app/assets/javascripts/u2f/error.js.coffee new file mode 100644 index 0000000000..1a2fc3e757 --- /dev/null +++ b/app/assets/javascripts/u2f/error.js.coffee @@ -0,0 +1,13 @@ +class @U2FError + constructor: (@errorCode) -> + @httpsDisabled = (window.location.protocol isnt 'https:') + console.error("U2F Error Code: #{@errorCode}") + + message: () => + switch + when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled) + "U2F only works with HTTPS-enabled websites. Contact your administrator for more details." + when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE + "This device has already been registered with us." + else + "There was a problem communicating with your device." diff --git a/app/assets/javascripts/u2f/register.js.coffee b/app/assets/javascripts/u2f/register.js.coffee new file mode 100644 index 0000000000..74472cfa12 --- /dev/null +++ b/app/assets/javascripts/u2f/register.js.coffee @@ -0,0 +1,63 @@ +# Register U2F (universal 2nd factor) devices for users to authenticate with. +# +# State Flow #1: setup -> in_progress -> registered -> POST to server +# State Flow #2: setup -> in_progress -> error -> setup + +class @U2FRegister + constructor: (@container, u2fParams) -> + @appId = u2fParams.app_id + @registerRequests = u2fParams.register_requests + @signRequests = u2fParams.sign_requests + + start: () => + if U2FUtil.isU2FSupported() + @renderSetup() + else + @renderNotSupported() + + register: () => + u2f.register(@appId, @registerRequests, @signRequests, (response) => + if response.errorCode + error = new U2FError(response.errorCode) + @renderError(error); + else + @renderRegistered(JSON.stringify(response)) + , 10) + + ############# + # Rendering # + ############# + + templates: { + "notSupported": "#js-register-u2f-not-supported", + "setup": '#js-register-u2f-setup', + "inProgress": '#js-register-u2f-in-progress', + "error": '#js-register-u2f-error', + "registered": '#js-register-u2f-registered' + } + + renderTemplate: (name, params) => + templateString = $(@templates[name]).html() + template = _.template(templateString) + @container.html(template(params)) + + renderSetup: () => + @renderTemplate('setup') + @container.find('#js-setup-u2f-device').on('click', @renderInProgress) + + renderInProgress: () => + @renderTemplate('inProgress') + @register() + + renderError: (error) => + @renderTemplate('error', {error_message: error.message()}) + @container.find('#js-u2f-try-again').on('click', @renderSetup) + + renderRegistered: (deviceResponse) => + @renderTemplate('registered') + # Prefer to do this instead of interpolating using Underscore templates + # because of JSON escaping issues. + @container.find("#js-device-response").val(deviceResponse) + + renderNotSupported: () => + @renderTemplate('notSupported') diff --git a/app/assets/javascripts/u2f/util.js.coffee.erb b/app/assets/javascripts/u2f/util.js.coffee.erb new file mode 100644 index 0000000000..d59341c38b --- /dev/null +++ b/app/assets/javascripts/u2f/util.js.coffee.erb @@ -0,0 +1,15 @@ +# Helper class for U2F (universal 2nd factor) device registration and authentication. + +class @U2FUtil + @isU2FSupported: -> + if @testMode + true + else + gon.u2f.browser_supports_u2f + + @enableTestMode: -> + @testMode = true + +<% if Rails.env.test? %> +U2FUtil.enableTestMode(); +<% end %> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e73b2d0855..62f6370179 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -342,6 +342,10 @@ class ApplicationController < ActionController::Base session[:skip_tfa] && session[:skip_tfa] > Time.current end + def browser_supports_u2f? + browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile? + end + def redirect_to_home_page_url? # If user is not signed-in and tries to access root_path - redirect him to landing page # Don't redirect to the default URL to prevent endless redirections @@ -355,6 +359,13 @@ class ApplicationController < ActionController::Base current_user.nil? && root_path == request.path end + # U2F (universal 2nd factor) devices need a unique identifier for the application + # to perform authentication. + # https://developers.yubico.com/U2F/App_ID.html + def u2f_app_id + request.base_url + end + private def set_default_sort diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 8f83fdd02b..6a358fdcc0 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -1,7 +1,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController skip_before_action :check_2fa_requirement - def new + def show unless current_user.otp_secret current_user.otp_secret = User.generate_otp_secret(32) end @@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController current_user.save! if current_user.changed? - if two_factor_authentication_required? + if two_factor_authentication_required? && !current_user.two_factor_enabled? if two_factor_grace_period_expired? - flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' + flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.' else grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." + flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}." end end @qr_code = build_qr_code + setup_u2f_registration end def create if current_user.validate_and_consume_otp!(params[:pin_code]) - current_user.two_factor_enabled = true + current_user.otp_required_for_login = true @codes = current_user.generate_otp_backup_codes! current_user.save! @@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController else @error = 'Invalid pin code' @qr_code = build_qr_code + setup_u2f_registration + render 'show' + end + end - render 'new' + # A U2F (universal 2nd factor) device's information is stored after successful + # registration, which is then used while 2FA authentication is taking place. + def create_u2f + @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges]) + + if @u2f_registration.persisted? + session.delete(:challenges) + redirect_to profile_account_path, notice: "Your U2F device was registered!" + else + @qr_code = build_qr_code + setup_u2f_registration + render :show end end @@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def issuer_host Gitlab.config.gitlab.host end + + # Setup in preparation of communication with a U2F (universal 2nd factor) device + # Actual communication is performed using a Javascript API + def setup_u2f_registration + @u2f_registration ||= U2fRegistration.new + @registration_key_handles = current_user.u2f_registrations.pluck(:key_handle) + u2f = U2F::U2F.new(u2f_app_id) + + registration_requests = u2f.registration_requests + sign_requests = u2f.authentication_requests(@registration_key_handles) + session[:challenges] = registration_requests.map(&:challenge) + + gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id, + register_requests: registration_requests, + sign_requests: sign_requests, + browser_supports_u2f: browser_supports_u2f? }) + end end diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 01ac816194..3d2a245ecb 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -11,7 +11,7 @@ %p Your private token is used to access application resources without authentication. .col-lg-9 - = form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f| + = form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f| %p.cgray - if current_user.private_token = label_tag "token", "Private token", class: "label-light" @@ -29,21 +29,22 @@ .row.prepend-top-default .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - Two-factor Authentication + Two-Factor Authentication %p - Increase your account's security by enabling two-factor authentication (2FA). + Increase your account's security by enabling Two-Factor Authentication (2FA). .col-lg-9 %p - Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'} - - if !current_user.two_factor_enabled? - %p - Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. - .append-bottom-10 - = link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' + Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'} + - if current_user.two_factor_enabled? + = link_to 'Manage Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-info' + = link_to 'Disable', profile_two_factor_auth_path, + method: :delete, + data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." }, + class: 'btn btn-danger' - else - = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger', - data: { confirm: 'Are you sure?' } + .append-bottom-10 + = link_to 'Enable Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-success' + %hr - if button_based_providers.any? .row.prepend-top-default diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml deleted file mode 100644 index 69fc81cb45..0000000000 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -- page_title 'Two-factor Authentication', 'Account' - -.row.prepend-top-default - .col-lg-3 - %h4.prepend-top-0 - Two-factor Authentication (2FA) - %p - Increase your account's security by enabling two-factor authentication (2FA). - .col-lg-9 - %p - Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. - .row.append-bottom-10 - .col-md-3 - = raw @qr_code - .col-md-9 - .account-well - %p.prepend-top-0.append-bottom-0 - Can't scan the code? - %p.prepend-top-0.append-bottom-0 - To add the entry manually, provide the following details to the application on your phone. - %p.prepend-top-0.append-bottom-0 - Account: - = current_user.email - %p.prepend-top-0.append-bottom-0 - Key: - = current_user.otp_secret.scan(/.{4}/).join(' ') - %p.two-factor-new-manual-content - Time based: Yes - = form_tag profile_two_factor_auth_path, method: :post do |f| - - if @error - .alert.alert-danger - = @error - .form-group - = label_tag :pin_code, nil, class: "label-light" - = text_field_tag :pin_code, nil, class: "form-control", required: true - .prepend-top-default - = submit_tag 'Enable two-factor authentication', class: 'btn btn-success' - = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable? diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml new file mode 100644 index 0000000000..ce76cb73c9 --- /dev/null +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -0,0 +1,69 @@ +- page_title 'Two-Factor Authentication', 'Account' +- header_title "Two-Factor Authentication", profile_two_factor_auth_path + +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + Register Two-Factor Authentication App + %p + Use an app on your mobile device to enable two-factor authentication (2FA). + .col-lg-9 + - if current_user.two_factor_otp_enabled? + = icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page." + - else + %p + Download the Google Authenticator application from App Store or Google Play Store and scan this code. + More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. + .row.append-bottom-10 + .col-md-3 + = raw @qr_code + .col-md-9 + .account-well + %p.prepend-top-0.append-bottom-0 + Can't scan the code? + %p.prepend-top-0.append-bottom-0 + To add the entry manually, provide the following details to the application on your phone. + %p.prepend-top-0.append-bottom-0 + Account: + = current_user.email + %p.prepend-top-0.append-bottom-0 + Key: + = current_user.otp_secret.scan(/.{4}/).join(' ') + %p.two-factor-new-manual-content + Time based: Yes + = form_tag profile_two_factor_auth_path, method: :post do |f| + - if @error + .alert.alert-danger + = @error + .form-group + = label_tag :pin_code, nil, class: "label-light" + = text_field_tag :pin_code, nil, class: "form-control", required: true + .prepend-top-default + = submit_tag 'Register with Two-Factor App', class: 'btn btn-success' + +%hr + +.row.prepend-top-default + + .col-lg-3 + %h4.prepend-top-0 + Register Universal Two-Factor (U2F) Device + %p + Use a hardware device to add the second factor of authentication. + %p + As U2F devices are only supported by a few browsers, it's recommended that you set up a + two-factor authentication app as well as a U2F device so you'll always be able to log in + using an unsupported browser. + .col-lg-9 + %p + - if @registration_key_handles.present? + = icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab." + - if @u2f_registration.errors.present? + = form_errors(@u2f_registration) + = render "u2f/register" + +- if two_factor_skippable? + :javascript + var button = "Configure it later"; + $(".flash-alert").append(button); + diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml new file mode 100644 index 0000000000..46af591fc4 --- /dev/null +++ b/app/views/u2f/_register.html.haml @@ -0,0 +1,31 @@ +#js-register-u2f + +%script#js-register-u2f-not-supported{ type: "text/template" } + %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). + +%script#js-register-u2f-setup{ type: "text/template" } + .row.append-bottom-10 + .col-md-3 + %a#js-setup-u2f-device.btn.btn-info{ href: 'javascript:void(0)' } Setup New U2F Device + .col-md-9 + %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left. + +%script#js-register-u2f-in-progress{ type: "text/template" } + %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. + +%script#js-register-u2f-error{ type: "text/template" } + %div + %p + %span <%= error_message %> + %a.btn.btn-warning#js-u2f-try-again Try again? + +%script#js-register-u2f-registered{ type: "text/template" } + %div.row.append-bottom-10 + %p Your device was successfully set up! Click this button to register with the GitLab server. + = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do + = hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response" + = submit_tag "Register U2F Device", class: "btn btn-success" + +:javascript + var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f); + u2fRegister.start(); diff --git a/config/routes.rb b/config/routes.rb index 1fc7985136..27ab79d68f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -343,8 +343,9 @@ Rails.application.routes.draw do resources :keys resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] - resource :two_factor_auth, only: [:new, :create, :destroy] do + resource :two_factor_auth, only: [:show, :create, :destroy] do member do + post :create_u2f post :codes patch :skip end diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 4fb1473c2d..d08d0018b3 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -8,21 +8,21 @@ describe Profiles::TwoFactorAuthsController do allow(subject).to receive(:current_user).and_return(user) end - describe 'GET new' do + describe 'GET show' do let(:user) { create(:user) } it 'generates otp_secret for user' do expect(User).to receive(:generate_otp_secret).with(32).and_return('secret').once - get :new - get :new # Second hit shouldn't re-generate it + get :show + get :show # Second hit shouldn't re-generate it end it 'assigns qr_code' do code = double('qr code') expect(subject).to receive(:build_qr_code).and_return(code) - get :new + get :show expect(assigns[:qr_code]).to eq code end end @@ -40,7 +40,7 @@ describe Profiles::TwoFactorAuthsController do expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true) end - it 'sets two_factor_enabled' do + it 'enables 2fa for the user' do go user.reload @@ -79,9 +79,9 @@ describe Profiles::TwoFactorAuthsController do expect(assigns[:qr_code]).to eq code end - it 'renders new' do + it 'renders show' do go - expect(response).to render_template(:new) + expect(response).to render_template(:show) end end end diff --git a/spec/javascripts/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml new file mode 100644 index 0000000000..393c0613fd --- /dev/null +++ b/spec/javascripts/fixtures/u2f/register.html.haml @@ -0,0 +1 @@ += render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f' } diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee new file mode 100644 index 0000000000..97ed0e83a0 --- /dev/null +++ b/spec/javascripts/u2f/mock_u2f_device.js.coffee @@ -0,0 +1,15 @@ +class @MockU2FDevice + constructor: () -> + window.u2f ||= {} + + window.u2f.register = (appId, registerRequests, signRequests, callback) => + @registerCallback = callback + + window.u2f.sign = (appId, challenges, signRequests, callback) => + @authenticateCallback = callback + + respondToRegisterRequest: (params) => + @registerCallback(params) + + respondToAuthenticateRequest: (params) => + @authenticateCallback(params) diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee new file mode 100644 index 0000000000..0858abeca1 --- /dev/null +++ b/spec/javascripts/u2f/register_spec.js.coffee @@ -0,0 +1,57 @@ +#= require u2f/register +#= require u2f/util +#= require u2f/error +#= require u2f +#= require ./mock_u2f_device + +describe 'U2FRegister', -> + U2FUtil.enableTestMode() + fixture.load('u2f/register') + + beforeEach -> + @u2fDevice = new MockU2FDevice + @container = $("#js-register-u2f") + @component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token") + @component.start() + + it 'allows registering a U2F device', -> + setupButton = @container.find("#js-setup-u2f-device") + expect(setupButton.text()).toBe('Setup New U2F Device') + setupButton.trigger('click') + + inProgressMessage = @container.children("p") + expect(inProgressMessage.text()).toContain("Trying to communicate with your device") + + @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) + registeredMessage = @container.find('p') + deviceResponse = @container.find('#js-device-response') + expect(registeredMessage.text()).toContain("Your device was successfully set up!") + expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') + + describe "errors", -> + it "doesn't allow the same device to be registered twice (for the same user", -> + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({errorCode: 4}) + errorMessage = @container.find("p") + expect(errorMessage.text()).toContain("already been registered with us") + + it "displays an error message for other errors", -> + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) + errorMessage = @container.find("p") + expect(errorMessage.text()).toContain("There was a problem communicating with your device") + + it "allows retrying registration after an error", -> + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) + retryButton = @container.find("#U2FTryAgain") + retryButton.trigger('click') + + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) + registeredMessage = @container.find("p") + expect(registeredMessage.text()).toContain("Your device was successfully set up!") diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js new file mode 100644 index 0000000000..e666b13605 --- /dev/null +++ b/vendor/assets/javascripts/u2f.js @@ -0,0 +1,748 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. +u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } + }; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); + }; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +}; \ No newline at end of file From 86b07caa599a7f064e9077770b1a87c670d7607c Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Jun 2016 10:20:39 +0530 Subject: [PATCH 252/507] Implement authentication (login) using a U2F device. - Move the `authenticate_with_two_factor` method from `ApplicationController` to the `AuthenticatesWithTwoFactor` module, where it should be. --- .../javascripts/u2f/authenticate.js.coffee | 63 +++++++++++++++++++ .../concerns/authenticates_with_two_factor.rb | 59 ++++++++++++++++- app/controllers/sessions_controller.rb | 23 +------ .../devise/sessions/two_factor.html.haml | 21 ++++--- app/views/u2f/_authenticate.html.haml | 28 +++++++++ spec/features/login_spec.rb | 26 ++++---- .../fixtures/u2f/authenticate.html.haml | 1 + spec/javascripts/u2f/authenticate_spec.coffee | 52 +++++++++++++++ 8 files changed, 230 insertions(+), 43 deletions(-) create mode 100644 app/assets/javascripts/u2f/authenticate.js.coffee create mode 100644 app/views/u2f/_authenticate.html.haml create mode 100644 spec/javascripts/fixtures/u2f/authenticate.html.haml create mode 100644 spec/javascripts/u2f/authenticate_spec.coffee diff --git a/app/assets/javascripts/u2f/authenticate.js.coffee b/app/assets/javascripts/u2f/authenticate.js.coffee new file mode 100644 index 0000000000..6deb902c8d --- /dev/null +++ b/app/assets/javascripts/u2f/authenticate.js.coffee @@ -0,0 +1,63 @@ +# Authenticate U2F (universal 2nd factor) devices for users to authenticate with. +# +# State Flow #1: setup -> in_progress -> authenticated -> POST to server +# State Flow #2: setup -> in_progress -> error -> setup + +class @U2FAuthenticate + constructor: (@container, u2fParams) -> + @appId = u2fParams.app_id + @challenges = u2fParams.challenges + @signRequests = u2fParams.sign_requests + + start: () => + if U2FUtil.isU2FSupported() + @renderSetup() + else + @renderNotSupported() + + authenticate: () => + u2f.sign(@appId, @challenges, @signRequests, (response) => + if response.errorCode + error = new U2FError(response.errorCode) + @renderError(error); + else + @renderAuthenticated(JSON.stringify(response)) + , 10) + + ############# + # Rendering # + ############# + + templates: { + "notSupported": "#js-authenticate-u2f-not-supported", + "setup": '#js-authenticate-u2f-setup', + "inProgress": '#js-authenticate-u2f-in-progress', + "error": '#js-authenticate-u2f-error', + "authenticated": '#js-authenticate-u2f-authenticated' + } + + renderTemplate: (name, params) => + templateString = $(@templates[name]).html() + template = _.template(templateString) + @container.html(template(params)) + + renderSetup: () => + @renderTemplate('setup') + @container.find('#js-login-u2f-device').on('click', @renderInProgress) + + renderInProgress: () => + @renderTemplate('inProgress') + @authenticate() + + renderError: (error) => + @renderTemplate('error', {error_message: error.message()}) + @container.find('#js-u2f-try-again').on('click', @renderSetup) + + renderAuthenticated: (deviceResponse) => + @renderTemplate('authenticated') + # Prefer to do this instead of interpolating using Underscore templates + # because of JSON escaping issues. + @container.find("#js-device-response").val(deviceResponse) + + renderNotSupported: () => + @renderTemplate('notSupported') diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index d5918a7af3..998b8adc41 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor # Returns nil def prompt_for_two_factor(user) session[:otp_user_id] = user.id + setup_u2f_authentication(user) + render 'devise/sessions/two_factor' + end - render 'devise/sessions/two_factor' and return + def authenticate_with_two_factor + user = self.resource = find_user + + if user_params[:otp_attempt].present? && session[:otp_user_id] + authenticate_with_two_factor_via_otp(user) + elsif user_params[:device_response].present? && session[:otp_user_id] + authenticate_with_two_factor_via_u2f(user) + elsif user && user.valid_password?(user_params[:password]) + prompt_for_two_factor(user) + end + end + + private + + def authenticate_with_two_factor_via_otp(user) + if valid_otp_attempt?(user) + # Remove any lingering user data from login + session.delete(:otp_user_id) + + remember_me(user) if user_params[:remember_me] == '1' + sign_in(user) + else + flash.now[:alert] = 'Invalid two-factor code.' + render :two_factor + end + end + + # Authenticate using the response from a U2F (universal 2nd factor) device + def authenticate_with_two_factor_via_u2f(user) + if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges]) + # Remove any lingering user data from login + session.delete(:otp_user_id) + session.delete(:challenges) + + sign_in(user) + else + flash.now[:alert] = 'Authentication via U2F device failed.' + prompt_for_two_factor(user) + end + end + + # Setup in preparation of communication with a U2F (universal 2nd factor) device + # Actual communication is performed using a Javascript API + def setup_u2f_authentication(user) + key_handles = user.u2f_registrations.pluck(:key_handle) + u2f = U2F::U2F.new(u2f_app_id) + + if key_handles.present? + sign_requests = u2f.authentication_requests(key_handles) + challenges = sign_requests.map(&:challenge) + session[:challenges] = challenges + gon.push(u2f: { challenges: challenges, app_id: u2f_app_id, + sign_requests: sign_requests, + browser_supports_u2f: browser_supports_u2f? }) + end end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d68c2a708e..1c19c1cc1a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -54,7 +54,7 @@ class SessionsController < Devise::SessionsController end def user_params - params.require(:user).permit(:login, :password, :remember_me, :otp_attempt) + params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response) end def find_user @@ -89,27 +89,6 @@ class SessionsController < Devise::SessionsController find_user.try(:two_factor_enabled?) end - def authenticate_with_two_factor - user = self.resource = find_user - - if user_params[:otp_attempt].present? && session[:otp_user_id] - if valid_otp_attempt?(user) - # Remove any lingering user data from login - session.delete(:otp_user_id) - - remember_me(user) if user_params[:remember_me] == '1' - sign_in(user) and return - else - flash.now[:alert] = 'Invalid two-factor code.' - render :two_factor and return - end - else - if user && user.valid_password?(user_params[:password]) - prompt_for_two_factor(user) - end - end - end - def auto_sign_in_with_provider provider = Gitlab.config.omniauth.auto_sign_in_with_provider return unless provider.present? diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index fd5937a45c..9d04db2c45 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -1,11 +1,18 @@ %div .login-box .login-heading - %h3 Two-factor Authentication + %h3 Two-Factor Authentication .login-body - = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - = f.hidden_field :remember_me, value: params[resource_name][:remember_me] - = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true, autocomplete: 'off' - %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. - .prepend-top-20 - = f.submit "Verify code", class: "btn btn-save" + - if @user.two_factor_otp_enabled? + %h5 Authenticate via Two-Factor App + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| + = f.hidden_field :remember_me, value: params[resource_name][:remember_me] + = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off' + %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. + .prepend-top-20 + = f.submit "Verify code", class: "btn btn-save" + + - if @user.two_factor_u2f_enabled? + + %hr + = render "u2f/authenticate" diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml new file mode 100644 index 0000000000..75fb0e303a --- /dev/null +++ b/app/views/u2f/_authenticate.html.haml @@ -0,0 +1,28 @@ +#js-authenticate-u2f + +%script#js-authenticate-u2f-not-supported{ type: "text/template" } + %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). + +%script#js-authenticate-u2f-setup{ type: "text/template" } + %div + %p Insert your security key (if you haven't already), and press the button below. + %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device + +%script#js-authenticate-u2f-in-progress{ type: "text/template" } + %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. + +%script#js-authenticate-u2f-error{ type: "text/template" } + %div + %p <%= error_message %> + %a.btn.btn-warning#js-u2f-try-again Try again? + +%script#js-authenticate-u2f-authenticated{ type: "text/template" } + %div + %p We heard back from your U2F device. Click this button to authenticate with the GitLab server. + = form_tag(new_user_session_path, method: :post) do |f| + = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" + = submit_tag "Authenticate via U2F Device", class: "btn btn-success" + +:javascript + var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f); + u2fAuthenticate.start(); diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index c1b178c3b6..72b5ff231f 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -33,11 +33,11 @@ feature 'Login', feature: true do before do login_with(user, remember: true) - expect(page).to have_content('Two-factor Authentication') + expect(page).to have_content('Two-Factor Authentication') end def enter_code(code) - fill_in 'Two-factor Authentication code', with: code + fill_in 'Two-Factor Authentication code', with: code click_button 'Verify code' end @@ -143,12 +143,12 @@ feature 'Login', feature: true do context 'within the grace period' do it 'redirects to two-factor configuration page' do - expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must enable Two-factor Authentication for your account before') + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content('You must enable Two-Factor Authentication for your account before') end - it 'disallows skipping two-factor configuration' do - expect(current_path).to eq new_profile_two_factor_auth_path + it 'allows skipping two-factor configuration', js: true do + expect(current_path).to eq profile_two_factor_auth_path click_link 'Configure it later' expect(current_path).to eq root_path @@ -159,26 +159,26 @@ feature 'Login', feature: true do let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) } it 'redirects to two-factor configuration page' do - expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must enable Two-factor Authentication for your account.') + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content('You must enable Two-Factor Authentication for your account.') end - it 'disallows skipping two-factor configuration' do - expect(current_path).to eq new_profile_two_factor_auth_path + it 'disallows skipping two-factor configuration', js: true do + expect(current_path).to eq profile_two_factor_auth_path expect(page).not_to have_link('Configure it later') end end end - context 'without grace pariod defined' do + context 'without grace period defined' do before(:each) do stub_application_setting(two_factor_grace_period: 0) login_with(user) end it 'redirects to two-factor configuration page' do - expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must enable Two-factor Authentication for your account.') + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content('You must enable Two-Factor Authentication for your account.') end end end diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml new file mode 100644 index 0000000000..859e79a6c9 --- /dev/null +++ b/spec/javascripts/fixtures/u2f/authenticate.html.haml @@ -0,0 +1 @@ += render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" } diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee new file mode 100644 index 0000000000..e8a2892d67 --- /dev/null +++ b/spec/javascripts/u2f/authenticate_spec.coffee @@ -0,0 +1,52 @@ +#= require u2f/authenticate +#= require u2f/util +#= require u2f/error +#= require u2f +#= require ./mock_u2f_device + +describe 'U2FAuthenticate', -> + U2FUtil.enableTestMode() + fixture.load('u2f/authenticate') + + beforeEach -> + @u2fDevice = new MockU2FDevice + @container = $("#js-authenticate-u2f") + @component = new U2FAuthenticate(@container, {}, "token") + @component.start() + + it 'allows authenticating via a U2F device', -> + setupButton = @container.find("#js-login-u2f-device") + setupMessage = @container.find("p") + expect(setupMessage.text()).toContain('Insert your security key') + expect(setupButton.text()).toBe('Login Via U2F Device') + setupButton.trigger('click') + + inProgressMessage = @container.find("p") + expect(inProgressMessage.text()).toContain("Trying to communicate with your device") + + @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) + authenticatedMessage = @container.find("p") + deviceResponse = @container.find('#js-device-response') + expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") + expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') + + describe "errors", -> + it "displays an error message", -> + setupButton = @container.find("#js-login-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) + errorMessage = @container.find("p") + expect(errorMessage.text()).toContain("There was a problem communicating with your device") + + it "allows retrying authentication after an error", -> + setupButton = @container.find("#js-login-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) + retryButton = @container.find("#js-u2f-try-again") + retryButton.trigger('click') + + setupButton = @container.find("#js-login-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) + authenticatedMessage = @container.find("p") + expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") From 4db19bb4455cd21e80097a3e547d8b266a884aea Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Jun 2016 10:22:06 +0530 Subject: [PATCH 253/507] Add a U2F-specific audit log entry after logging in. - "two-factor" for OTP-based 2FA - "two-factor-via-u2f-device" for U2F-based 2FA - "standard" for non-2FA login --- app/controllers/sessions_controller.rb | 13 ++++++++-- spec/controllers/sessions_controller_spec.rb | 26 +++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1c19c1cc1a..f6eedb1773 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -30,8 +30,7 @@ class SessionsController < Devise::SessionsController resource.update_attributes(reset_password_token: nil, reset_password_sent_at: nil) end - authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard" - log_audit_event(current_user, with: authenticated_with) + log_audit_event(current_user, with: authentication_method) end end @@ -117,4 +116,14 @@ class SessionsController < Devise::SessionsController def load_recaptcha Gitlab::Recaptcha.load_configurations! end + + def authentication_method + if user_params[:otp_attempt] + "two-factor" + elsif user_params[:device_response] + "two-factor-via-u2f-device" + else + "standard" + end + end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 5dc8724fb5..4e9bfb0c69 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -25,10 +25,15 @@ describe SessionsController do expect(response).to set_flash.to /Signed in successfully/ expect(subject.current_user). to eq user end + + it "creates an audit log record" do + expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:with]).to eq("standard") + end end end - context 'when using two-factor authentication' do + context 'when using two-factor authentication via OTP' do let(:user) { create(:user, :two_factor) } def authenticate_2fa(user_params) @@ -117,6 +122,25 @@ describe SessionsController do end end end + + it "creates an audit log record" do + expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:with]).to eq("two-factor") + end + end + + context 'when using two-factor authentication via U2F device' do + let(:user) { create(:user, :two_factor) } + + def authenticate_2fa_u2f(user_params) + post(:create, { user: user_params }, { otp_user_id: user.id }) + end + + it "creates an audit log record" do + allow(U2fRegistration).to receive(:authenticate).and_return(true) + expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:with]).to eq("two-factor-via-u2f-device") + end end end end From 7232bdb9ad459147201d4ec5250465776168a62b Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Jun 2016 10:23:27 +0530 Subject: [PATCH 254/507] Add feature specs covering U2F registration and authentication. --- spec/features/u2f_spec.rb | 239 ++++++++++++++++++++++++++++++++ spec/support/fake_u2f_device.rb | 36 +++++ 2 files changed, 275 insertions(+) create mode 100644 spec/features/u2f_spec.rb create mode 100644 spec/support/fake_u2f_device.rb diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb new file mode 100644 index 0000000000..366a90228b --- /dev/null +++ b/spec/features/u2f_spec.rb @@ -0,0 +1,239 @@ +require 'spec_helper' + +feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do + def register_u2f_device(u2f_device = nil) + u2f_device ||= FakeU2fDevice.new(page) + u2f_device.respond_to_u2f_registration + click_on 'Setup New U2F Device' + expect(page).to have_content('Your device was successfully set up') + click_on 'Register U2F Device' + u2f_device + end + + describe "registration" do + let(:user) { create(:user) } + before { login_as(user) } + + describe 'when 2FA via OTP is disabled' do + it 'allows registering a new device' do + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + + register_u2f_device + + expect(page.body).to match('Your U2F device was registered') + end + + it 'allows registering more than one device' do + visit profile_account_path + + # First device + click_on 'Enable Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + + # Second device + click_on 'Manage Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + click_on 'Manage Two-Factor Authentication' + + expect(page.body).to match('You have 2 U2F devices registered') + end + end + + describe 'when 2FA via OTP is enabled' do + before { user.update_attributes(otp_required_for_login: true) } + + it 'allows registering a new device' do + visit profile_account_path + click_on 'Manage Two-Factor Authentication' + expect(page.body).to match("You've already enabled two-factor authentication using mobile") + + register_u2f_device + + expect(page.body).to match('Your U2F device was registered') + end + + it 'allows registering more than one device' do + visit profile_account_path + + # First device + click_on 'Manage Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + + # Second device + click_on 'Manage Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + + click_on 'Manage Two-Factor Authentication' + expect(page.body).to match('You have 2 U2F devices registered') + end + end + + it 'allows the same device to be registered for multiple users' do + # First user + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + u2f_device = register_u2f_device + expect(page.body).to match('Your U2F device was registered') + logout + + # Second user + login_as(:user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device(u2f_device) + expect(page.body).to match('Your U2F device was registered') + + expect(U2fRegistration.count).to eq(2) + end + + context "when there are form errors" do + it "doesn't register the device if there are errors" do + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + + # Have the "u2f device" respond with bad data + page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };") + click_on 'Setup New U2F Device' + expect(page).to have_content('Your device was successfully set up') + click_on 'Register U2F Device' + + expect(U2fRegistration.count).to eq(0) + expect(page.body).to match("The form contains the following error") + expect(page.body).to match("did not send a valid JSON response") + end + + it "allows retrying registration" do + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + + # Failed registration + page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };") + click_on 'Setup New U2F Device' + expect(page).to have_content('Your device was successfully set up') + click_on 'Register U2F Device' + expect(page.body).to match("The form contains the following error") + + # Successful registration + register_u2f_device + + expect(page.body).to match('Your U2F device was registered') + expect(U2fRegistration.count).to eq(1) + end + end + end + + describe "authentication" do + let(:user) { create(:user) } + + before do + # Register and logout + login_as(user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + @u2f_device = register_u2f_device + logout + end + + describe "when 2FA via OTP is disabled" do + it "allows logging in with the U2F device" do + login_with(user) + + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Signed in successfully') + end + end + + describe "when 2FA via OTP is enabled" do + it "allows logging in with the U2F device" do + user.update_attributes(otp_required_for_login: true) + login_with(user) + + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Signed in successfully') + end + end + + describe "when a given U2F device has already been registered by another user" do + describe "but not the current user" do + it "does not allow logging in with that particular device" do + # Register current user with the different U2F device + current_user = login_as(:user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device + logout + + # Try authenticating user with the old U2F device + login_as(current_user) + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Authentication via U2F device failed') + end + end + + describe "and also the current user" do + it "allows logging in with that particular device" do + # Register current user with the same U2F device + current_user = login_as(:user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device(@u2f_device) + logout + + # Try authenticating user with the same U2F device + login_as(current_user) + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Signed in successfully') + end + end + end + + describe "when a given U2F device has not been registered" do + it "does not allow logging in with that particular device" do + unregistered_device = FakeU2fDevice.new(page) + login_as(user) + unregistered_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Authentication via U2F device failed') + end + end + end + + describe "when two-factor authentication is disabled" do + let(:user) { create(:user) } + + before do + login_as(user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device + end + + it "deletes u2f registrations" do + expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0) + end + end +end diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb new file mode 100644 index 0000000000..553fe9f1fb --- /dev/null +++ b/spec/support/fake_u2f_device.rb @@ -0,0 +1,36 @@ +class FakeU2fDevice + def initialize(page) + @page = page + end + + def respond_to_u2f_registration + app_id = @page.evaluate_script('gon.u2f.app_id') + challenges = @page.evaluate_script('gon.u2f.challenges') + + json_response = u2f_device(app_id).register_response(challenges[0]) + + @page.execute_script(" + u2f.register = function(appId, registerRequests, signRequests, callback) { + callback(#{json_response}); + }; + ") + end + + def respond_to_u2f_authentication + app_id = @page.evaluate_script('gon.u2f.app_id') + challenges = @page.evaluate_script('gon.u2f.challenges') + json_response = u2f_device(app_id).sign_response(challenges[0]) + + @page.execute_script(" + u2f.sign = function(appId, challenges, signRequests, callback) { + callback(#{json_response}); + }; + ") + end + + private + + def u2f_device(app_id) + @u2f_device ||= U2F::FakeU2F.new(app_id) + end +end From 09a2f2dbdc4a14a2f4199f33a898ebf2aee383ef Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Jun 2016 10:23:57 +0530 Subject: [PATCH 255/507] Add documentation for U2F registration & authentication. --- doc/profile/2fa_u2f_authenticate.png | Bin 0 -> 54413 bytes doc/profile/2fa_u2f_register.png | Bin 0 -> 112414 bytes doc/profile/two_factor_authentication.md | 63 ++++++++++++++++++++--- 3 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 doc/profile/2fa_u2f_authenticate.png create mode 100644 doc/profile/2fa_u2f_register.png diff --git a/doc/profile/2fa_u2f_authenticate.png b/doc/profile/2fa_u2f_authenticate.png new file mode 100644 index 0000000000000000000000000000000000000000..b9138ff60dbf3085b0083c047541df609f69122e GIT binary patch literal 54413 zcmeFZcQl+|_ctsdT7pCiQKLqW-dhmSdp88p%V>icEkX!_=)Fg8F27%Hud2I{~ zOmz&5n=H84z?B!S(>F0N@ITng$!RLd$|uFci8auJ`3fVdac_5Tf%au#&rJ8Y@vkd%Ac2v~6KoueM-(eQF4(LzD>`;r zYq%k)81!~GD<)E3*>lUKa?4Z6pREh94!WZSdWQEiQ$-hYcOJ!H$mD!Lb}(Ul-5nL< zkBkr5)Leg*zTYSz)kphC+$|1insA_HmXg)xo)(i|PWMYgOet{uZL%F4sYIN0nUqTJ z^OFzDeoOGzn)}7>GYyi#Ryh(iW%vjyDGlX%+C*XHVfpG3i9W@Bif{F|2+=2Cw)AR}RtJ(W~+x zOjlX)<6E-y)SWL^USh_{irsxR@to!x7QW2AZ#Rx#y1U})1{u5F$-vO~c z&i7kHK@@j_lb_%5!-&6&weXN#J~)9M@(hRX)xblOuXy_*7rB~`up@%w=%tumP0FU{ zG8AL6hY`v_MutZy*BLx%Hec_^1&=&BBtO5y`1n@gm(ma1Ex7fsK^D>?)MnVMpUZww zi_pzsCx1@-sr8Yv4R=0dx5M(3St1%ETy?!yDC>9r3a-475^K`FMeONtJw`1 z%O#UuM|HDU7+Br@Q1H!R%YKV&3$*q47?Uu}>c^q0kO-aW9qwBV`s5>+yAr1#={=m_oM1gNPJ2|rxXs>bDyG<^d;b2AYr}v~o)SdqO<6yHor<5z z&v(hU1Q#&kN75rbk(Njp5(Q^;_Ynn&YKT^vwwWe=XKsA>;K_Ji!1ZIf2$>=OQ>a5;iq{Pv0rxU@D|~1 zk=yw9GYo&q+|uXO#nUq?(=He*;HxYx+^k^NoGpSW9VI@H8&u#@aBi4})@eb~n&QPY z{2R`19o<)j7IN@M8FjY|P>*Yl{v5q)Pd&LlnSwx!za1GEcN+66R~Um&C?lpTCUOM} zD=KMnTGg9n(Lsb-J^HLhwpHpyLm`z}E(ts2v*fe)Bo!o+%%aVL=NSD<+iI@ZFO@IF zu|+UVf^~y2u_Z%4z?FS|*t43;>Tu$N>> zLc`mYvXmlwQ3}(MmiMmLD5yRRd3E>ax-!& zE@Q4a!v{tzhP1WRAM%WD89G;QemHkV|$V_TJ?eGS2LM$p03;LArLBAOFqhOX{}i z#sxy5|20Z`d;j~yC&8|xVw9t>j&SabeFL7i@5#~5Htn0R2E|;(hN2AH5ZlHz5+1cG zrYh_zUNFaQ-Q2d6A)Rz~rqm^;9(kBv&x3=yJ|)Vt&MYeV!Zx=M;Mj z--ooAzI0iELVhMjH%iIPyA*I*L!)m(SF?xVECszfk26>`{pcIXDl@i)5fO zO`5}N*&yRp&_XCPbXQo>*`?oLaCngJdHO*EtuQ!UVLh`8YR;wg@(1B}!mDUs@l5Z@q_0WK33s;fzfH8H zH#WPTpd~quO?m1#C2Nyw4QdTcSIsAzG<@fy{muL0`*-y}>IrNcAlo6oI&bU*wFPtI zP!W~TvWp#_DbB@UhP4t28*J()=`WPROS5RgCC1L;w>2OWeG>{3=8mU=i_;_1{SYAt z1|KZ)TGy~&kho64r_gBX8V`A9PSenJfRe0`aR zf&TX+o{o|XhH9Gha$t9BdO>ah?#B#L`1JJj67DZ-#I)s~{`+;{FG&VFPfu4d9v*LR zZ*FgXZm_#853i`GD9>X)9zH%U;0Z1dsEenC50{GvtExv_ObudlZ(f{rv*%q=lTv0FZW}f|FjLfDsg>POw-=S+R0Gf z-r3s41L#9aL_kR5_w)a+JAZoo>rJCSZwd-N{`<|p?)>{r37+c-{57F}N$dAjKwMJz z5#eJo?w~>(552#H5&$7#K1bO7gP0J~y^9 z@mjof8haX`0sO<8f-R)n%y~^NeVT>ZAp=Zu51E*mwI1p+G3&%72r%~QzWM0J#>AGU zRq2@398i|kzE)ndc0e~Nif)$5_Bg)UZ9O)249u=O2?+2d@GT6KqT{=XMUQa?3;(`%^F{~>*^U1^_WP>Lts5aax9Gj({=9yDT}DI;=YMHc z7r3a0VJZB-^cF$_`RA)I|J&q01@d26{*lLjmFSi_cHvwgKD{mY1vT55iTTP_S|MRX(&*L37qjt`r9I<;A2_FzG!zY@i;{3EM)W#-B?&&PoWqa zyQ-a;bZ&R{2!rGQW!Hk(6h1{VRLo?nk4Sg#tp14}6g8}}Nka+Vcr5RqGn=+onndIs$0jhVEm{uDOY%7yI6NF1u?&v}Z$Ah!DE6UGBHBo1l$|O~1Nvak2 z{KB%Ay=SXtIBQU7#_7%b65Scm`+sbD5FW|9_KU-D4O_57&9W|D74{yoo^GU7LhHC9bD6!s8!z^If|s1P>({1bxUA<;D_191`je~fAUp3h=9YsWX% zbdFEm(rT}HH8St13l;Wa#Umc)l^FCG1^w$Z``0mLj_b4c;%1-s3`&KC@!5;M+pLbx zajyG$N|;ltS-v~>=vh_%jQa+zs`Jel$zwzCqTsyquyGd464LvLh>}1_WR`KNfOFm* zp<#&w)u+B%Hvf1D|7MKTxn1GYJLrKJ{{wrtx|USI>-ah$=!i<6xjHkhwxF{%Ak{`(KiZ4}S?k zj(ck_ar?$^Na#+oab%R&DRn4usnrUp)t4$kAO1PdS;TLgEfPszxK=f10rNmLWER%C z#^qjoW1$Skd&q)bQ9S$mX_6atV!b@sTUNgxvm4812^qhep2qrgr% z{I&&)e?63NU!5;5B))C7v-56zfw8vwpJfdyk(VKOaJ{Z~)i?1%U6nhVHl<4{VSMjO zX3o~KrDMmm?)-WB2Ciq!r(hzfZ>U?>k6sb0r%`l-#2aVaRS%~Kv;4OsbvR}HLe3e`)`Qm!vVRoVGYzAIP4<8>gIEAx;nkll zw=MuUhObr?|33yF1I+9ZDc+xD$r;DGtB#jO2XvzKi1?3_C{u(73`js4=p5rs? z)JbH3wgZ4pp5{LLOVKh}uBCSI-wFJmvI0Rv_fBVb1CpKFFOL^mPG-I5otn4nQPh~~ z6u+0>jQ*u7EJ%UR5x3}ts;n$MMi3#%&yEeyF3g~Q#-$mOf?Y)TTls+{MO`b~Z#@94 z!(H`!!8870iFlCTUKcXcdoGo$c?*tYNq34HltORibLn<8e&MiK(yV2ZyH;K{oqOu+ zJxZ*Z39~R%1#*CY_`A2ZhQyxyG2O2{I%EKPJ9&_=pVMEX-4XA2+zdYWFpN5GFoieY zkwSF-&8O{#`79c*Zx%?ugTdqqcpp3R1`hS(cWBuJXJYAlz*xSN6lJAlx^+{wKG~`r zS4YnI?#`kI`wVaXe9#|kewPC4M#wmQvHIh|gphIc+)3Nkv}0A~4@xq+%7(D{%N_5U zTjYj0BE-H9_cUA2euhbBfCA1|>J|+YrlD(@HLb_fW$X{oMUQVxlq|(aOQ9zl+Q^2q zr^+VpoB1rT`AzaHXZs~4?Id}`fC77 zd&1IX>#jdfx|Bbxel5#a$i3;FI*IUZyoMBI=ZPGVBx(1XijRvCb;J{ws>7kTxj*LI z|3v`ykvrat36w~;FHD8a6izG8`05%!)dbNVQX9VwMSenv4oG6oe#&Hn?&@b)U8nx+ zVYq=B29+*9pJGbVkb{1N+vU-e?XLf>Z;yPy3Ws@WrfUaY6xP=itk8=k9O+Y8*H1IG z`iA~mv8mK~nF}qaR~`4;-nCw=@Kj}vm|yKDZ~X`+BHUw6lG+Q`c)=5}7wJeRP047S z>F5!&$xzj6XVoXscx}vT=UkPhJ3<-B~1syvQN+dwwb9Jxk3{(WxW&sHn%!fQ|K331dM(`TQ1?F$4b-(?=uBY(aD`Hn-#I zXEA;}!cBpvY#9RRe49m`5#7&#$3HA%(?*Qyq4nh)Z>+a)0eh^aIaWS4F~~$Rpr#$a zK|ZH6ENDX1AH={{l1xI4BbaK7PM*@RPkbt0b9npnB7Ih(kRb5EeGY|~=~HITeP6nH9E5BT#TY7UKv zhEGq)Jk2)6iqUUg*-RmB2L)i64|A19(CO2xx*7`RK;DQs%5|Lxp<;RP1lO(N3n*wNn;KZ%W&q!^6m>-Pmm^3F!8w7 ze!Lr@R(&0Z)kJbP*{MPi6+2vDXn%^cbKo}?o>c605d=o zT{~~UlEK8eyj;4%Rr%|>ySrKG;I^|6Ddyb$vi|pNnFU0#=p3<&`i54n@sBl$`o_Jx z>>!>}1F>gNzculBys2jyZ+7gPhL~jfvC{iEUD85Lr!sd&%`X=3%el`uH-H|MRaT3T zPK3<^X7XWF)5*FB2y`+9@&Ig_7C8NovN$ve_816G z%fXPV+M^1P_pIQu)yAZeJ`In{qU*St@Ptz3eZ?u2InD%y4kaE_LdnCCxjhArreh7! zm4xmJPO%ueU#C4tMd|a=y!Sm$$s}czLFoZmYsZzq1`W|x}=?GLQv5)D8oeKY8aYo^kpXBTHEBg>~evHNDBJ~sOGV*M@W z^beowqzY5!7ar&~sJflVPDN7t&z42(AmP=W4I-yG`l_@25REaK$Vjup zypgoL^IUYnYrdJo1KNqCvn(>*R_ zC=Y|5cWVoI2^{XzQ?gUHIqvA*vQGJnbkKi|F(C5rngf&j49V zJaCrfxqrwqznSYClMHVTX>Uq28gXk{TdoT&ONV1+*1am*3EWUIXQvKZL?vQnW4!}U zjGc#4xrkI=l@*+==f}|MRKih6b2->q^}&#M*m&&Ek^btY+^D->t%`TvZWg9`x2njL zC>Vgm$UMOxf4sU4KU+<&GEgmmetj7B_8z$dbOtaux0@MIEqXWuLJ?I>DF!$RZO++W zB})l-K*OayZ6;a98Gq9zpb*5O-A0vpwWfgq#9No5^N5sq zl@9A%6=MNmn{)Y0U3iomkCj)arj^zVALda>{z8(bcKL8`QAKK0=70}*=$vJ`TS1$UH6|fJT8vw172ThgZ8mg)n-WM0Cb6TCY$OV1ripb!U1z>M_9Goi zP~YhDH&r|Q3V9;jWdZ2@^UDVsEDz1S=tjQLicaE}f+1$O&`PmYAk#>A!kX>fQhA>M z2^%k>$i#B@;h42KAEu15+tQ<#wf>?tuz`r zQaV4Q`5`Y!Z#JJ3aaNzcX3=9WT8!#l`z(fat^h0pPn^H1B2jTA9_z2r`sm>89*Zr& z3qbDo^U%%VZZ5nRwt!a1&dImY1;k?hFzN6t?<;6%wjPQb{?cwgaa;&C@i&G{w(2ku zUS6rk&JC0{E0dX+>vtI#5Vk_}`qMLB@qI4PXN25<=QMoU)|?hgCZoX+NLlZu$u>ysRX8Ed((? zk&SZGegp@ZRw25SK_i>>9nj_}w*)aO5zSmG39xeCx0$K#b1_+Hbe`OWnd_apbYoZiJ z2rN97t<7?d>wrx#L;}UTcOm=EYD7@FDy%n>vVcfn?|0BoE>m9XZf8OhRL)8(fYYsEz!n?#k|78G7SRG zO>nikQPg-$Gz8!6Kb5^w@E`Iwc6RyM_F$bW)%eSyO;xWaN-9bWHK&xIlYey?U{fq+ zUp!Y5)7<7aACtqYm{vI3#6c#i9#Ir@_&|<6ta3=CPH4q(DdBA%;7e_8R~YxCO~cvf zcue~%=~Sk~RUx-d`!TG znPn-OEWDU{x6jj*<&Tv3BDQx~4LRGOFb_Wo zu~Gobsa6nrI4<9O1MT~s1!L^=Ys(MRlk_!**@OQ6bmzP4_W_Tk{n;@u%`BqnQO5F{ z>qOpXs6K~=*?lpKV`208qrn(oc1I*HYr%l*$?Q9pbIsAgxjC%l%qjV@cis`pPGwZl z-5dmr#ci82eCMl?EL|f4y$+v0?zW-3Uzjh8oN-542ha2{O)e@@6}=#Ze4c?vVNQ@n zt(n`t%`Mn92}ixM7aS%d(;=ni%R&Qah+wax&@})uF6Nhwl)bf2wtwW3l7I zpKzNe-W}hQn4aWl4oz?wJ=!`ryo2gI>6u(8I?b{``p*{4?m`A#ijxDIPA&mQWyl3M zz&`98l<44G3#4{MuVDr*eZPIjxqMQ}KU$9G#^R5qy2dM1mU#wG`@<29>n$+0rDF`k z_0$5kKz2}Zaqm1e(uX=UWC}?6(R6c@N#-2t>(hxKJo_W!PXOcIOS!BR9P$rRJig9*9`9v`o`9E{W*qPUM(jtbLV@aZlN>9DxO+ujre<~V$6 z!e*PY&`L6xYb#U_w=F%4=*vzvn5!>sdfs#D7iOU{kIzwLg4w1w38zPYRZ4iST|Az) zx&6J{%>mZ3X6UK$NW7=;tjm7F?a+^!ri$Au4GNEFA6BW?lpHOhCXE=HIIuIR+IpZZ z1*yat{-Ewx;P|M zFG(X9?$fV5kx=IeXfA5FOuIKCm0*}yZ~Ow$#SoBh zkf@>81UW$!<_2|M&Cp#Liq1KfS3fP>%`7etyhNFQ!67Fr`s%h%gn&;z$<|0eDpXe* z@En1B9%363uH+}LqElH}DH?Q1FRW}VAcdp`H;jRy&C7H|rTaa9doO zx#AWx7|xHfs@(r3`2HvlWrXJR356`qxEaHQ6xK+4I2@c0Hq>8$?)*Zx{}LbP4Ba%Q z#&Mgq%a7_vk%u&z0^wfA@)MX%VrFoGmEX*_A_AU7w%uEKiA6(jm$o+UT0oapb&3W$ zgJetqU8-Nk_Q`M=0FkNd2|tytmNS#~lK!GGch%3`%;^~!dlq3_+u|`Av&Sse+7gyK zglyWMDA89~YA@X{iT5(1E~wgw-ma+cnow1Jkj+&6{^(R(?#jXl*i<4V_(c|oTUKIr zCdN|6=NdOox1A@;q=f!xMvr~eaHD;|-k#nGf7glIp>IXq0<@wzE|v;H9Sk$#O=&s-cD? z;fmmH;a#JJg8`Eh>d@Y@|I9nSC-ff{#jcq>!*l2O*n}whl%O5L);GgSa!Td+oneLU z`2!VA0q4?1WC~`1k!?=PBH68MaC(@9R@2_G>>6Y4hDjaA1sLKk8&Qh&I0v1JS|Dtt6~S)~4McDMCpPLqhVeZXiu!2JHSi2Ahx zl=T(vcjaCS;wnHlp`FfLpOjU-VH-T0zuo$^@?m1`I_!Am;Exqq$mxzfR|-fdk}qLt zg^s7s3G8@~PlG@6S6P`~?~MG#l>;uvy@TJo;h^NpnA0cXo0mW`bCY#Ys-S?%+2D2j z-E&IVqCR7~XvumZn-@#VBCqDvg7HxWYro>kM$huy^QS-*wrL?UdbP~1WA>A1U5IG5 zMZh_|A-q0(?>gH#18O=X>SyTb_0HvlJKM7ede^%tKh?UAZLboed{*3Cw#A|NVH0hC zWoTmXVA5aT-mW8QW4vf)hgao46Q4np6`nDZig36KEW;0#ZFm@Grl1pIpwN# z*re3$EQ`!juZ$$fDO*;XY+>qmfuZ7UC|hRU59jwHWmRpkLfdg5FBd_$;e0ZHLdY)7 ztV(R|@2`!sWY6_p`gYBH+@khx5t<5|1$I~u822z9qoX8S0G*KJQVSwiZxhcljv^FW z&l$;Epo_MiZM>lJltyLQ_|5>3M{r`P4Mt5^GJ`=^1z)qv@g!MQ?^|oVjc)WMW)mHe zoXUJ4h2Bg=p=-nG-k3CJBsJ^sqdKFbYZ+Mge-nb>Kze$*+Ap_Y)>DO5x1FhfpQ6@; zAU~<&X8QoI3@(%itnF=+OIdfg(X?}to0)U>&>8A_v913s_8#;oJvXp$owkx~X^FeH z?R&s%a)mmLK%4W=+#jFXGk+WII(9Zgsdece&p+M00K-Dn~YOSN^NAbFp#G+0|T=gsL@e za^g6(zFoRfh3NQ(#=eDHV6Db!C-85V;Z9!?FF%h@X*X+E&-uLB%fwddpOD!jqeKpsLy>}=+pT zBX#3r;R#EiAA5(=+5#%WhIgW-#QB4U-})*1e$;OhmW~LkU+ZwYTg>p%wUj~N)n(JD zmyrlw<1E5|iq7|Wa+JncbwHmj)guWYT^q)hR0ZHyeQp%WU%XN2U#Ucq+B)JK=C9#l z%5NSA1p;%OWb5m{>ZSq@AR=-!C)3_}m)D<#S*PZ&6y@>wIvOQtWNzz+b+Xb*8+cwk z+U&@2+8EY?Zq-ehS)2##6~j`0#6(Rk!OWIT&wEW+jFUG-%(C(`CJSrA&YDbE+f!6P zio-E`R4*dS)<%zMEvDWL)osCq$g^@;k^o!_;$*M_=ay#n?B{9-DfwP!#xhseuJm)5 zMD1>Vbu|oof*=d3v!HuIYi(`c(SP#kC(k3Uil%;+6Gij;*igv{F7ZA3kx)Rh3F>ap zuV22wdqg@!QAIO6o+v+&OiC} zcbZU@NLt@5UT!w&VDs@w176X^H;nK|o;H#9COvx*qCzz0BAkjELyi6yCahDs&);H# zK0I9I-KvB^oK_>}W>iBeq%mp=?u%uN=izmEjaDb4?yNSAfTCtUsNBEG$dw zmc_J|as)+@l#V+rH5=FqiAb(553nUb4i0P%NyzQc_C=ZGZG#I-2^FZ%wE+He)J8jx zuz#c?4$cmgInfb06OKpkG=nKZGLr-6>aS54@r$2dYRW2`*tfVnuyg~Y({saD_<}N$ zZIMDe8EKwW54=DW30#s?Ja5pWk^|mT9I)W+%n=qI;!e56qwgNW+nG-?QN2?oLQ|cO zX(ZVOj)k0oI@2R(J=7hMRRYk# z+zEC%ei6&bqr_%59%-+#!Nx9r0{sab^ZT2o?bTdqS>}EcfSX8k?Ivn&c|2&+@&<|E zl{8Ic04em`nb%Ft_TS?(vc*6VEy0CUwX|98Er+g;(n2MM-?kOVLQ!98RJiJN%@DjZ zFXZrA0N~+~6L^s+TB-PROn`_26nUo4uO=HXz}xc{ed;pjMr@i^X&_8_TaC6{%m&Eb z;(-^pPvHea=5(uxBhwCL74G?)&eg_Tq-kPqU&?O>!wY2hxTs5~)S9u3!~#nAd>7K! z4%wo2n~W7dY^0EANbtV3*gKnWYmrc-F*)28%C0o0E6it2-}>-{5V;%p3$2MIie%09 zb7$-^=V1rpE|)-jt4YZip(onmYVsm_bFbiSy=4;91qTe))7@iP`Qn_Cl zm_e;o+`sd=SZjp@K&U8_Q)fnqioP;WnH(|bQV~h~{G>WwcK2)BTY zkBWQ>$Nq)gT%)5^Xj)v_(t+_TYiw)FX>qv0e z#wVc*FR)gC@StxFU<;g_KD$Hh{oM@-U~}JR$gAFD8}l>kl3ALdCyi!>4lRi}ANz`T_rs?cB02N28y9w(cZU#YnTi4YCgE%V z*+*JNawSpC3!S%Ec9R!5Ay+T@#;oYDrU8e(E?4M>)uF4LNK|Id=Xb)5NLW~Jbk+Fa z@Zz*K5f0>_CM=}Bs{QIbSNamLee=HQC70Jwmd_Xa9tWefil&%(uU0yBEv>mLuHr=} z0CPriH*dvpwVf_5TRr1=nekCK5U_N|Cp{~-ez65H6b*|@CxvEw7!;nDxQ6#$f#`gG1Z~BsaN`|yEv`?` z1KDD^WDqiUr&Y#HFiGV|K^Ec-a9D_ro*l_rwQ{%o>~I3m&_Mtg73}BcR9sj;tE|?i zsLOGw?jp~vn`z4GMNTztq>V$4zB0yyte2I;mG9<5mnP1&s?zXT$@&5%Os3%7o$Kca z$OVv2vrT)sx|$9qtoYRSy{!^)kEXldJEq3jVQ{?1VZuu0PE@v3X=O6EzBH+MD^>j* z%u(D^wLe*ClN#Y(p-(>HajW%t+_r#TqZ;=6ZDo}w%14buqN>Z+C`GoRoVEUlN;rY= z_s55)BjDMkqfAanpl*NLt!mBTcK~`lBt*uo=q5=<4lw`it}|i8-|30Qzc};Zv!aG_ zsO-~adw6wzy=(A?&IUS}w;Tq~*J-Rc%rGle5Wo&98de~|`#LH*S(xU+Q7n6%{pX7( z{n$zEV?#hRc?ne!{qn$%Q&EONUfqml;e~xk`ckYSt-Z7(*O7%Xjm~lXVRHM0^-kw4 zDNIp0_q&5iN9a(k`+l>@TT5Nb#^>(Syr zJ>3@N8kaMP(s+H# z5(mrpdh5ItT9-R?O@G^=1X4x}Dlb71b-Cpna-h&Ql*TC4R$lG$2S*#8)@>cT1DgBU z^;_Ssx!jOGn340t1rQHZ5ka}1SYVxmUO1)bvrRKwkk!SP zmhhi?ZSFh6qH7tvV37)m05(Lyd z^>g=^6Ash0b3N8kf#^yZ2mjGd4l~Zqz4WVN&|Y76tUbBHia&r&liB3z(#yR0GzHzMw{wTdw2#U!}cxUGtJm>~df8 zwSo{L8; zPteco4c^Q$Z{N`Z?6VI%(#&aHY8Sc@P-dHvbm@n*JcEaPdI8Yzlt+~R%bWZ^g*W*h z!tZ~nxBsRa|C{*yU+(aK$KI{ z85sUB)-kX`p8ym?(t$xfSrNe{1*boxb)Z-Qpv&8jgqXaKado&6CHEJ>p8l~65HjBA za84KKDB%)~4*tWXfBES-5Ljb>fjlW;$Y2SH3I1!W^dsKDYi*2~KCf_#!ugqQ{oUJt zvTfuYB^vZf*njBu7&rZa#LXrNK)4MC(voUpe^K{ee(D5(FP5PXyKF!>ADw{pcXy8; z0^Q*)Pz`*>E&A-rtoL_!wLrmwgNn<`o`)IOxXFK)GSH8_0wT9twX$@tTptKnjD`I* zIT?PSMVppTVybJQy}0=wy9Y4ArvY%fPK$ttp~K#%y!Wqi0gRgvKqQ-*58SCk#@_|S z)BG)ecHkB}MwF#+hdn8x_aDVvT|k3fPEYG`76kYXU)=e-FH>L`rkwe+Z(JX61-k!Z zgaF5bltk%=a0lBBMop~0rz~<4mlcOGQ7=S?Jt>F!f3)(tl;ZRnHhU#_6pO3?Mvi@3#S}<^U{)ydPku#W+nG**a(2btqo5N0C!)$8}pk;;f(C=tZnk zGRUhp3FL(gQOR%_rS<#y`I^0d8)Do2mJ%sGe>QYo&9p(D5=Zd*+L7JZ|eFvfh?kxWcwgub5K!gKXwb= z&y6Z*>19a|ac(8RG={F{hVkJ&f1PTR#%8KQdugoHJ(a8VaLlTH z5A(OJD3rQZVj*=SfZ;a{Fh6UGvS6i(E`~$r*Bs7wyFih^{g9C-x494;!4;i^AE=wJ z`SZw&gJG~1OPBVV8xShv1eG2n#$OZIqS=}k{Q#Qq&^2Xk&MR~Gn)2eb@cRCaGgX%| zjY(9VsRC5_UCR>9?pIxx+s!*oj&>Eb^TCFFp}WvwUwFDp6C0B`^Win?)2S%aYm-c& zX9q|tway!lHeOdMddeKZ2R0D@TSKyI;c?Zipd(fbMjDxUp1!;wZO;AQN?p zSTiAHDRYG#jw;r0)qi8~1*-jSb}%TaaWP$2BKW3+FOed9C+n%mk&tJ z&HK(QCN{37ai9VIV;WcEa-5Y{mo;Tg%@4(lK(kt4MmRyvb8SB9l?D?Kyb|44DJ}p@ zyLIu|w+-vA#YmpHnQr{wmee!&dXnP(K}uR8C8pP*#c#?rb)EmotT2$M-R^eYoeKR{ z!r}gH(I;L{M2WNg5&+BwZv3Yxf-X#F&3ENsiN~dJCFvR=?4LWIY=|~}(IfN-KJhW0 zat|BAhF%~FOzG7EY9-96jcs)Uu_-O}^^WRPMM?;_fSTO82#z2wS$-e_JX-9r-q?O* z)a$qxkuQ8t-D?XdzPW3hvvpFO+yJCOysp{LmQEi&V_(y~q&FF2rGL-k?MvXi#TqGt zCt$|sxoQiy0xVAW6n@@;rb-I?s4_S>n%)L4?5C zNFTE#7y=209)d?a5&7Gt?|+>H{<@zbgK8>t(RKfn<+tDauDyU#JdnMJMP&rTt2K7Y zFs_F#P(tKY8l}n-=`u#rYhbr=P@lvEUNDM;Z{{8J8MN^=ltu41R4XZu_Z3KVY1o$x zuqStWbb>1ZmGkyK5ZEKAe{Wp9PAkkk>UaKR))A z=&K~z#P%=IUlF^(mI8iVo`mh>iz$kZ1L=U{l)Xo4&MW=JR*5~=B=N^*K{h-jRBn}N zexJr-i|fNl5OH~K7SJ`Odp~;kDETM&FO440Ai?drWr!*O_5bu#B4JgLF|#toLuKS;0M}6s)qY>A2Rv4y1pP7~?W`=qBr#1E@_Sub07WY16Nis22v!Mc*KW z7Z3uw^4upzvtt3EFQxqe9~<0JSAM$L-7;zHNHz)39P$qTRWOhwjVEZ%gZVR&@l9RbDAs993nxQd~KcPIS7$(g&%- zA-y~oUM2B3=MB)#gGCadL`L>PW&HS>cv=O%l{M5nFNpTJt|Xvik^I0UKnn&6QyQ`@ zO3rhQek{M|@W~~vIaxutU3L9G?44&+Q(xQW?TAQGkX}Tjcj;YJid2!_LX##XMrx=Q zkPgy2C<&HP~6#x)EP=xIYW6Z-j9#`lJKK^sh_q` z(Cj{&rpnK(_s_n?{3)=Q#&4kV>4V1tVjD)fLdt*C7Hm*rbNGEZrGSN2n+;)nT_cFW zwg`BjClLEliL1X?bF3k{vTj#zsc4y&m0t%>xgtNt9q1X6B;J;g{#Z9Iq5(bm8>qSY z?!@!`#T&^DHuP5`?WcU>A{-g4gNRDEkVkIlk#}UekyQio(mTGN@ln zq+SBzi$WoD@GPxE(E}r)G2(6l2Ybi&g)kg|dObEa>l4jb}bzu-`X{ zYBm+=lWnhuUupAfHL9Y>rEukT!KK&ZUl$VqGSPGQjz{|8oRJMJ3@&|wC?nR+S6-J& zCT4{Ll|2iP(@PJY$FAFWe_rV!3tw1dzr)_1{e)pueaho*DbYwwU~Eq!J80ZmpQ&NO zZQYo6%YcE3=vhA(wMXpX@w52;rS?fz9`Q%|#j$=q$u;YK*$&2{l@5xNCxFUByScDx z636#1N4%mEu%(nd3Y5UeXU2SZ3yqY!#0|y24m>KpPb+i)EO#3{_%>#8op~u;i>j|p z*Gw@NlPg&&fm3)XB+Q`p>(p)`+G4B`VmRRGjNSs4o(Vc+LK9!IBT@|2=gD}xPKG^M zA{w=Z#Djr_a1Qzxbdg-jFqEG_UiQl zu$;sVCSxkZPHtk=py2$XGeI9IxVMem3Bn+W^Hjrbv`Qqo&CV-6;^^Ibpn$dhN}x* zpC$nU83GjZkcR1WDCkQaxrdxfUaodxgri!fk%~y+k4^%vHS16N6GYLtz*j!oka;Z+ zn9G0!d$@gg>3SU;-E=K@hh}3bKS4O+fiC9Y)j$qG(4&9$f7FtI`Ee*(- z^u{W3A=>xgz+YFmo=@9mrt?;hy=5-B?2vwhF#68(cGI!5j|W6URqdGX))AED>O*g= zk4fpFBuLLTGTPjN%lUWka>wSd?yWaRpfGVA(dj<5lKgLYHDKW#pR2>UKqg|A+ippP zesr-NzoCQ^dBR)Oq2&+#Z%&Xf@ zQjVV5mH3o!x>_T^xG3Q?Ez%Y||i$NuLM8^IXR0-Vs079|Aj+ zheqjpC|0jcQFC-ep>U{bGiG1m0F5^-cKS{pT6n!S1izz=F z7gM>t-baLD5yjf|k95q0+kRa=uZH=SR+sU8UfT`FqbF z@}V>R{S2!fr@<=C^w?B6wk54+dlMEccti6zvo8Wz-xHIox($aTIP7gi<<3kQRtvBX z0SlkcK6P}WK4YOa^vu!N#0;e;5N6ve>Bw7>05KgEMG~tw^8)X2cenLEOk^I!^kKtZ zxJN>QS*J-@i75kl^Dixr=jz3bwYSa=dMBEQ0a+?VFslc2Eq{Q6lrD$@%h{M~rDbF- z7hlbUe(OH4`A-$>a|>W*C608$#I5}K^#piR{~UOofB1ZUY6BkK?=l5z%a+DShC_^? zS+CYR2GUmaJye`>EaagS)#57>XKpT^>2|sQyy?!(5lqO8hKYU~&fIwyNi(^jWV>ssbRv0Fy2JOaNiE~R7oUHKrz0(?! zdOM8H16+Nft4e56UB%VT8#y6!02oy0gg&dx(NZCAUWhON!-sOehl(kdiXEIPmk7IX z{Pz{WJFhb!DEejm6*y1&+`#?>EsyMp7D`4_cam6Vv{52L()Y3^V(||GoBJwHv*(0J zdgYzGUPc9Z@OP15Q`8UOnUI2O(4h8DlFxV;c&+$W?cQfznV0*W`JS3m>{BcAtj~&_ z%6=ah^+*k+L0I=x>Y9iWFY6P1Yq(8$d!@1E4qf>AQ4*CH;~m^^p83PWVabrZ<@ancy!6+PM47$5AJ&tuCVb+eOO(mpLxoKArDxf8wH=qQh#7tZ*N7>953t0%o*+`bOiw|~uZdU7Ys7-12X4)OKT zv!a2a3J;d5Eb|uj!_y^P$X1?mI+;@6gVZue!R-uJ-%Sg;^-w4B@1(v`;a^=!AMwm= z50QP(&1TKKy~N(~lN2{2URO-}5?f}?wD!JYjkP4t ztcaXMkfd50e=a2;LSO7rvV&>R+#o+uc`3y?)*BbKUqbi0$ko|i@HBbS%<7(njYLK9 zo-t%*mS$gyM<|RTDtLcq%!ogY@2~B%&usY4SJCx^vT5R3#%&!Gm7DsSLev+qdd02Y zlJzt1FZ;tkJ{}uyL=W- zUbZzstT^Dl`KD7l0f9tn(U!MSYHFvU|E`Ibb2|`g(FaZ?SDstu8ADiUg-t!u-vAe9 z54boU*4!UOt*$SjR;uYc=D+K(sdw$}wvpNr@m9Uzu-yE!A!W+%yRKS{`>EUIAD|PN z$%N8D=s~S9O0@#qziLZ+j&sGDILA!4^kr(IvWLhG$sl%q1a1GN=CDG)J_rg zJz77L5VGm(0=6xg)+zD)B*3+kn)xUj2?SsW^w-fpl;AuCscxQkXf&;mCw8N$JXl5r zX{AeE6F3@u?0>_??hF(VX;`QW(|B@p^THuC%i*sLe&1FjH~4)PO&v$bzVgW9=BOxf z-Q;oP*jo7;j^?UpW4Rj~r3HefwV}gY&U# zM?INwQZ+~GnBK>4&#O;&-Uz>CBHQ|hI+`EKt*N!TxM-{CI8>Q+)0_eJNZ!1f1&KX7 zz%KDVy|9}P{eB5y#{+C#YDSH|=_u4qy>Hy86r;<}=E@&;fp>PR&A&QxTT=2q*%$vmO;FO^#|*A^f7Q=3Mv0`Z7`yJ zAZ<|7z8wjzKrMwlu(U1g>3%Ee){}%bFr=v(6Z>+|GQ+h<;osbu_n9ydKSJCmoY$KI}099jOg)VVOl zX8BqX?Jd6Ng>>(5Ja%2{Zi-2jqgj`%o2k|m`qoQiT`*x{R zADjYWM}|%c`^4rxeDzDXgFNKez88lM?}kHYP}f&N$)kv|fn|GK5UOWL-N7Y9F>8$s z$Y_#%x>s|M9uek`7A&#iUdf&iBCIj^-VR;!kQU|Rk3=T0KOOt1KQ=GyzKfDHn3Sk_ z){dAx%F1caGb$wr79lJX1oK%c$%H~2QybVrKdY~q z&pPElE5Q8Tc%i#n>3v%F@CBTxVvBoYz~DGVuP~zoRq(^>mm%XBgijY*&h$?S`xGzc znPvFCRwo?;E(IZ5^&JDEueUxmTw8JTEsveJ<|XmeJsf`ZMCrwR^V3(t+2gE>6U7@DQH0tMHLhiaz3?a@>%I_Te{EGD?2=@}PBCV!XgavGcZbT%2SVLm zEdKG;_!lAHQ`d{l+tZ5(xl4%qW;tS2^5`M$$5VY%xA6tRjmxKZj>@%ml8m0_6xJ?E zQr&htlLTtxH`v3nbBvtFik0VsmVR`T2>*Q1Hy0-EA8w>JrpvLDs`gO!BP|lwDR#v6 zX=u}(_P0L}ZgKkVOZwQnGUB&)@{0#3F5{5KFZ(e~OC@#|XzZ0ROU6c)QF=-MOfReA zHHT$6)ujBiy8Bxy9;WseFZ4_atK$ z2q?lhTSwC1&NoUNo)Nj_^%51_eE7*S4`UCMbm_zvRe>GwT3LyU9CJj6wnI)sb_r05 zU7V@e@}mhepB`e%+TE)Hp;KzC4j^{Ep)Do;vn4qVux+3KkJ{LKHQ0eS?&6ob_Y;63 zZJCd8PR5rH?{#-+3GxHw!5>x}RhN~8+l5fNtSlC}uFIRsCTlD(An(U~+=6_D-_{_q zu7~%*Z$6{Be1fw*(9o8bG0 zDN`yW1-BX_%lV-1p&aBG^`4Q^x(Wo~3!&?5T)!Oz4|yj!%lT672cd6YVyhZltf&6U+YqD1qPnd$kzXabWS<{j|R7$gbp1tHsp+)iFd-l?`CHdR$ z^vJF&+@gPT>zrEW)!nZF&sPJQcT^VAJ@gyA4^zB%4#(WK<_jAgV1*5h86^d^25E|MT-FnDz0ZPFulfur@Kbg5ksK56J%Wp|-p6 zP(!51k1&r`FSO$6f1T!uz|gIyY3~g#@P{d^U*CH5{68P_AB4){<7KC;z+ujwnE&-e z&&j`kD6lwG1rEQBWBsqwkbVBlhW`p1iNN6;4%Pql2i;VE-kSeyoF6!>qLuJpr+I$s zPRgeMZQ?>R;Bb5vPssoLJ)iF07ru`F#?8Rt|NcS$Pj~3^4Ef&$=t{i)Hz2L+pNt}~ zWOG0>5RledVcpx>9zx#Mmn_uw>ve2Oq|$ZZ?kKW-zD-0KfDae4Km(l#s^y*)mSO4> zK5R%Xtpd0IeMa(&;OFUtJcbod&w=tAX)L=Yi6*p1`+fAd)DBDt&z1}F<4c=KGT!dI z`{O{^Enu?@0&>_S!WpDQ(}0K>3qT?lY~VUy+*{Z5m*g?3vNNERJzpdZ0_aPxsq*0l zE2kTNfa0tvelYY>$w5m>uLSrHpY~bD84#if$s+L2LQSvFOL~q z+UKO#j098C=u9U#!T%dv3ifQHYXv+m!+LD>M|jhTC+Uw1CLpfHsYC4HO*0^JViBlo zgQ$gU)No^YvX4aPT}Pu0!b?^STk{J&jl*Eht?R=|OblKEMZov+A{sTS<{JC-&;aHM z5eHJ}+5pa*BDF@mWtadPN3;JVbYc@w*v|i6Xn5F6KM-Mf235l5$Z6yPzVB6p>t?pbB#1y33vo(#_d`EHkG zK-81)y=NcL_>sKgMy_$$vI~@arC`8CD`KwRez*RafblM@m{HEpX9_SBH^c^8GK}B) zCssznW%Yi%pyscsUc!ZaCqNLFfjZ1bXdvD&pY5Um1cJy6AZe|7@PTb@LK%DExW;=| z__D|%py5YdP8vBB@0NHpaf7hHuHIW5P?@~nGWkBQQH;qhRxi@}!$-f%&<}G+MH37@ zFyS|o9*Hu)1qgu`4vx(xblFbr`E*cm**KuqXzV`ffQ`%+*1Bo#<42mLKen%5_~70^f6r=9slSk-W*L zcRE5Nh+muE-=+9jYrCs*t}0%pr+X+Bfg?XcF6XrdW^p!*qM@e+6 z$t*fOBK1oxL`LxtZv-wgN!!~NF&+L8R^q$9V7UN#P6NrbhdB1Vrxac3C%%df&BVCr9vw!+mRRgUvB%>Y{7iuXWe`wx0A zkkag z4SrJLCCu~E3H9FvX8UXnMF+l33)aOkBjQY;lD!C^5=3g1*iVL01~GOi*KbNMr-Mc26&FX`c^Aimr#7%fZm$F^ za9Y~udjWWqhC-?M^q z(CYXM?PWLnWYTyLf9aJvonhSv3+cE-vz=NM_GyHEoG7e*!Vn>!`i}Q8CTs%a?UZT4 zled>4>SVNd`1-Nc!kT0oI9@~Jfj`}}?E@JG^MF|N4kok;R@ne?C9nDpI@N=yF)twl_QkWp)lr&hzL4CKo>o~3uO8L!4FvZrJDpU+0u1&!lQ`lQGfK*pO`ftKrN*UR0N}#&W(Pm&-nk!(3{8Furu|k$aQWy3B zrI^@&_g$utui!TO+J$p-wb6N^IewlTVwQ2W8i>5%Udd|LdQe&L0q`8nkr2;5^Tjk@ zVw#tZp+0#xG!-03eN1!x3O;5Ha0%`-&vj5NAFI zq_ijy5@2w?0TLf8zX650dw-m%EhTkJr%2ynz-!BGs)ZBpIP@T-QgZcT7Q8bVIXzJu zrdJ9Xv5ngVg7Tsjq#PRopHQcR50F+*RbG5Kne?!saL$NfN@EGtHvHAdrRMeBt z7i0J$^`b~ARJ{b(w!`m#(uhptBl$ztTzcK4xz5)_jyes$m}^=CB3JmF z%+B=7uVVa;qdN$$z`~ffm<-AsUpz2+NF>SRcezgazh4HVp9E+%@i|@p2 z8H2;el5YQ$4z_1+L(XB(XUl;EWEt4HI=EtfDj$`#YV&q=L;?-3#4G)B=fIP3AINLp zE#n}gb=BxKIUPFkgK~F9MA6BRiZB{L!G)gWs(I9io8}re(1@Vx+d$sZU7(ME`?={6 zKQEKt#hXSMwGnY20?tG2zz5KNa;IOT7)e%j#tppoZ`o+x+|>_aecGM9GesfuSh{yi zQ&$5fU**v5VeW&=I$z4%DcxmDC7>Pfu<;RJYNMr?d}Y{JDPZmNsYW#@Su9H&ypZaP z+lvXP9km{)0ZIP=WM^-FMn>$Dfiiuc$WE7(dZ1-<9ei3KwDJ%ZcLD95_{6fE?j71l8~8N@Id}N zzit0##KPkqQh=>n%i+>QG7_$p$}8%1`7eZV^&)^*`0Af9ksTfq&_^S0W;yDBGL5F= z9e*((JMnfRY2syWx(|@#OJgGvCA~cF!BbS~upQQXI$pXY)H^_q=FCfbsFe15&?d9? z7ikm_#fst($3K=WGZozm;U^&+(2BvCe^y*86 zTynJ)L<% zGwR@zWcw!ZRSGx&+9~n>+DoW~Su0O{*UzO{mYKV_8TJPnHZGwdyXDm6|M*PI6qh~P zlj@qwHxh0AtGAs|dbDE`Ub(}>)^0uEqIscXQ3aIGJEQ5s2Eo2_O}|Iu#Gp`sntT9c z_5oUAaHNRS_ekP&U&){LNm&E|w#ldYJt3k7W1oM@q6gCHq^PCGerwKrA#_q4PU6Fc znVnnOQS1S10fhd+LEp*NtorGfCx^Z8(!46--K9@rP2!-3X+Xb*wuMipB*c@~_~{Iu z_ZwsIsn_KZoqwjA2YptBKIb=NP_0xo2>3hZQ?-+huI@{&E|gx>47hrM%dzkhZ8;b* zyIJsPTywdmQbWijqT8=>vab1vaM11!3nR?>5}5lbq>7da2>iGGO;qcIx2jAPHK&^^ zY8?DVioxXWk;6_K9Z|6zi(z=$Qlf9oJh3UOue}7Yx#OW;KK=|} z_V|x)K|Nax(w@xLin4ZcnqDKZS&4cxEZ2QgtE#%hj=exd(Uj|dNtJ?t!v4i3?S!s9 zw|F6o-)Q~P={KroNO*|=1CfSwAx_3+wa=4pC-dqK&EX${kmP-h)L-cW+HEr-R59%< zv#)2iB{KCVmotaWb8}tHmOx(}4xznkS9ms-n&3+;)emXJZ?J5|`$~m<&;jm@=E3gU zBPX!8vFC<%CM_mV@|fS*$N1n-W!kuY@{L>{?5_faQ<5gdvdoUvBG)CBUll6n0P86)Gmv}0;! zI1@7eHCT2zv9=aZ?1F+M_WL7J1fbwg-=G@qNipqlq-i^klf;mOk4cYmV^FZ$N>z(> zVtLCkObknNf4S3r!6B}hlF*@Va{#$g^sNY$gw^klp<~xBIP)MF3V-Y=apY!0_ zMa}*Sw7?kRUz+>&T2Zd|e7#p;>bgrl8IT*!W8r)wZ0qj?Soxi>YNXa)lzjaT(n;so z!{ymeJDcA#b&}3B_!tNmibgw|ux?9%jOnboF-!Zo-W6<-A3rjbMPBENkiBl6ZX@Z9 zJ5JXhS-<7K^xCv=vaVEh=@+V%&SR(UvYzzMb+lO#ZvR8c?~>jbzQz4CFnL)(@$4-7}zN0J#Zpg^YMeiWCst^aXxx~X67YEA7Y1TpaQnpOp zG$wiLZ`U1MQXs+J6D_b!YkuQw)V7k|Z=&4ahNdU(ONPcX_NM5j$1SDJ38}sdBYx{e zg9D0H<`@Qa-?f_vUn#boqZ@%PV`BW~BM!5*3jU-lYK_OVgFr}mtRi>h8Pf$Ze<%;5 zA`UOT*QF-p&DU&{l0Q^Np{cEHmSYml#Ld|EK3yu{%T11Vg%_eZ($-$>*`$kc zh-ZIDMJm(p-W?Vdn`(ryIhVJxw=)i#?u4}1w>~yJB<&xI;uo{%p*|4@f}4$NF~Uxy zJksc5SC+(Y-0zh#tT*zw*faG&1i8b|o-a(^v4egy35EKsLErQVMjny1oz z(`;jE(&J_IXdTOjuOruY(~+)E1IzqOHR9a~s8VZIrA8A-Ya(!>k~^sVW*Mwa31o1v zau0%)I>kory-nzYCgOE*Q1Xo#z^xnQ;p7zryBXrn{a% zPOBZ^Y5g@y{=`CspAu>Fr&tDy3SacCnl{_hjQVCa7aV@?)T$cfkZeu(IFvwbC3yYXsg6tX=m@uIWtmNP;3#s1{6IDqA?Nky52Kp9pW5do}mp zV;`vyTRgYv>ESjzQSnnmOI?SPZ=#=%LKZDz(4NM&4B7){Bqg@#vT8hhS3_zSxQ}9% z@Agid_%UO(LC+Sqv51`lh8u$;wZ==od%E-DCqLV}W>kIf+fZp(?F!RHF6e*jyV{w( z#!s3Aa!{1N+j79OK;f+fdSHE|HAgH47H7Jn%P3UZ>OQ`rgtGgt zUYb=TMg11E zkDz6dkQB-@`-S4{9*L_L-BFCV{6S95R*{bx*6|gdz`e=S@+uyCNy|RU1f6yV>YpU) zNVSV!P=`3M*`&f?ihE}5a*a{{L~+r1H3<<4xXKeJYxSS#)sZa1y1+Pk!TI5Eo| z{w#j=Di+++W&0-7#EaB1ulOy2UwVpIUra7Ct$nQ z<@_1~)vZy*0uHy=ek7>3?C$N()_y*qKZ8O}V5@=MQi{nJI&TO2qwG!(Y5lnLKxyvb z=SknWft<&T*%k2m%mkvpTyfHKL(WN?lHd5M28nKbPjBrsnTPT}6V-CP+B-}{q^)O+ z5xzjuAB%<4n!gD;%yZ}CZO-J{P?A5t+ho?3R8e-&Qv;U~2PS$wDmc5aE^v2KUVzf%M4X;NWCV&Cvtv*Mo-A3U1p`D6yt( z9%>^cA08Cw;f_53|M8?2Qiz~?(ickaz*#4_STN_q zlVNjil;ny*ZyD2U@>2R~yDY)KzB$S*-c(!z`y|+;lvWUEu=i|@Vrg%A>@I!kIT__GxX zvU3EQaSU5BpFtxrCl8*LT!+`%q*G9za4N}~oTi=&#DuvO9}pLxY(4b{ruNkO zPHjJHu-}E-S~70`dsNmyL^CPH$==G}S}?i&e5zSyY1RBFVGZX94sEsUBSjpR7k6FB zcRKY**z7JBD1a;~qEkqw+%70%z?)cY_!0Gk?Qrge)oi`t`h=sz>#F4V8ay!co(CleXg@Bi;S+Wj+hqlOOlk z3rTXl(%fh47lGPwSkNYOk3Ob8DfvlTMBRIW-lgJ?*lz6XM#L!pP?z%HbZUy z>yFvED_8T6RPh2yxx>(EAJ>H9n@J_e_Y`?DYxVFl73=h8M3cebY%HbbaLu&C9G08T z@FlFG=Bw8(xVO8Ms?@%*$|vF9FB0V)qSe}>d1KSRiDy($l&sT0<*ET_0iUdavoM!x zwzv5lZtq>^8jxX?VOzOXUc}QLfrV;xbgM9}T*<^%U%3MA&u%5A5A;@~Vt0L1 z-T$m%3EtCN;;K^3ubCG}esPbkUlK5nEUA_YM{S--b~nmmTkp>Z0$b1tajMN9MC=8( zNsc;uUOf~8VFH%&)@-`#rO{x}dIaa;&c&+XHPs8Mtyv!@=LA=hlakvIZ=rt52_o8a ztosC9*V1pwi@cnT%PBoBb6@8j$DjRICY&$LwiUzgBEvDR=?^o(qzf-BvVEYVrD|?% zk3&4#ok%qz^GKr$(tH?42Z;A_(I*<=OOxy}u4O>wzLg&Z(BXYJ%oMhh^wU$ZBXBw*){h$MI~ z&S}f5;A4chpS9a6GA@tbdB&+x!Ftc>NfW<8Qic1Dlqoc~R>omTwUjdw8kSvIS?!@V zoeis#zWVpg5p3Da9CTSYbhr$y$#^Z~G@F;YfZ{E7juJnS{%)g{T6T^r2CFJ$EBb)< z@ZEmyucD47Rzgc0Zk*(&%fbc;tnF4nsM&93Ve2U8)!HcjrVL(2v+aXvOj{nB2Uc2I zReb7h;GRC!6fTY}5mR;eUW*?!zO+|A?y@fwJR$MaPSQZ{jf$T4j~bI@pYBUG(c8-N zlF}nz7ktDr^9*Qe@2;0h8(%b=J>P!0LY#O;$K+Svl8Y?;ll3^^E+-M6uQ`r@GX1S& zC|kkqj?%CFR2sAphJRh|JWObdo^trafdG%jOO2 z1S0b|hHSX4l6Wr&d;C}~v{wQYY*Ex1h1>P9y`XRl#xA0+AD6~^M>OYZV-m}9KquJ| z&z_{fJzcsHnegJ3CV|Cz5>myk5BcKzi=!@r&yVe#FwRub~~Hd=Ke~jFPj}+0(>cCVg1781ITUWAuWPXVmU4EbV1$ z(5Fhey*H_CmE;}RW1C?Vg@T_LGCha)lfL*I%|{Q$kosKq{4>b^EVygupmxKz5)hP{-FeSh+|)pj18 zR^DB!cD}95$j459r0A^6t)yYES$62&ud>gdMyk);*~KV{*rOLhGp}}^`Wg1NRtdd& z$Y3b@0~ecl{?y&|?P8>V8oG90`Q$(czv}iL#M~ENLazCwfEHvS!Y&bc=LR?1(f7y* z$0Yd0`XI=bwSeZys#$m5ZP(e~>VJ`w#xtL>ts&?8477@ZC)W;-o3AeZ97AE;8%ZZx zPqA`RbiPZMspz*Ey0cb>hCJ!+vq3vmF6qzILphDcut204gXr`-a@F@!39c8u5oh8I zuAp0%=_FPv%Sd{duTt`^UjftPo#R(C)FmpWs3=b(=)x&3BxsOfSKjqh6yH>&?sTJ2 zLV9_``;Au=>O{$V>9R{GTnn-@vU{0UYAXd2eZ(lY@YGB4c1q&cbz2>r$tQn#`}HZ& z{+sKSpa}Rihs(7c&qR>1tdG|~Ykak?|G+s`dJgbb-56cNhh6?0RGT` z(kHdCpzuc>(X!wB%}&2ppn$PK*rAy%7IkT1Ou-J_9W8{(As-wgw#3$1cpm7l-^?>q zq-3A<8{=HxS4t%O#r;tsX$aIQSANoB4C-UyhhL}x^*)?IRh z?o35gEb>zQUH6&XS)fzAE% z&)wE)SltqfcDMzh~SR?F_u=Ag3 zt4YoZbbT(njyW<3@f{=fwZ21~)^Xpe-X?m?L*>@(8sri$2fW#GCKEMFRZNX(fy(`& zjrKB6Mn&-;-R0d(c(@&#&xV-w@EXW!)B>w0NwZbq_T$&hvn+VjU{L0bttj+`Mu!&r z4A&(4qzTBlulMEfVKUm|d*o|iyIfOmG4@*gL_}y^hkF0LO?hwbG_E7#DEh_AK`}<4 zF-{S)DhSAh&DNaz>$nvtr!)gZ0~p?SU{LnYP{;`aMsykHC+TZ-G(#-|-m#cAY^rb< z9g9f5YKMu}!cHt7siw0@-2rjJu|qSBQIOMVLIO4}V*RFpe6P#$tR_l8Ztm}5J)q^c zYjHULSf?npp)s8Eo5hPloYCr6tTwKip*7-f-D$d&D%?x z4B!2fo$J?Kc?B;pVObCe&S^OUI?hFA=U-Ao_T6bdn=T{H2G6!4NPju~InTQhLB(Jq zwo2WPH{!XJno{HY`$W@#XRv8zb?OAHA_*~6Y&%I2Z{ClX9}C$LMyoqLYxbLU^e*~H97{FBIw(J*G<>&U zQ0X+)eE{1UMYf1&4p(lQl0AMa%ageX?k^Fwg8u=jd;O4=)t7vYU5JNcT7w}9l zPPP7=M)PJp0ExP^FQKvziyYc-iG8!!&v==@#_udh9V0oh^N?Wc)@MUk(#jA0a}MWg z8VkZN4RB~~=?lBeoMdN0TkZ36i4u>$5=f=*ig3X=Abe>^8^n) z)Qkh;xAp$N#_wQF^7fwL#nAEd0`S1f=KXT{o-Y51!*EqYYb-SC;pAoal{4194ew#tr zMntB3kHpN$@|6vAH%I??qVGh_>}DiWg5M9vu)F5D6vAvWjeb ze4k;FtAx)*uKZogC*{G;dkS!?E~mv#@RK$wACnEsV({zg_HV8UEb!7@xG=z zKM^vI^hHPPv-{%mdxB$E0&N%Z+Qm+y3Cr$puO?&qE$4jlcA-f&wFl!(11}q`*YrK7 zrVnokf$qc~@}I^JX1OBXiDfU5;wSy43nX#OD+t9?EBeR2xA*ocHlzmlJ=Aa}Scvt&;(0C+E)zWjq^>XM&sy3<_4R=o-O zBIga&T`iSk)hn2P`gn0ny@9v0|2F61*b^A-9h5rV=*{G%)>O{sF8H@;-GuiVpTs8A zJXVN#N`_4?8YqKyGzEEYPxjCjSD7pbLrRm^Xf+*Po;`WPbOZmW#lgo?GU9`^V0cc7 z_WNIhjN7lVgB(Gpy74W0Nu6Br!uG)DGbrY3|YlN7#)IRr(CYGU%rf(la zL))tiClqr>m-bM_k1S0S2SvJwN$~95ga}oxBCwqnd{p`BV){)p=?mNKc5+Ps^t;d}IEZ3c5xB5`hulG4_?& zj^A-(8<82VRpiA%BMPSYt>UIz_b47tjKl|RyRMH2cpq(k0ODg-t;Nm(;D-dDKb6|2xb`W)fXD%UJyC~Ob32h( zTACh`F;>kmN?zdVIZX>!06M1aB4CLv=npn!9L=)2u-Tfmqd{ezW9t4*58s!>PbxLn z{<-#Fd8S*vVXwtSKE=BjwQ>@u1d+ZLcccED^{-n+E&dO)yx%~`J`q=BQ2wNZI~i!| z`xGCta9h$+tvuzKwaS$1sCt`r#(vz36zpn$7#hH&i~rD4_TYl#&Y4SyQUokscme_| z!(Oe9;yupil?sun6VKz-rfY>uoKIs`>B2HQ*aSx1C~2&B!eK+w3zmWKWi#OZQKJL3FRR zvt=pyOvH;A7K1*T+wbC=Al`4)X_{=>b}PRWzt$K+&aOBCFl6YUa0hyzP4aNv9qT{Q z;0akMqP}qlF7<4GYZ2(Kyie=LOYe4yhtN#TJgPjFEF~A0yJM{wW)ICRdTY`xw?^xL zdv7_b;5jlOHvKMk7ra|ji;URxMEF6@qDS-DeA!WqXSXRsLV>d8vznfbMa3_mEFi`c&m&QFuKS_N#9+NZ` zK1u6B0n-~5KD^}Q$IIq=KaNW9O_--Ap8hPADv2M}l#>aa|M`46N|mqvsn2mEG9oDW z>zOjwM|{4_H||o{vr|sm>O;sz?Y{@)0iL}t4bfMQ8ceKR>GGnd$wI*Zkb@L@ zJX*FkcJ&{oXb9>DFyid)mNdRUM_&;%_6=*z0?Yd#7*1nxsAZWTtCwf`M8k2(X=)p= z<}ofDxa{NCG4Ji(WZ+x2T;O~dEtegOK5i*ade?w+~7x@N}6 zb1ZnOx4d?)y-+c%M>I<~(Y_Lx-jVKXj{&lC6I+W6E5B)+d|WApJn{Fj4W}Zrjuh?a zVsiWd`zx;o*;?Hz`)H;95TSY#6pqtkSFH9-{$i~%;F7M9D$090Dfe%4!*3#B;%XQR z=p|r?kR)Q4RKi2XZ5mQr6iV$t0C|P@M@t1|DQl2;GE)uSxa^6C7d^e<03~xraz(VE z1KLj2x@b7)rAKerl9CcJ@Ha7b3V0UllwxP<`s^PA9Z==V*;?RFiBg#;`efT5jr@5JLw}q7&Ii za{L}pq%{MP^{SZ3%6B^Z0puD6FU3zp|6UBOe-mR-0X%5BeawetlOXR75(#v&moh_t zx|EpTS*V66)CZf2lpd?^WV$0^N9sD1+-VzW|D(+O9-CWWnb_ijvbh0$_>%d0RQ?A% zKT`yjO04ma%5auynIU?!l;;REVm5A#%{cfqbxBqY5OrpjBd}jMAfZS=xB`XYPXI4UH!#&iV9M{-LK`S<*>iV-8^udZ(g#1 zt6uU_jeQMYkCdb>th;mgx(zJ@MigenFbrhlGm#1&ZnN@3UvJI{IfV>}WkpuMTz8e1 zw4rBlO51t$}jqN$_Je(-{aGKZ3!-ghFq1Qqvxl zlv(l0^IS-TB&hDE6@~#*MV8tg>+akncb2{yqJaTkWs4$piQ}cV>k831Efck$rRDIz z?vk+P`8JTnvIuyh(P#lBP;zMZjQG^F7tc;eq|E4;<2|~i>ws8;%W;;lLC#ygi^`9v zVo2jBJ;QgGM+s{(5xh7zMxIjU-CSWdTR&*`9J#>UazI{UdlYHazP@}MW2V^V*ptDp z&z0s$CJ?IH*wT{ewM&v)CbGADdj#Ljq3hb!y0Sgj(H^!yzln?9bx8g^d>eQDT$ zp+EydauAR=RwDiL>OYGm7W_NC;C}DD^i0Wnd07KoCw!RQGYug>Qh-nsS7`;i*yoK9 z-NCj^c9Bp2SJ3N#$TGKeluIBgZ6$JW1M2Pl;9%m0eB_f$i) zoz%frWaZ|0i3g1y2GAc`-{E*GSF?j1dw+3C0?puf!i&7Ke~uF`0`W%GsAE;q^I9i_ zxVW23ud3!``SX8Qz;NycdQ_{KPAON?i$Tp<4&6fUexU&a`^W7WC4Nhvv%M7+nzJKg zwp-f%q77ZbGP#y99&)s5&;dZv5#Q(?lbujx{`J@C%q@cws&_CfQWGjeYP z=h(LplLNV%)be1sDgnB5+c>j{S$k-`$xm4ql)Dy8GUNGj7yjsztn`$vI;Jnm)h;Sy z9!)>m@q8#zs#04c`Hm@Et_`C|V$r3lC@u)PZSiL;Hf0TFoZmlnyo3 zCU&@_2ZSouF0J|kn<0Z|cuZbfG|@Qpfv!7l-=bD-$)mRKGib?(RUsx*CGME|5>v6{ zZY@Q>t;4SrxVdf9d-wi|@RVo9HSrq!DfGJ29!%7zL%rp*h2~wa(5|M28nSQd8$F*y z>XU@h&9#ppoH1pz438%{hftv50Z9c~YCXPw(w%D42{C2m?7gs=$|Y#FMxV_18^>aC zvFhiI&-EzDtqRFh$5#=Iv1_OphMU9kliMqn$Wfqxyc>+y6nBpb^0noLjgO&S}n8* z@n9}n4Ja^+24g1&qm*%q6?San3=0uU(T%Je$KrMO1!0sug;|Bts3r6&O2~f zhYP){2}pJm@_8=pgeF2@0tV6HJUtpyW6hMTJ*>~vH>LJ6_P42LM3kj|%h-KyQm(I( z*B6DyicGw18j68-?<8S0xsSHqLXF-lK}koBFUY1ImZdZkiCED! zr)Ql>nKof|4%I$9yr1z*O;n#>$!vR3cuQK9hlg^lyrP-|_fwU?nDDmrON6|BwqU>2 zIQjP4Or;L(ccvUV*}71h2ae1$P_lV1i?i5IlQg&(dnsHOX z$@;MmMOZ~MVL(iqP)GfwqMK$%f|B@ZMVIQY{f2aoB_EV<2{EY^)d$KwT?0k-OjFx-vcFHUcuD14-^gQW1x48OnDcT}sCXb}wmzB=Ucu0qp=267HMQch0p|%F6xO)6rsZl=_dS?>U zP;UgL2yVTgy@lR-D9m3`)1w(WkWrx2Dsd3(%DafqA!a9i*5doV7bvliV(F#z?jJdD z#|(({XcjDu5;KRRa+gOtZjV4X9bIT;IkI&gUfLMWKR@Z%SYx@?2>02=LkAZCi zq%!IOIj3mY^Eeje<4f5=~o=y>(lstopLLwpL8>UvJqyjKutI{`Ua%Llt$T%^& z@)v~KotO_H7 zCMUhnWimSp&BXnwO*P^e@pqcFsk;-u4k}Q&g2sFkz_ad1%aIjR>9#eh{wXgVi#A$B z1bE(AJlaF`U3_Q^R&z7NxtAR9&qE47GfH30vPXuwK6EK1FS-w)oBIg`BKv=4kAjZBe&#H}!zWu= zLsv5nF{w5vm^>FOakm_8rWb!c5Q^`hGb z$)Weuz|D2K+oAMJ{-}t+rVZj+H%CJ60zy0k+^c5WHZ&165z_kPBt~CJY2{$TEu=K- zEqx_wd?tYxGz3`H?)FrLxlB}H;xB_e#g5Se^=hR_ycWg6?Pz`-=|(9+TCG>`Kj`i* zBO(UjG*kS~7}Vu2%V}=cIDYAw9;>)pvWdLK3BHgh526Q=rb*-_9*6w} zEK?r(R4`fc&H6ani*S5ytT&d&$4H*qD*Vl0O9+_pfy6`CB*8^Gr4cnvQ6=o<1Nh(f9*3|w^5xdRJ}TS%k9_U z&+?hBblNtlFXZELZ2=VaP-mL{KF#iHp#><7O*Q`2ygi#Zeyeqrmp1h)6l-JjC1%TM z`w#YRAA6vmObskt?0#wELM^RCeJnf3OmFvo>FcUvaX9R++B^fE$F%YM>Vrk>Zlm|0 zZ56bb^X|W9*)T@w8I@)=zkgtG4tavFvkjf`}_$z4or;F0SBhK{q(bH&^g?N!Ba=U4>VnyGbZ!H z<|{O;LzT`!GZ6YmgLe7_XwMFP?icf}=G~3f8}o8A2rnfU$DLw_7B73@z(~-8nB( zD!}&F2W1^lhSVYKPh;mrY6OU-zA_Xx2acZy%1-|UumIkp#4P4#qs^L(Upp?4%S-MRpi%r$xd#^kM_pPh=c(ldd?tVRMPCcVkCkWo zZ&VAZ_jF*v!ryz%DGeYC&n7KgWg}N7%W|KSbo4#+{$vQem^Y$i$3`;s-_khQ)xP+dJgOQN9I1rt&q-L?%??ZdA3pk4=`kG>k5u<`S~5M#${M7JvD0I!x;}N znnWg2K2ya2i(Co)9zJc<@LJla9m0k+F<<&8olhc2Q~69*`WtGZw7 zc^g}*x4s4cS$;3(AwPAn*hocb8hTUH%p0=3s}=RK&lLaq&C?C&mKN9>n}DLSPfn@+ zl?G*iC{0?+Jw9dD5^J!z7ZO1hXeXFR3`3br`pbMkp52LD62Q{9qsxiC!Pl2)|4`#U zh)fd6`&i8U3L|TmH{T&rHIBZC@p(@B(?3$&cv%|GJaO(Zj0O39Vc~9S`N45o>?^SS zsZJqW2ysMWahOJ;3jVO$Z!7)P_OBT3sMLldRgu|>FK{`r=(wMNAGnh9RbA_^TyYRJ zAGlyz$n3=iH*@?n++rzE`p|8M+1?e`3cAE~f$)SwZILy9D?O<);M=Bg_iEcL1N>53Ye4;UJ6! zgicX&Sio1_hVGm#@c)Vc`~OM=iqxIAs<*caSL%z#HFr62HO>6!e^i@qW{=Hy zhwv;19@&YBQ61?)qeTIARx~Yyl*5}Yb$y@->21I*dTTwmECiXhc|t>wUgI?S(^t^f zHr=(;eZn{(1Ehead-@4fNxgSvpn$d|dP()O)SO^`$s zF-0cf&vV6U`V)D^v#yecd_Jl6*#41fVCIxB>T6p$TVyuQi+z0%$`RV zKf%B!(*AZ_>Y2y8!^{flg~bDp+>e|EJMb({^1SJx39GfrzYRC!W7ozqNXAc6-BHOD z&a(RZnPKCZ3Hf9}FQ*-96%m26=5WOs&VyOxn!mwGliRoAqu)D=!2$1h7#e+M;cZBN zvMvtArivh1i;PJ~t+#2S-*3P1241-3bjAHv4_QWLalc;r@%=B4m)3snkfbQwNcZWE zlt+e1gcfS*yZB}Q{CtxM{gs32&%u7jrp@-lvC3S1{puWj=2dYT9h#kQYrekGmQ3S( z8K0GsFx%9hND^a+)%lr`^9SDTlnXz1Gk8&9Imat2iy^al6<+o8zfs3bx_i5gaEZLE zqZaCZ{DO9w8OKP+YIOM3I>kZe$FT=}(Tl-HydMH}62o59m2?MQ5=a559~PxV;)a2R z^%&c4FsMsJHqRbvmJ9E|K#eoCbqGnLltwI8Ru{=Vh?B)

                                      9}7pAG4CND^Nnez?Jw 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�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 **Note:** When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you lose your codes for GitLab.com, we can't disable or recover them. +In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as +the second factor of authentication. Once enabled, in addition to supplying your username and +password to login, you'll be prompted to activate your U2F device (usually by pressing +a button on it), and it will perform secure authentication on your behalf. + +> **Note:** Support for U2F devices was added in version 8.8 + +The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend +that you set up both methods of two-factor authentication, so you can still access your account +from other browsers. + +> **Note:** GitLab officially only supports [Yubikey] U2F devices. + ## Enabling 2FA +### Enable 2FA via mobile application + **In GitLab:** 1. Log in to your GitLab account. @@ -38,9 +53,26 @@ lose your codes for GitLab.com, we can't disable or recover them. 1. Click **Submit**. If the pin you entered was correct, you'll see a message indicating that -Two-factor Authentication has been enabled, and you'll be presented with a list +Two-Factor Authentication has been enabled, and you'll be presented with a list of recovery codes. +### Enable 2FA via U2F device + +**In GitLab:** + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Enable Two-Factor Authentication**. +1. Plug in your U2F device. +1. Click on **Setup New U2F Device**. +1. A light will start blinking on your device. Activate it by pressing its button. + +You will see a message indicating that your device was successfully set up. +Click on **Register U2F Device** to complete the process. + +![Two-Factor U2F Setup](2fa_u2f_register.png) + ## Recovery Codes Should you ever lose access to your phone, you can use one of the ten provided @@ -51,21 +83,39 @@ account. If you lose the recovery codes or just want to generate new ones, you can do so from the **Profile Settings** > **Account** page where you first enabled 2FA. +> **Note:** Recovery codes are not generated for U2F devices. + ## Logging in with 2FA Enabled Logging in with 2FA enabled is only slightly different than a normal login. Enter your username and password credentials as you normally would, and you'll -be presented with a second prompt for an authentication code. Enter the pin from -your phone's application or a recovery code to log in. +be presented with a second prompt, depending on which type of 2FA you've enabled. -![Two-factor authentication on sign in](2fa_auth.png) +### Log in via mobile application + +Enter the pin from your phone's application or a recovery code to log in. + +![Two-Factor Authentication on sign in via OTP](2fa_auth.png) + +### Log in via U2F device + +1. Click **Login via U2F Device** +1. A light will start blinking on your device. Activate it by pressing its button. + +You will see a message indicating that your device responded to the authentication request. +Click on **Authenticate via U2F Device** to complete the process. + +![Two-Factor Authentication on sign in via U2F device](2fa_u2f_authenticate.png) ## Disabling 2FA 1. Log in to your GitLab account. 1. Go to your **Profile Settings**. 1. Go to **Account**. -1. Click **Disable Two-factor Authentication**. +1. Click **Disable**, under **Two-Factor Authentication**. + +This will clear all your two-factor authentication registrations, including mobile +applications and U2F devices. ## Note to GitLab administrators @@ -74,3 +124,4 @@ You need to take special care to that 2FA keeps working after [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en [FreeOTP]: https://fedorahosted.org/freeotp/ +[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ From cdf7a6c2ded729174a5e099b2fc255ee61f0cc79 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 6 Jun 2016 10:25:07 +0530 Subject: [PATCH 256/507] Add the U2F feature to the CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index ecce18af06..72fe32a01a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 8.9.0 (unreleased) - Fix issues filter when ordering by milestone - Todos will display target state if issuable target is 'Closed' or 'Merged' - Fix bug when sorting issues by milestone due date and filtering by two or more labels + - Add support for using Yubikeys (U2F) for two-factor authentication - Link to blank group icon doesn't throw a 404 anymore - Remove 'main language' feature - Pipelines can be canceled only when there are running builds From f5f65d69138f6c348a69a486f38ef5dc693fe7d2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Jun 2016 10:55:13 +0200 Subject: [PATCH 257/507] Refactor all testing suites --- .gitlab-ci.yml | 208 ++++++++++++++++--------------------------------- 1 file changed, 68 insertions(+), 140 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85bf783ace..d8d8557a46 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,16 +32,7 @@ stages: - post-test - notifications -precompile: - stage: prepare - services: [] - variables: - USE_DB: "false" - script: - - bundle exec rake assets:precompile - artifacts: - paths: - - public/assets/ +# Prepare and merge knapsack tests .knapsack_state: &knapsack_state services: [] @@ -56,20 +47,6 @@ precompile: paths: - knapsack/ -.knapsack: &knapsack - stage: test - script: - - JOB_NAME=( $CI_BUILD_NAME ) - - export CI_NODE_INDEX=${JOB_NAME[1]} - - export CI_NODE_TOTAL=${JOB_NAME[2]} - - export KNAPSACK_REPORT_PATH=knapsack/${JOB_NAME}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - - export KNAPSACK_GENERATE_REPORT=true - - cp knapsack/${JOB_NAME}_report.json ${KNAPSACK_REPORT_PATH} - - knapsack ${JOB_NAME[0]} - artifacts: - paths: - - knapsack/ - knapsack: <<: *knapsack_state stage: prepare @@ -86,10 +63,22 @@ update-knapsack: - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json - rm -f knapsack/*_node_*.json -.exec: &exec +# Execute all testing suites + +.knapsack: &knapsack stage: test script: - - bundle exec $CI_BUILD_NAME + - bundle exec rake assets:precompile 2>/dev/null + - JOB_NAME=( $CI_BUILD_NAME ) + - export CI_NODE_INDEX=${JOB_NAME[1]} + - export CI_NODE_TOTAL=${JOB_NAME[2]} + - export KNAPSACK_REPORT_PATH=knapsack/${JOB_NAME}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_GENERATE_REPORT=true + - cp knapsack/${JOB_NAME}_report.json ${KNAPSACK_REPORT_PATH} + - knapsack ${JOB_NAME[0]} + artifacts: + paths: + - knapsack/ rspec 0 20: *knapsack rspec 1 20: *knapsack @@ -123,6 +112,57 @@ spinach 7 10: *knapsack spinach 8 10: *knapsack spinach 9 10: *knapsack +# Execute all testing suites against Ruby 2.2 + +.knapsack-ruby22: &knapsack-ruby22 + <<: *knapsack + image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.2 + only: + - master + cache: + key: "ruby22" + paths: + - vendor + +rspec 0 20 ruby22: *knapsack-ruby22 +rspec 1 20 ruby22: *knapsack-ruby22 +rspec 2 20 ruby22: *knapsack-ruby22 +rspec 3 20 ruby22: *knapsack-ruby22 +rspec 4 20 ruby22: *knapsack-ruby22 +rspec 5 20 ruby22: *knapsack-ruby22 +rspec 6 20 ruby22: *knapsack-ruby22 +rspec 7 20 ruby22: *knapsack-ruby22 +rspec 8 20 ruby22: *knapsack-ruby22 +rspec 9 20 ruby22: *knapsack-ruby22 +rspec 10 20 ruby22: *knapsack-ruby22 +rspec 11 20 ruby22: *knapsack-ruby22 +rspec 12 20 ruby22: *knapsack-ruby22 +rspec 13 20 ruby22: *knapsack-ruby22 +rspec 14 20 ruby22: *knapsack-ruby22 +rspec 15 20 ruby22: *knapsack-ruby22 +rspec 16 20 ruby22: *knapsack-ruby22 +rspec 17 20 ruby22: *knapsack-ruby22 +rspec 18 20 ruby22: *knapsack-ruby22 +rspec 19 20 ruby22: *knapsack-ruby22 + +spinach 0 10 ruby22: *knapsack-ruby22 +spinach 1 10 ruby22: *knapsack-ruby22 +spinach 2 10 ruby22: *knapsack-ruby22 +spinach 3 10 ruby22: *knapsack-ruby22 +spinach 4 10 ruby22: *knapsack-ruby22 +spinach 5 10 ruby22: *knapsack-ruby22 +spinach 6 10 ruby22: *knapsack-ruby22 +spinach 7 10 ruby22: *knapsack-ruby22 +spinach 8 10 ruby22: *knapsack-ruby22 +spinach 9 10 ruby22: *knapsack-ruby22 + +# Other generic tests + +.exec: &exec + stage: test + script: + - bundle exec $CI_BUILD_NAME + teaspoon: *exec rubocop: *exec rake scss_lint: *exec @@ -138,122 +178,10 @@ bundler:audit: script: - "bundle exec bundle-audit check --update --ignore OSVDB-115941" -# Ruby 2.2 jobs - -spec:feature:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - bundle exec rake assets:precompile 2>/dev/null - - bundle exec rake spec:feature - cache: - key: "ruby22" - paths: - - vendor - -spec:api:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - bundle exec rake spec:api - cache: - key: "ruby22" - paths: - - vendor - -spec:models:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - bundle exec rake spec:models - cache: - key: "ruby22" - paths: - - vendor - -spec:lib:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - bundle exec rake spec:lib - cache: - key: "ruby22" - paths: - - vendor - -spec:services:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - bundle exec rake spec:services - cache: - key: "ruby22" - paths: - - vendor - -spec:other:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - bundle exec rake spec:other - cache: - key: "ruby22" - paths: - - vendor - -spinach:project:half:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - bundle exec rake assets:precompile 2>/dev/null - - bundle exec rake spinach:project:half - cache: - key: "ruby22" - paths: - - vendor - -spinach:project:rest:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - bundle exec rake assets:precompile 2>/dev/null - - bundle exec rake spinach:project:rest - cache: - key: "ruby22" - paths: - - vendor - -spinach:other:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - bundle exec rake assets:precompile 2>/dev/null - - bundle exec rake spinach:other - cache: - key: "ruby22" - paths: - - vendor +# Notify slack in the end notify:slack: - stage: notifications + stage: post-test script: - ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See " when: on_failure From 2f9c2149a38e8a4067cb50c1cd1bbb1e72c263b3 Mon Sep 17 00:00:00 2001 From: ZJ van de Weg Date: Wed, 25 May 2016 21:07:36 +0200 Subject: [PATCH 258/507] Backend awardables on comments --- app/assets/javascripts/notes.js.coffee | 2 +- .../concerns/toggle_award_emoji.rb | 11 +++++- app/controllers/projects/notes_controller.rb | 3 ++ app/models/note.rb | 1 + .../projects/notes_controller_spec.rb | 36 +++++++++++++++++++ spec/models/note_spec.rb | 10 ++++++ 6 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 spec/controllers/projects/notes_controller_spec.rb diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 7c3d57fc19..101154114b 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -162,7 +162,7 @@ class @Notes renderNote: (note) -> unless note.valid if note.award - flash = new Flash('You have already used this award emoji!', 'alert') + flash = new Flash('You have already awarded this emoji!', 'alert') flash.pinTo('.header-content') return diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 09ff44f291..036777c80c 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -9,13 +9,22 @@ module ToggleAwardEmoji name = params.require(:name) awardable.toggle_award_emoji(name, current_user) - TodoService.new.new_award_emoji(awardable, current_user) + TodoService.new.new_award_emoji(to_todoable(awardable), current_user) render json: { ok: true } end private + def to_todoable(awardable) + case awardable + when Note + awardable.noteable + else + awardable + end + end + def awardable raise NotImplementedError end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index c205474e99..836f79ff08 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -1,4 +1,6 @@ class Projects::NotesController < Projects::ApplicationController + include ToggleAwardEmoji + # Authorize before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] @@ -61,6 +63,7 @@ class Projects::NotesController < Projects::ApplicationController def note @note ||= @project.notes.find(params[:id]) end + alias_method :awardable, :note def note_to_html(note) render_to_string( diff --git a/app/models/note.rb b/app/models/note.rb index 46c3f6e24a..585d8c4ad8 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -3,6 +3,7 @@ class Note < ActiveRecord::Base include Gitlab::CurrentSettings include Participable include Mentionable + include Awardable default_value_for :system, false diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb new file mode 100644 index 0000000000..cb7d04a059 --- /dev/null +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -0,0 +1,36 @@ +require('spec_helper') + +describe Projects::NotesController do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let(:note) { create(:note, noteable: issue, project: project) } + + describe 'POST #toggle_award_emoji' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it "toggles the award emoji" do + expect do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: note.id, name: "thumbsup") + end.to change { AwardEmoji.count }.by(1) + + expect(response.status).to eq(200) + end + + it "removes the already let award emoji" do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: note.id, name: "thumbsup") + + expect do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: note.id, name: "thumbsup") + end.to change { AwardEmoji.count }.by(-1) + + expect(response.status).to eq(200) + end + end +end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 139f7cb978..f15e96714b 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -9,6 +9,16 @@ describe Note, models: true do it { is_expected.to have_many(:todos).dependent(:destroy) } end + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Participable) } + it { is_expected.to include_module(Mentionable) } + it { is_expected.to include_module(Awardable) } + + it { is_expected.to include_module(Gitlab::CurrentSettings) } + end + describe 'validation' do it { is_expected.to validate_presence_of(:note) } it { is_expected.to validate_presence_of(:project) } From eaff5afc9b50f41aceae32282e92f6450062a074 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 26 May 2016 12:15:20 +0300 Subject: [PATCH 259/507] Show emoji menu in notes. --- app/assets/javascripts/awards_handler.coffee | 9 ++++++--- app/assets/stylesheets/pages/notes.scss | 16 ++++++++++++++++ app/views/projects/notes/_note.html.haml | 3 +++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 766c653111..e211c86f42 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -51,7 +51,7 @@ class @AwardsHandler $('#emoji_search').focus() else $addBtn.addClass 'is-loading is-active' - url = $addBtn.data 'award-menu-url' + url = @getAwardMenuUrl() @createEmojiMenu url, => $addBtn.removeClass 'is-loading' @@ -74,6 +74,7 @@ class @AwardsHandler positionMenu: ($menu, $addBtn) -> + position = $addBtn.data('position') # The menu could potentially be off-screen or in a hidden overflow element @@ -240,8 +241,10 @@ class @AwardsHandler return @createEmoji_ emoji if $('.emoji-menu').length - awardMenuUrl = gl.awardMenuUrl or '/emojis' - @createEmojiMenu awardMenuUrl, => @createEmoji emoji + @createEmojiMenu @getAwardMenuUrl(), => @createEmoji emoji + + + getAwardMenuUrl: -> return gl.awardMenuUrl or '/emojis' resolveNameToCssClass: (emoji) -> diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index a3e1ac13a4..3acc9152c0 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -120,6 +120,22 @@ ul.notes { padding-bottom: 3px; } + .note-emoji-button { + .fa-spinner { + display: none; + } + + &.is-loading { + .fa-smile-o { + display: none; + } + + .fa-spinner { + display: inline-block; + } + } + } + } } diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index f1045bbd8c..c176778c51 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -22,6 +22,9 @@ %span.note-role = access - if note_editable + = link_to '#', title: 'Add Reaction', class: 'note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do + = icon('spinner spin') + = icon('smile-o') = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do From f261be0949bb589b54359b8fba2bcee9872e69cf Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 27 May 2016 18:29:27 +0300 Subject: [PATCH 260/507] Award emoji implementation for notes. --- app/assets/javascripts/awards_handler.coffee | 151 ++++++++++-------- app/views/award_emoji/_awards_block.html.haml | 2 +- app/views/projects/notes/_note.html.haml | 4 +- 3 files changed, 92 insertions(+), 65 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index e211c86f42..071dbd3664 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -6,35 +6,43 @@ class @AwardsHandler $(document) .off 'click', '.js-add-award' - .on 'click', '.js-add-award', (event) => - event.stopPropagation() - event.preventDefault() + .on 'click', '.js-add-award', (e) => + e.stopPropagation() + e.preventDefault() - @showEmojiMenu $(event.currentTarget) + @showEmojiMenu $(e.currentTarget) - $('html').on 'click', (event) -> - unless $(event.target).closest('.emoji-menu').length + $('html').on 'click', (e) -> + $target = $ e.target + + unless $target.closest('.emoji-menu-content').length + $('.js-awards-block.current').removeClass 'current' + + unless $target.closest('.emoji-menu').length if $('.emoji-menu').is(':visible') $('.js-add-award.is-active').removeClass 'is-active' $('.emoji-menu').removeClass 'is-visible' $(document) .off 'click', '.js-emoji-btn' - .on 'click', '.js-emoji-btn', @handleClick + .on 'click', '.js-emoji-btn', (e) => + e.preventDefault() + $target = $ e.currentTarget + emoji = $target.find('.icon').data 'emoji' - handleClick: (e) => - - e.preventDefault() - - emoji = $(e.currentTarget).find('.icon').data 'emoji' - @getVotesBlock().addClass 'js-awards-block' - @addAward @getAwardUrl(), emoji + $target.closest('.js-awards-block').addClass 'current' + @addAward @getAwardUrl(), emoji showEmojiMenu: ($addBtn) -> - $menu = $('.emoji-menu') + $menu = $ '.emoji-menu' + + if $addBtn.hasClass 'note-emoji-button' + $addBtn.parents('.note').find('.js-awards-block').addClass 'current' + else + $addBtn.closest('.js-awards-block').addClass 'current' if $menu.length $holder = $addBtn.closest('.js-award-holder') @@ -68,7 +76,7 @@ class @AwardsHandler createEmojiMenu: (awardMenuUrl, callback) -> - $.get awardMenuUrl, (response) => + $.get awardMenuUrl, (response) -> $('body').append response callback() @@ -94,11 +102,10 @@ class @AwardsHandler addAward: (awardUrl, emoji, checkMutuality = yes) -> - emoji = @normilizeEmojiName(emoji) - @postEmoji awardUrl, emoji, => - @addAwardToEmojiBar(emoji, checkMutuality) + emoji = @normilizeEmojiName emoji - $('.js-awards-block-current').removeClass 'js-awards-block-current' + @postEmoji awardUrl, emoji, => + @addAwardToEmojiBar emoji, checkMutuality $('.emoji-menu').removeClass 'is-visible' @@ -108,25 +115,26 @@ class @AwardsHandler @checkMutuality emoji if checkForMutuality @addEmojiToFrequentlyUsedList(emoji) - emoji = @normilizeEmojiName(emoji) + emoji = @normilizeEmojiName(emoji) $emojiBtn = @findEmojiIcon(emoji).parent() if $emojiBtn.length > 0 - if @isActive($emojiBtn) - @decrementCounter($emojiBtn, emoji) + if @isActive $emojiBtn + @decrementCounter $emojiBtn, emoji else - counter = $emojiBtn.find('.js-counter') - counter.text(parseInt(counter.text()) + 1) - $emojiBtn.addClass('active') - @addMeToUserList(emoji) + counter = $emojiBtn.find '.js-counter' + counter.text parseInt(counter.text()) + 1 + $emojiBtn.addClass 'active' + @addMeToUserList emoji else - @createEmoji(emoji) + @getVotesBlock().removeClass 'hidden' + @createEmoji emoji - getVotesBlock: -> return $ '.awards.js-awards-block' + getVotesBlock: -> return $ '.js-awards-block.current' - getAwardUrl: -> @getVotesBlock().data 'award-url' + getAwardUrl: -> return @getVotesBlock().data 'award-url' checkMutuality: (emoji) -> @@ -135,8 +143,9 @@ class @AwardsHandler if emoji in [ 'thumbsup', 'thumbsdown' ] mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' + selector = "[data-emoji=#{mutualVote}]" - isAlreadyVoted = $("[data-emoji=#{mutualVote}]").parent().hasClass 'active' + isAlreadyVoted = @getVotesBlock().find(selector).parent().hasClass 'active' @addAward awardUrl, mutualVote, no if isAlreadyVoted @@ -144,28 +153,35 @@ class @AwardsHandler decrementCounter: ($emojiBtn, emoji) -> - isntNoteBody = $emojiBtn.closest('.note-body').length is 0 - counter = $('.js-counter', $emojiBtn) - counterNumber = parseInt(counter.text()) - if !isntNoteBody - # If this is a note body, we just hide the award emoji row like the initial state - $emojiBtn.closest('.js-awards-block').addClass 'hidden' + counter = $('.js-counter', $emojiBtn) + counterNumber = parseInt(counter.text()) if counterNumber > 1 counter.text(counterNumber - 1) @removeMeFromUserList($emojiBtn, emoji) - else if (emoji == 'thumbsup' || emoji == 'thumbsdown') && isntNoteBody + else if emoji is 'thumbsup' or emoji is 'thumbsdown' $emojiBtn.tooltip('destroy') counter.text('0') @removeMeFromUserList($emojiBtn, emoji) + @removeEmoji $emojiBtn if $emojiBtn.parents('.note').length else - $emojiBtn.tooltip('destroy') - $emojiBtn.remove() + @removeEmoji $emojiBtn $emojiBtn.removeClass('active') + removeEmoji: ($emojiBtn) -> + + $emojiBtn.tooltip('destroy') + $emojiBtn.remove() + + $votesBlock = @getVotesBlock() + + if $votesBlock.find('.js-emoji-btn').length is 0 + $votesBlock.addClass 'hidden' + + getAwardTooltip: ($awardBlock) -> return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') @@ -225,16 +241,12 @@ class @AwardsHandler " emoji_node = $(buttonHtml) - .insertBefore '.js-awards-block .js-award-holder:not(.js-award-action-btn)' + .insertBefore '.js-awards-block.current .js-award-holder:not(.js-award-action-btn)' .find '.emoji-icon' .data 'emoji', emoji $('.award-control').tooltip() - - $currentBlock = $ '.js-awards-block' - - if $currentBlock.is '.hidden' - $currentBlock.removeClass 'hidden' + $('.js-awards-block.current').removeClass 'current' createEmoji: (emoji) -> @@ -261,42 +273,53 @@ class @AwardsHandler postEmoji: (awardUrl, emoji, callback) -> + $.post awardUrl, { name: emoji }, (data) -> - if data.ok - callback.call() + callback.call() if data.ok + findEmojiIcon: (emoji) -> - $(".js-awards-block.awards > .js-emoji-btn [data-emoji='#{emoji}']") + + return $(".js-awards-block.current > .js-emoji-btn [data-emoji='#{emoji}']") + scrollToAwards: -> - $('body, html').animate({ - scrollTop: $('.awards').offset().top - 80 - }, 200) - normilizeEmojiName: (emoji) -> - @aliases[emoji] || emoji + options = scrollTop: $('.awards').offset().top - 80 + $('body, html').animate options, 200 + + + normilizeEmojiName: (emoji) -> return @aliases[emoji] or emoji + addEmojiToFrequentlyUsedList: (emoji) -> - frequently_used_emojis = @getFrequentlyUsedEmojis() - frequently_used_emojis.push(emoji) - $.cookie('frequently_used_emojis', frequently_used_emojis.join(','), { expires: 365 }) + + frequentlyUsedEmojis = @getFrequentlyUsedEmojis() + frequentlyUsedEmojis.push emoji + $.cookie 'frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 } + getFrequentlyUsedEmojis: -> - frequently_used_emojis = ($.cookie('frequently_used_emojis') || '').split(',') - _.compact(_.uniq(frequently_used_emojis)) + + frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') or '').split(',') + return _.compact _.uniq frequentlyUsedEmojis + renderFrequentlyUsedBlock: -> - if $.cookie('frequently_used_emojis') - frequently_used_emojis = @getFrequentlyUsedEmojis() + + if $.cookie 'frequently_used_emojis' + frequentlyUsedEmojis = @getFrequentlyUsedEmojis() ul = $("

                                        ") - for emoji in frequently_used_emojis + for emoji in frequentlyUsedEmojis $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) $('input.emoji-search').after(ul).after($('
                                        ').text('Frequently used')) + setupSearch: -> + $('input.emoji-search').on 'keyup', (ev) => term = $(ev.target).val() @@ -313,5 +336,7 @@ class @AwardsHandler else $('.emoji-menu-content').children().show() - searchEmojis: (term)-> + + searchEmojis: (term) -> + $(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone() diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index e9302c3975..84fd146a26 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -11,7 +11,7 @@ gl.awardMenuUrl = "#{emojis_path}" .award-menu-holder.js-award-holder - %button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } + %button.btn.award-control.js-add-award{ type: "button" } = icon('smile-o', class: "award-control-icon award-control-icon-normal") = icon('spinner spin', class: "award-control-icon award-control-icon-loading") %span.award-control-text diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index c176778c51..47ab890631 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -22,7 +22,7 @@ %span.note-role = access - if note_editable - = link_to '#', title: 'Add Reaction', class: 'note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do + = link_to '#', title: 'Award Emoji', class: 'note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do = icon('spinner spin') = icon('smile-o') = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do @@ -35,6 +35,8 @@ = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author) - if note_editable = render 'projects/notes/edit_form', note: note + .note-awards + = render 'award_emoji/awards_block', awardable: note, inline: false = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note.attachment.url From 40c86c916e252dd3556c0dc526e6a9de505e5de3 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Sat, 28 May 2016 01:49:18 +0300 Subject: [PATCH 261/507] Refactor awardsHandler methods to work with votesBlock. --- app/assets/javascripts/awards_handler.coffee | 68 ++++++++++---------- app/assets/javascripts/notes.js.coffee | 3 +- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 071dbd3664..27b2853e7d 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -32,7 +32,7 @@ class @AwardsHandler emoji = $target.find('.icon').data 'emoji' $target.closest('.js-awards-block').addClass 'current' - @addAward @getAwardUrl(), emoji + @addAward @getVotesBlock(), @getAwardUrl(), emoji showEmojiMenu: ($addBtn) -> @@ -100,23 +100,23 @@ class @AwardsHandler $menu.css(css) - addAward: (awardUrl, emoji, checkMutuality = yes) -> + addAward: (votesBlock, awardUrl, emoji, checkMutuality = yes) -> emoji = @normilizeEmojiName emoji @postEmoji awardUrl, emoji, => - @addAwardToEmojiBar emoji, checkMutuality + @addAwardToEmojiBar votesBlock, emoji, checkMutuality $('.emoji-menu').removeClass 'is-visible' - addAwardToEmojiBar: (emoji, checkForMutuality = yes) -> + addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = yes) -> - @checkMutuality emoji if checkForMutuality - @addEmojiToFrequentlyUsedList(emoji) + @checkMutuality votesBlock, emoji if checkForMutuality + @addEmojiToFrequentlyUsedList emoji - emoji = @normilizeEmojiName(emoji) - $emojiBtn = @findEmojiIcon(emoji).parent() + emoji = @normilizeEmojiName emoji + $emojiBtn = @findEmojiIcon(votesBlock, emoji).parent() if $emojiBtn.length > 0 if @isActive $emojiBtn @@ -125,10 +125,10 @@ class @AwardsHandler counter = $emojiBtn.find '.js-counter' counter.text parseInt(counter.text()) + 1 $emojiBtn.addClass 'active' - @addMeToUserList emoji + @addMeToUserList votesBlock, emoji else - @getVotesBlock().removeClass 'hidden' - @createEmoji emoji + votesBlock.removeClass 'hidden' + @createEmoji votesBlock, emoji getVotesBlock: -> return $ '.js-awards-block.current' @@ -137,7 +137,7 @@ class @AwardsHandler getAwardUrl: -> return @getVotesBlock().data 'award-url' - checkMutuality: (emoji) -> + checkMutuality: (votesBlock, emoji) -> awardUrl = @getAwardUrl() @@ -145,8 +145,8 @@ class @AwardsHandler mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' selector = "[data-emoji=#{mutualVote}]" - isAlreadyVoted = @getVotesBlock().find(selector).parent().hasClass 'active' - @addAward awardUrl, mutualVote, no if isAlreadyVoted + isAlreadyVoted = votesBlock.find(selector).parent().hasClass 'active' + @addAward votesBlock, awardUrl, mutualVote, no if isAlreadyVoted isActive: ($emojiBtn) -> $emojiBtn.hasClass 'active' @@ -155,20 +155,20 @@ class @AwardsHandler decrementCounter: ($emojiBtn, emoji) -> counter = $('.js-counter', $emojiBtn) - counterNumber = parseInt(counter.text()) + counterNumber = parseInt counter.text(), 10 if counterNumber > 1 - counter.text(counterNumber - 1) - @removeMeFromUserList($emojiBtn, emoji) + counter.text counterNumber - 1 + @removeMeFromUserList $emojiBtn, emoji else if emoji is 'thumbsup' or emoji is 'thumbsdown' - $emojiBtn.tooltip('destroy') - counter.text('0') - @removeMeFromUserList($emojiBtn, emoji) + $emojiBtn.tooltip 'destroy' + counter.text '0' + @removeMeFromUserList $emojiBtn, emoji @removeEmoji $emojiBtn if $emojiBtn.parents('.note').length else @removeEmoji $emojiBtn - $emojiBtn.removeClass('active') + $emojiBtn.removeClass 'active' removeEmoji: ($emojiBtn) -> @@ -207,9 +207,9 @@ class @AwardsHandler @resetTooltip(awardBlock) - addMeToUserList: (emoji) -> + addMeToUserList: (votesBlock, emoji) -> - awardBlock = @findEmojiIcon(emoji).parent() + awardBlock = @findEmojiIcon(votesBlock, emoji).parent() origTitle = @getAwardTooltip awardBlock users = [] @@ -231,29 +231,29 @@ class @AwardsHandler ), 200 - createEmoji_: (emoji) -> + createEmoji_: (votesBlock, emoji) -> emojiCssClass = @resolveNameToCssClass emoji - - buttonHtml = "" emoji_node = $(buttonHtml) - .insertBefore '.js-awards-block.current .js-award-holder:not(.js-award-action-btn)' + .insertBefore votesBlock.find '.js-award-holder:not(.js-award-action-btn)' .find '.emoji-icon' .data 'emoji', emoji $('.award-control').tooltip() - $('.js-awards-block.current').removeClass 'current' + votesBlock.removeClass 'current' - createEmoji: (emoji) -> + createEmoji: (votesBlock, emoji) -> - return @createEmoji_ emoji if $('.emoji-menu').length + if $('.emoji-menu').length + return @createEmoji_ votesBlock, emoji - @createEmojiMenu @getAwardMenuUrl(), => @createEmoji emoji + @createEmojiMenu @getAwardMenuUrl(), => @createEmoji votesBlock, emoji getAwardMenuUrl: -> return gl.awardMenuUrl or '/emojis' @@ -278,14 +278,14 @@ class @AwardsHandler callback.call() if data.ok - findEmojiIcon: (emoji) -> + findEmojiIcon: (votesBlock, emoji) -> - return $(".js-awards-block.current > .js-emoji-btn [data-emoji='#{emoji}']") + return votesBlock.find ".js-emoji-btn [data-emoji='#{emoji}']" scrollToAwards: -> - options = scrollTop: $('.awards').offset().top - 80 + options = scrollTop: $('.awards').offset().top - 110 $('body, html').animate options, 200 diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 101154114b..0fdbe55ea9 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -167,7 +167,8 @@ class @Notes return if note.award - awardsHandler.addAwardToEmojiBar(note.name) + votesBlock = $('.js-awards-block').eq 0 + awardsHandler.addAwardToEmojiBar votesBlock, note.name awardsHandler.scrollToAwards() # render note if it not present in loaded list From ce5729139fb8e37f4242996b29c4dfb181610acd Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 30 May 2016 16:15:02 +0300 Subject: [PATCH 262/507] Fix mutual exclusivity for emoji only comments. --- app/assets/javascripts/awards_handler.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 27b2853e7d..2b3e6ad622 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -131,7 +131,10 @@ class @AwardsHandler @createEmoji votesBlock, emoji - getVotesBlock: -> return $ '.js-awards-block.current' + getVotesBlock: -> + + currentBlock = $ '.js-awards-block.current' + return if currentBlock.length then currentBlock else $('.js-awards-block').eq 0 getAwardUrl: -> return @getVotesBlock().data 'award-url' From 59b34188fa26bd50e0696df6dd6fef786334f092 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 30 May 2016 16:27:25 +0300 Subject: [PATCH 263/507] Animate emoji when rendered. --- app/assets/javascripts/awards_handler.coffee | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 2b3e6ad622..0c7b3493a1 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -126,6 +126,7 @@ class @AwardsHandler counter.text parseInt(counter.text()) + 1 $emojiBtn.addClass 'active' @addMeToUserList votesBlock, emoji + @animateEmoji $emojiBtn else votesBlock.removeClass 'hidden' @createEmoji votesBlock, emoji @@ -242,15 +243,25 @@ class @AwardsHandler 1 " - emoji_node = $(buttonHtml) + $emojiButton = $ buttonHtml + emoji_node = $emojiButton .insertBefore votesBlock.find '.js-award-holder:not(.js-award-action-btn)' .find '.emoji-icon' .data 'emoji', emoji + @animateEmoji $emojiButton $('.award-control').tooltip() votesBlock.removeClass 'current' + animateEmoji: ($emoji) -> + + className = 'pulse animated' + + $emoji.addClass className + setTimeout (-> $emoji.removeClass className), 321 + + createEmoji: (votesBlock, emoji) -> if $('.emoji-menu').length From 47fc235fe1d8b6387428a13ea7bda33b59561cee Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 30 May 2016 16:39:43 +0300 Subject: [PATCH 264/507] Be consistent on naming. It was named as $emojiBtn before I was using $emojiButton so updated them to be consistent. --- app/assets/javascripts/awards_handler.coffee | 44 ++++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 0c7b3493a1..4425921b13 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -115,18 +115,18 @@ class @AwardsHandler @checkMutuality votesBlock, emoji if checkForMutuality @addEmojiToFrequentlyUsedList emoji - emoji = @normilizeEmojiName emoji - $emojiBtn = @findEmojiIcon(votesBlock, emoji).parent() + emoji = @normilizeEmojiName emoji + $emojiButton = @findEmojiIcon(votesBlock, emoji).parent() - if $emojiBtn.length > 0 - if @isActive $emojiBtn - @decrementCounter $emojiBtn, emoji + if $emojiButton.length > 0 + if @isActive $emojiButton + @decrementCounter $emojiButton, emoji else - counter = $emojiBtn.find '.js-counter' + counter = $emojiButton.find '.js-counter' counter.text parseInt(counter.text()) + 1 - $emojiBtn.addClass 'active' + $emojiButton.addClass 'active' @addMeToUserList votesBlock, emoji - @animateEmoji $emojiBtn + @animateEmoji $emojiButton else votesBlock.removeClass 'hidden' @createEmoji votesBlock, emoji @@ -153,32 +153,32 @@ class @AwardsHandler @addAward votesBlock, awardUrl, mutualVote, no if isAlreadyVoted - isActive: ($emojiBtn) -> $emojiBtn.hasClass 'active' + isActive: ($emojiButton) -> $emojiButton.hasClass 'active' - decrementCounter: ($emojiBtn, emoji) -> + decrementCounter: ($emojiButton, emoji) -> - counter = $('.js-counter', $emojiBtn) + counter = $('.js-counter', $emojiButton) counterNumber = parseInt counter.text(), 10 if counterNumber > 1 counter.text counterNumber - 1 - @removeMeFromUserList $emojiBtn, emoji + @removeMeFromUserList $emojiButton, emoji else if emoji is 'thumbsup' or emoji is 'thumbsdown' - $emojiBtn.tooltip 'destroy' + $emojiButton.tooltip 'destroy' counter.text '0' - @removeMeFromUserList $emojiBtn, emoji - @removeEmoji $emojiBtn if $emojiBtn.parents('.note').length + @removeMeFromUserList $emojiButton, emoji + @removeEmoji $emojiButton if $emojiButton.parents('.note').length else - @removeEmoji $emojiBtn + @removeEmoji $emojiButton - $emojiBtn.removeClass 'active' + $emojiButton.removeClass 'active' - removeEmoji: ($emojiBtn) -> + removeEmoji: ($emojiButton) -> - $emojiBtn.tooltip('destroy') - $emojiBtn.remove() + $emojiButton.tooltip('destroy') + $emojiButton.remove() $votesBlock = @getVotesBlock() @@ -191,9 +191,9 @@ class @AwardsHandler return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') - removeMeFromUserList: ($emojiBtn, emoji) -> + removeMeFromUserList: ($emojiButton, emoji) -> - awardBlock = $emojiBtn + awardBlock = $emojiButton originalTitle = @getAwardTooltip awardBlock authors = originalTitle.split ', ' From f1d74ccc8ca8e3fa91e87193fc886e9a00a636b3 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 30 May 2016 19:33:43 +0300 Subject: [PATCH 265/507] Some design related tweaks. --- app/assets/javascripts/awards_handler.coffee | 2 +- app/assets/stylesheets/framework/timeline.scss | 2 +- app/assets/stylesheets/pages/awards.scss | 1 + app/assets/stylesheets/pages/notes.scss | 16 ++++++++++++++++ app/views/projects/issues/show.html.haml | 6 +++--- app/views/projects/notes/_note.html.haml | 4 ++-- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 4425921b13..8a6f6d7d18 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -39,7 +39,7 @@ class @AwardsHandler $menu = $ '.emoji-menu' - if $addBtn.hasClass 'note-emoji-button' + if $addBtn.hasClass 'js-note-emoji' $addBtn.parents('.note').find('.js-awards-block').addClass 'current' else $addBtn.closest('.js-awards-block').addClass 'current' diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 29501069d2..62935c95c5 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -5,7 +5,7 @@ padding: 0; .timeline-entry { - padding: $gl-padding $gl-btn-padding; + padding: $gl-padding $gl-btn-padding 11px $gl-btn-padding; border-color: $table-border-color; color: $gl-gray; border-bottom: 1px solid $border-white-light; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 07d40f4055..56d04229f0 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -95,6 +95,7 @@ .award-control { margin-right: 5px; + margin-bottom: 5px; padding-left: 5px; padding-right: 5px; line-height: 20px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 3acc9152c0..53a9d1144c 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -69,6 +69,10 @@ ul.notes { .note-edit-form { display: block; + + &.current-note-edit-form + .note-awards { + display: none; + } } } @@ -116,6 +120,18 @@ ul.notes { } } + .note-awards { + .js-awards-block { + padding: 2px 0; + margin-top: 10px; + } + + .award-control { + font-size: 13px; + padding: 2px 5px; + } + } + .note-header { padding-bottom: 3px; } diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index a35c13fbd4..b2f14a5407 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -68,9 +68,9 @@ #related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } } // This element is filled in using JavaScript. - .content-block.content-block-small - = render 'new_branch' - = render 'award_emoji/awards_block', awardable: @issue, inline: true + .content-block.content-block-small + = render 'new_branch' + = render 'award_emoji/awards_block', awardable: @issue, inline: true %section.issuable-discussion = render 'projects/issues/discussion' diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 47ab890631..3a1aa35fa2 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -22,7 +22,7 @@ %span.note-role = access - if note_editable - = link_to '#', title: 'Award Emoji', class: 'note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do + = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do = icon('spinner spin') = icon('smile-o') = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do @@ -33,11 +33,11 @@ .note-text = preserve do = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author) + = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable = render 'projects/notes/edit_form', note: note .note-awards = render 'award_emoji/awards_block', awardable: note, inline: false - = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note.attachment.url .note-attachment From 63a5402182aa9697bbd9e2d7192ef661c6d74b86 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 30 May 2016 20:04:08 +0300 Subject: [PATCH 266/507] Create and show emoji loader on the fly. --- app/assets/javascripts/awards_handler.coffee | 24 ++++++++++++++++---- app/assets/stylesheets/pages/awards.scss | 3 ++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 8a6f6d7d18..e01c5d68c4 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -100,12 +100,13 @@ class @AwardsHandler $menu.css(css) - addAward: (votesBlock, awardUrl, emoji, checkMutuality = yes) -> + addAward: (votesBlock, awardUrl, emoji, checkMutuality = yes, callback) -> emoji = @normilizeEmojiName emoji @postEmoji awardUrl, emoji, => @addAwardToEmojiBar votesBlock, emoji, checkMutuality + callback?() $('.emoji-menu').removeClass 'is-visible' @@ -146,11 +147,24 @@ class @AwardsHandler awardUrl = @getAwardUrl() if emoji in [ 'thumbsup', 'thumbsdown' ] - mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' - selector = "[data-emoji=#{mutualVote}]" + mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' + $emojiButton = votesBlock.find("[data-emoji=#{mutualVote}]").parent() + isAlreadyVoted = $emojiButton.hasClass 'active' - isAlreadyVoted = votesBlock.find(selector).parent().hasClass 'active' - @addAward votesBlock, awardUrl, mutualVote, no if isAlreadyVoted + if isAlreadyVoted + @showEmojiLoader $emojiButton + @addAward votesBlock, awardUrl, mutualVote, no, -> + $emojiButton.removeClass 'is-loading' + + + showEmojiLoader: ($emojiButton) -> + + $loader = $emojiButton.find '.fa-spinner' + + unless $loader.length + $emojiButton.append '' + + $emojiButton.addClass 'is-loading' isActive: ($emojiButton) -> $emojiButton.hasClass 'active' diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 56d04229f0..05d1ee5b99 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -109,7 +109,8 @@ } &.is-loading { - .award-control-icon-normal { + .award-control-icon-normal, + .emoji-icon { display: none; } From c1e2b02e34e5038be0c39e11c3172c3375dc0734 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Tue, 31 May 2016 22:32:29 +0300 Subject: [PATCH 267/507] Added tests for award emoji feature. --- app/assets/javascripts/awards_handler.coffee | 20 +- app/assets/stylesheets/pages/notes.scss | 2 +- .../javascripts/awards_handler_spec.js.coffee | 202 ++++ .../behaviors/quick_submit_spec.js.coffee | 22 +- .../fixtures/awards_handler.html.haml | 52 + .../fixtures/behaviors/quick_submit.html.haml | 2 +- spec/javascripts/fixtures/emoji_menu.coffee | 957 ++++++++++++++++++ spec/javascripts/new_branch_spec.js.coffee | 2 +- 8 files changed, 1235 insertions(+), 24 deletions(-) create mode 100644 spec/javascripts/awards_handler_spec.js.coffee create mode 100644 spec/javascripts/fixtures/awards_handler.html.haml create mode 100644 spec/javascripts/fixtures/emoji_menu.coffee diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index e01c5d68c4..4f4009e6db 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -202,7 +202,7 @@ class @AwardsHandler getAwardTooltip: ($awardBlock) -> - return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') + return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') or '' removeMeFromUserList: ($emojiButton, emoji) -> @@ -222,7 +222,7 @@ class @AwardsHandler .attr 'data-original-title', newAuthors .attr 'data-title', newAuthors - @resetTooltip(awardBlock) + @resetTooltip awardBlock addMeToUserList: (votesBlock, emoji) -> @@ -232,21 +232,21 @@ class @AwardsHandler users = [] if origTitle - users = origTitle.trim().split(', ') + users = origTitle.trim().split ', ' - users.push('me') - awardBlock.attr('title', users.join(', ')) + users.push 'me' + awardBlock.attr 'title', users.join ', ' - @resetTooltip(awardBlock) + @resetTooltip awardBlock resetTooltip: (award) -> - award.tooltip('destroy') + + award.tooltip 'destroy' # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. - setTimeout (-> - award.tooltip() - ), 200 + cb = -> award.tooltip() + setTimeout cb, 200 createEmoji_: (votesBlock, emoji) -> diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 53a9d1144c..4ebaf22727 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -122,7 +122,7 @@ ul.notes { .note-awards { .js-awards-block { - padding: 2px 0; + padding: 2px; margin-top: 10px; } diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee new file mode 100644 index 0000000000..5a95ae52dc --- /dev/null +++ b/spec/javascripts/awards_handler_spec.js.coffee @@ -0,0 +1,202 @@ +#= require awards_handler +#= require jquery +#= require jquery.cookie +#= require ./fixtures/emoji_menu + +awardsHandler = null +window.gl or= {} +window.gl.awardMenuUrl = '/emojis' +window.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } + + +lazyAssert = (done, assertFn) -> + + setTimeout -> # Maybe jasmine.clock here? + assertFn() + done() + , 333 + + +describe 'AwardsHandler', -> + + fixture.preload 'awards_handler.html' + + beforeEach -> + fixture.load 'awards_handler.html' + awardsHandler = new AwardsHandler + spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb() + spyOn(jQuery, 'get').and.callFake (req, cb) -> + expect(req).toBe '/emojis' + cb window.emojiMenu + + + describe '::showEmojiMenu', -> + + it 'should show emoji menu when Add emoji button clicked', (done) -> + + $('.js-add-award').eq(0).click() + + lazyAssert done, -> + $emojiMenu = $ '.emoji-menu' + expect($emojiMenu.length).toBe 1 + expect($emojiMenu.hasClass('is-visible')).toBe yes + expect($emojiMenu.find('#emoji_search').length).toBe 1 + expect($('.js-awards-block.current').length).toBe 1 + + + it 'should also show emoji menu for the smiley icon in notes', (done) -> + + $('.note-action-button').click() + + lazyAssert done, -> + $emojiMenu = $ '.emoji-menu' + expect($emojiMenu.length).toBe 1 + + + it 'should remove emoji menu when body is clicked', (done) -> + + $('.js-add-award').eq(0).click() + + lazyAssert done, -> + $emojiMenu = $('.emoji-menu') + $('body').click() + expect($emojiMenu.length).toBe 1 + expect($emojiMenu.hasClass('is-visible')).toBe no + expect($('.js-awards-block.current').length).toBe 0 + + + describe '::addAwardToEmojiBar', -> + + it 'should add emoji to votes block', -> + + $votesBlock = $('.js-awards-block').eq 0 + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + + $emojiButton = $votesBlock.find '[data-emoji=heart]' + + expect($emojiButton.length).toBe 1 + expect($emojiButton.next('.js-counter').text()).toBe '1' + expect($votesBlock.hasClass('hidden')).toBe no + + + it 'should remove the emoji when we click again', -> + + $votesBlock = $('.js-awards-block').eq 0 + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + $emojiButton = $votesBlock.find '[data-emoji=heart]' + + expect($emojiButton.length).toBe 0 + + + it 'should decrement the emoji counter', -> + + $votesBlock = $('.js-awards-block').eq 0 + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + + $emojiButton = $votesBlock.find '[data-emoji=heart]' + $emojiButton.next('.js-counter').text 5 + + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + + expect($emojiButton.length).toBe 1 + expect($emojiButton.next('.js-counter').text()).toBe '4' + + + describe '::getAwardUrl', -> + + it 'should return the url for request', -> + + expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji' + + + describe '::addAward and ::checkMutuality', -> + + it 'should handle :+1: and :-1: mutuality', -> + + awardUrl = awardsHandler.getAwardUrl() + $votesBlock = $('.js-awards-block').eq 0 + $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent() + $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent() + + awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no + + expect($thumbsUpEmoji.hasClass('active')).toBe yes + expect($thumbsDownEmoji.hasClass('active')).toBe no + + $thumbsUpEmoji.tooltip() + $thumbsDownEmoji.tooltip() + + awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes + + expect($thumbsUpEmoji.hasClass('active')).toBe no + expect($thumbsDownEmoji.hasClass('active')).toBe yes + + + describe '::removeEmoji', -> + + it 'should remove emoji', -> + + awardUrl = awardsHandler.getAwardUrl() + $votesBlock = $('.js-awards-block').eq 0 + + awardsHandler.addAward $votesBlock, awardUrl, 'fire', no + expect($votesBlock.find('[data-emoji=fire]').length).toBe 1 + + awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button') + expect($votesBlock.find('[data-emoji=fire]').length).toBe 0 + + + describe 'search', -> + + it 'should filter the emoji', -> + + $('.js-add-award').eq(0).click() + + expect($('[data-emoji=angel]').is(':visible')).toBe yes + expect($('[data-emoji=anger]').is(':visible')).toBe yes + + $('#emoji_search').val('ali').trigger 'keyup' + + expect($('[data-emoji=angel]').is(':visible')).toBe no + expect($('[data-emoji=anger]').is(':visible')).toBe no + expect($('[data-emoji=alien]').is(':visible')).toBe yes + expect($('h5.emoji-search').is(':visible')).toBe yes + + + describe 'emoji menu', -> + + selector = '[data-emoji=sunglasses]' + + openEmojiMenuAndAddEmoji = -> + + $('.js-add-award').eq(0).click() + + $menu = $ '.emoji-menu' + $block = $ '.js-awards-block' + $emoji = $menu.find ".emoji-menu-list-item #{selector}" + + expect($emoji.length).toBe 1 + expect($block.find(selector).length).toBe 0 + + $emoji.click() + + expect($menu.hasClass('.is-visible')).toBe no + expect($block.find(selector).length).toBe 1 + + + it 'should add selected emoji to awards block', -> + + openEmojiMenuAndAddEmoji() + + + it 'should remove already selected emoji', -> + + openEmojiMenuAndAddEmoji() + $('.js-add-award').eq(0).click() + + $block = $ '.js-awards-block' + $emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}" + + $emoji.click() + expect($block.find(selector).length).toBe 0 diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee index 09708c12ed..d3b003a328 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee +++ b/spec/javascripts/behaviors/quick_submit_spec.js.coffee @@ -14,17 +14,17 @@ describe 'Quick Submit behavior', -> } it 'does not respond to other keyCodes', -> - $('input').trigger(keydownEvent(keyCode: 32)) + $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32)) expect(@spies.submit).not.toHaveBeenTriggered() it 'does not respond to Enter alone', -> - $('input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) + $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) expect(@spies.submit).not.toHaveBeenTriggered() it 'does not respond to repeated events', -> - $('input').trigger(keydownEvent(repeat: true)) + $('input.quick-submit-input').trigger(keydownEvent(repeat: true)) expect(@spies.submit).not.toHaveBeenTriggered() @@ -38,26 +38,26 @@ describe 'Quick Submit behavior', -> # only run the tests that apply to the current platform if navigator.userAgent.match(/Macintosh/) it 'responds to Meta+Enter', -> - $('input').trigger(keydownEvent()) + $('input.quick-submit-input').trigger(keydownEvent()) expect(@spies.submit).toHaveBeenTriggered() it 'excludes other modifier keys', -> - $('input').trigger(keydownEvent(altKey: true)) - $('input').trigger(keydownEvent(ctrlKey: true)) - $('input').trigger(keydownEvent(shiftKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) expect(@spies.submit).not.toHaveBeenTriggered() else it 'responds to Ctrl+Enter', -> - $('input').trigger(keydownEvent()) + $('input.quick-submit-input').trigger(keydownEvent()) expect(@spies.submit).toHaveBeenTriggered() it 'excludes other modifier keys', -> - $('input').trigger(keydownEvent(altKey: true)) - $('input').trigger(keydownEvent(metaKey: true)) - $('input').trigger(keydownEvent(shiftKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(metaKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) expect(@spies.submit).not.toHaveBeenTriggered() diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml new file mode 100644 index 0000000000..d55936ee4f --- /dev/null +++ b/spec/javascripts/fixtures/awards_handler.html.haml @@ -0,0 +1,52 @@ +.issue-details.issuable-details + .detail-page-description.content-block + %h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem. + .description.js-task-list-container.is-task-list-enabled + .wiki + %p Qui exercitationem magnam optio quae fuga earum odio. + %textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio. + %small.edited-text + .content-block.content-block-small + .awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"} + %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"} + .icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"} + %span.award-control-text.js-counter 0 + %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"} + .icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"} + %span.award-control-text.js-counter 0 + .award-menu-holder.js-award-holder + %button.btn.award-control.js-add-award{:type => "button"} + %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal + %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading + %span.award-control-text Add + %section.issuable-discussion + #notes + %ul#notes-list.notes.main-notes-list.timeline + %li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""} + .timeline-entry-inner + .timeline-icon + %a{:href => "/u/agustin"} + %img.avatar.s40{:alt => "", :src => "#"}/ + .timeline-content + .note-header + %a.author_link{:href => "/u/agustin"} + %span.author Brenna Stokes + .inline.note-headline-light + @agustin commented + %a{:href => "#note_348"} + %time 11 days ago + .note-actions + %span.note-role Reporter + %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"} + %i.fa.fa-spinner.fa-spin + %i.fa.fa-smile-o + .js-task-list-container.note-body.is-task-list-enabled + .note-text + %p Suscipit sunt quia quisquam sed eveniet ipsam. + .note-awards + .awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"} + .award-menu-holder.js-award-holder + %button.btn.award-control.js-add-award{:type => "button"} + %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal + %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading + %span.award-control-text Add diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml index e3788bee81..dc2ceed42f 100644 --- a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml +++ b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml @@ -1,5 +1,5 @@ %form.js-quick-submit{ action: '/foo' } - %input{ type: 'text' } + %input{ type: 'text', class: 'quick-submit-input'} %textarea %input{ type: 'submit'} Submit diff --git a/spec/javascripts/fixtures/emoji_menu.coffee b/spec/javascripts/fixtures/emoji_menu.coffee new file mode 100644 index 0000000000..e529dd5f1c --- /dev/null +++ b/spec/javascripts/fixtures/emoji_menu.coffee @@ -0,0 +1,957 @@ +window.emojiMenu = """ +
                                        +
                                        + +
                                        + Emoticons +
                                        +
                                          +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        • + +
                                        • +
                                        +
                                        +
                                        +""" diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee index f2ce85efcd..ce77379381 100644 --- a/spec/javascripts/new_branch_spec.js.coffee +++ b/spec/javascripts/new_branch_spec.js.coffee @@ -1,4 +1,4 @@ -#= require jquery-ui +#= require jquery-ui/autocomplete #= require new_branch_form describe 'Branch', -> From 5dbb3883f81682f07425c2ddb52bd0f75059885d Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 1 Jun 2016 20:14:45 +0300 Subject: [PATCH 268/507] Fix scss-lint. --- app/assets/stylesheets/framework/timeline.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 62935c95c5..0b0bd80c32 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -5,7 +5,7 @@ padding: 0; .timeline-entry { - padding: $gl-padding $gl-btn-padding 11px $gl-btn-padding; + padding: $gl-padding $gl-btn-padding 11px; border-color: $table-border-color; color: $gl-gray; border-bottom: 1px solid $border-white-light; From 7d3f8f542f0fe02ae27d5f527d578c19f47eec71 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 2 Jun 2016 18:25:51 +0200 Subject: [PATCH 269/507] Update tests on wording --- spec/controllers/projects/notes_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index cb7d04a059..00bc38b607 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -16,12 +16,12 @@ describe Projects::NotesController do expect do post(:toggle_award_emoji, namespace_id: project.namespace.path, project_id: project.path, id: note.id, name: "thumbsup") - end.to change { AwardEmoji.count }.by(1) + end.to change { note.award_emoji.count }.by(1) expect(response.status).to eq(200) end - it "removes the already let award emoji" do + it "removes the already awarded emoji" do post(:toggle_award_emoji, namespace_id: project.namespace.path, project_id: project.path, id: note.id, name: "thumbsup") From 9614c52266fd7009c1dd2960564d4f65b1a12f80 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Jun 2016 11:19:54 +0200 Subject: [PATCH 270/507] Fix tests offenses: use `pipeline` of Ci::Build and rename views --- .../projects/merge_requests_controller.rb | 2 +- app/views/admin/runners/show.html.haml | 4 +- app/views/notify/build_fail_email.html.haml | 4 +- app/views/notify/build_fail_email.text.erb | 6 +-- .../notify/build_success_email.html.haml | 4 +- app/views/notify/build_success_email.text.erb | 6 +-- app/views/projects/builds/show.html.haml | 12 ++--- .../_pipeline.html.haml} | 44 +++++++++---------- features/steps/shared/builds.rb | 2 +- spec/requests/api/merge_requests_spec.rb | 2 +- spec/requests/ci/api/builds_spec.rb | 2 +- 11 files changed, 44 insertions(+), 44 deletions(-) rename app/views/projects/ci/{commits/_commit.html.haml => pipelines/_pipeline.html.haml} (57%) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e6924a6a45..1fca05e949 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -316,7 +316,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff @pipeline = @merge_request.pipeline - @statuses = @ci_commit.statuses if @pipeline + @statuses = @pipeline.statuses if @pipeline if @merge_request.locked_long_ago? @merge_request.unlock_mr diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index c3784bf719..e049b40bfa 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -99,8 +99,8 @@ %td.build-link - if project - = link_to ci_status_path(build.commit) do - %strong #{build.commit.short_sha} + = link_to ci_status_path(build.pipeline) do + %strong #{build.pipeline.short_sha} %td.timestamp - if build.finished_at diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index 81d6503731..4bf7c1f4d6 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -10,7 +10,7 @@ %p Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} %p - Author: #{@build.commit.git_author_name} + Author: #{@build.pipeline.git_author_name} %p Branch: #{@build.ref} %p @@ -18,7 +18,7 @@ %p Job: #{@build.name} %p - Message: #{@build.commit.git_commit_message} + Message: #{@build.pipeline.git_commit_message} %p Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb index 675acea60a..9d49798349 100644 --- a/app/views/notify/build_fail_email.text.erb +++ b/app/views/notify/build_fail_email.text.erb @@ -1,11 +1,11 @@ Build failed for <%= @project.name %> Status: <%= @build.status %> -Commit: <%= @build.commit.short_sha %> -Author: <%= @build.commit.git_author_name %> +Commit: <%= @build.pipeline.short_sha %> +Author: <%= @build.pipeline.git_author_name %> Branch: <%= @build.ref %> Stage: <%= @build.stage %> Job: <%= @build.name %> -Message: <%= @build.commit.git_commit_message %> +Message: <%= @build.pipeline.git_commit_message %> Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 5d247eb4cf..252a5b7152 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -10,7 +10,7 @@ %p Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} %p - Author: #{@build.commit.git_author_name} + Author: #{@build.pipeline.git_author_name} %p Branch: #{@build.ref} %p @@ -18,7 +18,7 @@ %p Job: #{@build.name} %p - Message: #{@build.commit.git_commit_message} + Message: #{@build.pipeline.git_commit_message} %p Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb index 747da44aca..c5ed4f8486 100644 --- a/app/views/notify/build_success_email.text.erb +++ b/app/views/notify/build_success_email.text.erb @@ -1,11 +1,11 @@ Build successful for <%= @project.name %> Status: <%= @build.status %> -Commit: <%= @build.commit.short_sha %> -Author: <%= @build.commit.git_author_name %> +Commit: <%= @build.pipeline.short_sha %> +Author: <%= @build.pipeline.git_author_name %> Branch: <%= @build.ref %> Stage: <%= @build.stage %> Job: <%= @build.name %> -Message: <%= @build.commit.git_commit_message %> +Message: <%= @build.pipeline.git_commit_message %> Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 16017c994b..5477fc65c2 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -4,7 +4,7 @@ .build-page .row-content-block.top-block Build ##{@build.id} for commit - %strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit) + %strong.monospace= link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline) from = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) - merge_request = @build.merge_request @@ -13,7 +13,7 @@ = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request) #up-build-trace - - builds = @build.commit.builds.latest.to_a + - builds = @build.pipeline.builds.latest.to_a - if builds.size > 1 %ul.nav-links.no-top.no-bottom - builds.each do |build| @@ -178,16 +178,16 @@ Commit .pull-right %small - = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" + = link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline), class: "monospace" %p %span.attr-name Branch: = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) %p %span.attr-name Author: - #{@build.commit.git_author_name} + #{@build.pipeline.git_author_name} %p %span.attr-name Message: - #{@build.commit.git_commit_message} + #{@build.pipeline.git_commit_message} - if @build.tags.any? .build-widget @@ -201,7 +201,7 @@ .build-widget %h4.title #{pluralize(@builds.count(:id), "other build")} for = succeed ":" do - = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" + = link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline), class: "monospace" %table.table.builds - @builds.each_with_index do |build, i| %tr.build diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml similarity index 57% rename from app/views/projects/ci/commits/_commit.html.haml rename to app/views/projects/ci/pipelines/_pipeline.html.haml index 5e3a4123a8..a0ffa06506 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,55 +1,55 @@ -- status = commit.status +- status = pipeline.status %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: "ci-status ci-#{status}" do + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: "ci-status ci-#{status}" do = ci_icon_for_status(status) - %strong ##{commit.id} + %strong ##{pipeline.id} %td %div.branch-commit - - if commit.ref - = link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref), class: "monospace" + - if pipeline.ref + = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace" · - = link_to commit.short_sha, namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "commit-id monospace" + = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace"   - - if commit.tag? + - if pipeline.tag? %span.label.label-primary tag - - elsif commit.latest? + - elsif pipeline.latest? %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest - - if commit.triggered? + - if pipeline.triggered? %span.label.label-primary triggered - - if commit.yaml_errors.present? - %span.label.label-danger.has-tooltip{ title: "#{commit.yaml_errors}" } yaml invalid - - if commit.builds.any?(&:stuck?) + - if pipeline.yaml_errors.present? + %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid + - if pipeline.builds.any?(&:stuck?) %span.label.label-warning stuck %p.commit-title - - if commit_data = commit.commit_data + - if commit_data = pipeline.commit_data = link_to_gfm truncate(commit_data.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message" - else Cant find HEAD commit for this branch - - stages_status = commit.statuses.stages_status + - stages_status = pipeline.statuses.stages_status - stages.each do |stage| %td - status = stages_status[stage] - tooltip = "#{stage.titleize}: #{status || 'not found'}" - if status - = link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do = ci_icon_for_status(status) - else .light.has-tooltip{ title: tooltip } \- %td - - if commit.started_at && commit.finished_at + - if pipeline.started_at && pipeline.finished_at %p.duration - #{duration_in_words(commit.finished_at, commit.started_at)} + #{duration_in_words(pipeline.finished_at, pipeline.started_at)} %td .controls.hidden-xs.pull-right - - artifacts = commit.builds.latest.select { |b| b.artifacts? } + - artifacts = pipeline.builds.latest.select { |b| b.artifacts? } - if artifacts.present? .dropdown.inline.build-artifacts %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} @@ -63,9 +63,9 @@ %span #{build.name} - if can?(current_user, :update_pipeline, @project) - - if commit.retryable? - = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do + - if pipeline.retryable? + = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do = icon("repeat") - - if commit.cancelable? - = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do + - if pipeline.cancelable? + = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do = icon("remove") diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index 92d7bed045..bcb8d0fc50 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -11,7 +11,7 @@ module SharedBuilds step 'project has a recent build' do @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha, ref: 'master') - @build = create(:ci_build_with_coverage, commit: @ci_commit) + @build = create(:ci_build_with_coverage, pipeline: @ci_commit) end step 'recent build is successful' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 04cf15641d..03bc8b7055 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -443,7 +443,7 @@ describe API::API, api: true do it "enables merge when build succeeds if the ci is active" do allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) - allow(ci_commit).to receive(:active?).and_return(true) + allow(pipeline).to receive(:active?).and_return(true) put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 10e3631f5f..11316d89eb 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -85,7 +85,7 @@ describe Ci::API::API do trigger = FactoryGirl.create(:ci_trigger, project: project) commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: commit, trigger: trigger) + trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) commit.create_builds(nil, trigger_request) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") From 72947148e54cda37723adad4cad73311e0c926d7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Jun 2016 12:17:37 +0200 Subject: [PATCH 271/507] Don't install knapsack --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d8d8557a46..ac522160d6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,7 +23,6 @@ before_script: - cp config/gitlab.yml.example config/gitlab.yml - bundle --version - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' - - retry gem install knapsack - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' stages: From 8c3ba8d6c9021f250fb1597f6b597d817af46b38 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 6 Jun 2016 13:16:30 +0200 Subject: [PATCH 272/507] Add workhorse controller and API helpers --- app/controllers/application_controller.rb | 1 + app/controllers/projects/avatars_controller.rb | 5 +---- app/controllers/projects/raw_controller.rb | 5 +---- .../projects/repositories_controller.rb | 3 +-- app/helpers/workhorse_helper.rb | 17 +++++++++++++++++ db/schema.rb | 1 + lib/api/helpers.rb | 10 ++++++++++ lib/api/repositories.rb | 10 +++------- lib/gitlab/workhorse.rb | 8 ++++---- .../controllers/projects/raw_controller_spec.rb | 2 ++ .../projects/repositories_controller_spec.rb | 5 +++-- spec/lib/gitlab/workhorse_spec.rb | 2 +- 12 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 app/helpers/workhorse_helper.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 62f6370179..cd6ae507cf 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base include Gitlab::GonHelper include GitlabRoutingHelper include PageLayoutHelper + include WorkhorseHelper before_action :authenticate_user_from_token! before_action :authenticate_user! diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 72921b3aa1..5962f74c39 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -10,10 +10,7 @@ class Projects::AvatarsController < Projects::ApplicationController return if cached_blob? - headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) - headers['Content-Disposition'] = 'inline' - headers['Content-Type'] = safe_content_type(@blob) - head :ok # 'render nothing: true' messes up the Content-Type + send_git_blob @repository, @blob else render_404 end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 10de0e6053..10d24da16d 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -18,10 +18,7 @@ class Projects::RawController < Projects::ApplicationController if @blob.lfs_pointer? send_lfs_object else - headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) - headers['Content-Disposition'] = 'inline' - headers['Content-Type'] = safe_content_type(@blob) - head :ok # 'render nothing: true' messes up the Content-Type + send_git_blob @repository, @blob end else render_404 diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index bb7a6b6a5a..d5af0341d1 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -11,8 +11,7 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format])) - head :ok + send_git_archive @repository, ref: params[:ref], format: params[:format] rescue => ex logger.error("#{self.class.name}: #{ex}") return git_not_found! diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb new file mode 100644 index 0000000000..9d306c9096 --- /dev/null +++ b/app/helpers/workhorse_helper.rb @@ -0,0 +1,17 @@ +# Helpers to send Git blobs or archives through Workhorse. +# Workhorse will also serve files when using `send_file`. +module WorkhorseHelper + # Send a Git blob through Workhorse + def send_git_blob(repository, blob) + headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob)) + headers['Content-Disposition'] = 'inline' + headers['Content-Type'] = safe_content_type(blob) + head :ok # 'render nothing: true' messes up the Content-Type + end + + # Archive a Git repository and send it through Workhorse + def send_git_archive(repository, ref:, format:) + headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) + head :ok + end +end diff --git a/db/schema.rb b/db/schema.rb index 9b991f347a..659ddc6df0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,6 +12,7 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 20160530150109) do + # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "pg_trgm" diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2aaa0557ea..0e47bb0b8a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -408,5 +408,15 @@ module API error!(errors[:access_level], 422) if errors[:access_level].any? not_found!(errors) end + + def send_git_blob(repository, blob) + env['api.format'] = :txt + content_type 'text/plain' + header *Gitlab::Workhorse.send_git_blob(repository, blob) + end + + def send_git_archive(repository, ref:, format:) + header *Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format) + end end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 9cb14e95eb..f55aceed92 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -56,8 +56,7 @@ module API blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath]) not_found! "File" unless blob - content_type 'text/plain' - header(*Gitlab::Workhorse.send_git_blob(repo, blob)) + send_git_blob repo, blob end # Get a raw blob contents by blob sha @@ -80,10 +79,7 @@ module API not_found! 'Blob' unless blob - env['api.format'] = :txt - - content_type blob.mime_type - header(*Gitlab::Workhorse.send_git_blob(repo, blob)) + send_git_blob repo, blob end # Get a an archive of the repository @@ -98,7 +94,7 @@ module API authorize! :download_code, user_project begin - header(*Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format])) + send_git_archive user_project.repository, ref: params[:sha], format: params[:format] rescue not_found!('File') end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index c3ddd4c268..96e99dc008 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -18,10 +18,10 @@ module Gitlab ] end - def send_git_archive(project, ref, format) + def send_git_archive(repository, ref:, format:) format ||= 'tar.gz' format.downcase! - params = project.repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format) + params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format) raise "Repository or ref not found" if params.empty? [ @@ -29,9 +29,9 @@ module Gitlab "git-archive:#{encode(params)}", ] end - + protected - + def encode(hash) Base64.urlsafe_encode64(JSON.dump(hash)) end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index fb29274c68..33c35161da 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -17,6 +17,7 @@ describe Projects::RawController do expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') expect(response.header['Content-Disposition']). to eq("inline") + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:") end end @@ -31,6 +32,7 @@ describe Projects::RawController do expect(response.status).to eq(200) expect(response.header['Content-Type']).to eq('image/jpeg') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:") end end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 0ddbec9eac..aad62cf20e 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -20,10 +20,11 @@ describe Projects::RepositoriesController do project.team << [user, :developer] sign_in(user) end - it "uses Gitlab::Workhorse" do - expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip") + it "uses Gitlab::Workhorse" do get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") end context "when the service raises an error" do diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index d940bf0506..c5c1402e8f 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::Workhorse, lib: true do end it "raises an error" do - expect { subject.send_git_archive(project, "master", "zip") }.to raise_error(RuntimeError) + expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError) end end end From ae011aff63bd6b20043d8ee533f07a65db0b75ff Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 6 Jun 2016 13:35:26 +0200 Subject: [PATCH 273/507] Add changelog item --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 72fe32a01a..4594d9d554 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -63,6 +63,7 @@ v 8.8.3 - Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392 - Fix missing number on generated ordered list element. !4437 - Prevent disclosure of notes on confidential issues in search results. + - Add workhorse controller and API helpers v 8.8.2 - Added remove due date button. !4209 From 3855e33cf8fe95d6de83b18698a958095fa49df5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 6 Jun 2016 15:34:50 +0200 Subject: [PATCH 274/507] Move changelog item --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4594d9d554..03e7e5b1e5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,6 +38,7 @@ v 8.9.0 (unreleased) - Improve error handling importing projects - Put project Files and Commits tabs under Code tab - Replace Colorize with Rainbow for coloring console output in Rake tasks. + - Add workhorse controller and API helpers v 8.8.4 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds @@ -63,7 +64,6 @@ v 8.8.3 - Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392 - Fix missing number on generated ordered list element. !4437 - Prevent disclosure of notes on confidential issues in search results. - - Add workhorse controller and API helpers v 8.8.2 - Added remove due date button. !4209 From 8dc1fa0d8dc9106ab95a3039723e9013c4eed00c Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Sat, 4 Jun 2016 00:16:13 +0300 Subject: [PATCH 275/507] Updated MR notes. --- app/assets/javascripts/awards_handler.coffee | 24 +++++++++---------- app/assets/javascripts/dispatcher.js.coffee | 4 ++-- .../lib/emoji_aliases.js.coffee.erb | 2 +- app/assets/javascripts/notes.js.coffee | 4 ++-- config/routes.rb | 1 + .../javascripts/awards_handler_spec.js.coffee | 8 +++---- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 4f4009e6db..efa8f6cd01 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -2,7 +2,7 @@ class @AwardsHandler constructor: -> - @aliases = emojiAliases() + @aliases = gl.emojiAliases() $(document) .off 'click', '.js-add-award' @@ -172,7 +172,7 @@ class @AwardsHandler decrementCounter: ($emojiButton, emoji) -> - counter = $('.js-counter', $emojiButton) + counter = $ '.js-counter', $emojiButton counterNumber = parseInt counter.text(), 10 if counterNumber > 1 @@ -218,9 +218,7 @@ class @AwardsHandler awardBlock .closest '.js-emoji-btn' .removeData 'original-title' - .removeData 'title' .attr 'data-original-title', newAuthors - .attr 'data-title', newAuthors @resetTooltip awardBlock @@ -258,8 +256,8 @@ class @AwardsHandler " $emojiButton = $ buttonHtml - emoji_node = $emojiButton - .insertBefore votesBlock.find '.js-award-holder:not(.js-award-action-btn)' + $emojiButton + .insertBefore votesBlock.find '.js-award-holder' .find '.emoji-icon' .data 'emoji', emoji @@ -281,21 +279,21 @@ class @AwardsHandler if $('.emoji-menu').length return @createEmoji_ votesBlock, emoji - @createEmojiMenu @getAwardMenuUrl(), => @createEmoji votesBlock, emoji + @createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji - getAwardMenuUrl: -> return gl.awardMenuUrl or '/emojis' + getAwardMenuUrl: -> return gl.awardMenuUrl resolveNameToCssClass: (emoji) -> - emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") + emojiIcon = $ ".emoji-menu-content [data-emoji='#{emoji}']" - if emoji_icon.length > 0 - unicodeName = emoji_icon.data('unicode-name') + if emojiIcon.length > 0 + unicodeName = emojiIcon.data 'unicode-name' else # Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') + unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data 'unicode-name' return "emoji-#{unicodeName}" @@ -303,7 +301,7 @@ class @AwardsHandler postEmoji: (awardUrl, emoji, callback) -> $.post awardUrl, { name: emoji }, (data) -> - callback.call() if data.ok + callback() if data.ok findEmojiIcon: (votesBlock, emoji) -> diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index bae67a2eba..ec54006045 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -23,7 +23,7 @@ class Dispatcher new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() - window.awardsHandler = new AwardsHandler() + gl.awardsHandler = new AwardsHandler() when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() when 'dashboard:todos:index' @@ -54,7 +54,7 @@ class Dispatcher new Diff() shortcut_handler = new ShortcutsIssuable(true) new ZenMode() - window.awardsHandler = new AwardsHandler() + gl.awardsHandler = new AwardsHandler() when "projects:merge_requests:diffs" new Diff() new ZenMode() diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb index 97be65116e..80f9936b9c 100644 --- a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb +++ b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb @@ -1,2 +1,2 @@ -window.emojiAliases = -> +gl.emojiAliases = -> JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 0fdbe55ea9..8e33e915ba 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -168,8 +168,8 @@ class @Notes if note.award votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar votesBlock, note.name - awardsHandler.scrollToAwards() + gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name + gl.awardsHandler.scrollToAwards() # render note if it not present in loaded list # or skip if rendered diff --git a/config/routes.rb b/config/routes.rb index 1fc7985136..9b54b29284 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -757,6 +757,7 @@ Rails.application.routes.draw do resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do member do + post :toggle_award_emoji delete :delete_attachment end end diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee index 5a95ae52dc..0bd6d69638 100644 --- a/spec/javascripts/awards_handler_spec.js.coffee +++ b/spec/javascripts/awards_handler_spec.js.coffee @@ -3,10 +3,10 @@ #= require jquery.cookie #= require ./fixtures/emoji_menu -awardsHandler = null -window.gl or= {} -window.gl.awardMenuUrl = '/emojis' -window.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } +awardsHandler = null +window.gl or= {} +gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } +gl.awardMenuUrl = '/emojis' lazyAssert = (done, assertFn) -> From 13d4231eb9f577456b420ff5044c55584add9b39 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Jun 2016 16:46:18 +0200 Subject: [PATCH 276/507] Use `@ci_commit` in merge_request_controller --- app/controllers/projects/merge_requests_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 0de3442088..e96e816bcd 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -317,8 +317,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff - @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses if @pipeline + @ci_commit = @merge_request.pipeline + @statuses = @ci_commit.statuses if @ci_commit if @merge_request.locked_long_ago? @merge_request.unlock_mr @@ -327,8 +327,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_widget_vars - @pipeline = @merge_request.pipeline - @pipelines = [@pipeline].compact + @ci_commit = @merge_request.pipeline + @ci_commits = [@ci_commit].compact closes_issues end From 028592eba6485e574fb0fb42937519b9e8ace37d Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 6 Jun 2016 17:59:20 +0300 Subject: [PATCH 277/507] Use warning color merge conflicts icon in MR status widget. --- app/assets/stylesheets/pages/merge_requests.scss | 5 +++++ .../projects/merge_requests/widget/open/_conflicts.html.haml | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 8046e203a9..9ddb216d94 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -105,6 +105,11 @@ font-size: 17px; margin: 5px 0; color: $gl-gray-dark; + + &.has-conflicts .fa-exclamation-triangle { + color: $gl-warning; + } + } p:last-child { diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml index e6c089fefb..06ab0a3fa0 100644 --- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml +++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml @@ -1,9 +1,9 @@ -%h4 +%h4.has-conflicts = icon("exclamation-triangle") This merge request contains merge conflicts %p - Please resolve these conflicts or + Please resolve these conflicts or - if @merge_request.can_be_merged_by?(current_user) #{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}. - else From 659d5d48303c4550a2c4ef7cc938d17b5b45d4c9 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 3 Jun 2016 15:30:38 -0300 Subject: [PATCH 278/507] Disable Webhooks before proceeding with the GitHub import --- lib/gitlab/github_import/hook_formatter.rb | 23 +++++++ lib/gitlab/github_import/importer.rb | 26 ++++++-- .../github_import/hook_formatter_spec.rb | 65 +++++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 lib/gitlab/github_import/hook_formatter.rb create mode 100644 spec/lib/gitlab/github_import/hook_formatter_spec.rb diff --git a/lib/gitlab/github_import/hook_formatter.rb b/lib/gitlab/github_import/hook_formatter.rb new file mode 100644 index 0000000000..db1fabaa18 --- /dev/null +++ b/lib/gitlab/github_import/hook_formatter.rb @@ -0,0 +1,23 @@ +module Gitlab + module GithubImport + class HookFormatter + EVENTS = %w[* create delete pull_request push].freeze + + attr_reader :raw + + delegate :id, :name, :active, to: :raw + + def initialize(raw) + @raw = raw + end + + def config + raw.config.attrs + end + + def valid? + (EVENTS & raw.events).any? && active + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 9d077e79c3..860ced5474 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -69,6 +69,9 @@ module Gitlab end def import_pull_requests + hooks = client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?) + disable_webhooks(hooks) + pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc) .map { |raw| PullRequestFormatter.new(project, raw) } .select(&:valid?) @@ -77,7 +80,7 @@ module Gitlab 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) + restore_branches(branches_removed) pull_requests.each do |pull_request| merge_request = MergeRequest.new(pull_request.attributes) @@ -93,10 +96,25 @@ module Gitlab rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message ensure - delete_refs(branches_removed) + clean_up_restored_branches(branches_removed) + clean_up_disabled_webhooks(hooks) end - def create_refs(branches) + def disable_webhooks(hooks) + update_webhooks(hooks, active: false) + end + + def clean_up_disabled_webhooks(hooks) + update_webhooks(hooks, active: true) + end + + def update_webhooks(hooks, options) + hooks.each do |hook| + client.edit_hook(repo, hook.id, hook.name, hook.config, options) + end + end + + def restore_branches(branches) branches.each do |name, sha| client.create_ref(repo, "refs/heads/#{name}", sha) end @@ -104,7 +122,7 @@ module Gitlab project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') end - def delete_refs(branches) + def clean_up_restored_branches(branches) branches.each do |name, _| client.delete_ref(repo, "heads/#{name}") project.repository.rm_branch(project.creator, name) diff --git a/spec/lib/gitlab/github_import/hook_formatter_spec.rb b/spec/lib/gitlab/github_import/hook_formatter_spec.rb new file mode 100644 index 0000000000..110ba42825 --- /dev/null +++ b/spec/lib/gitlab/github_import/hook_formatter_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::HookFormatter, lib: true do + describe '#id' do + it 'returns raw id' do + raw = double(id: 100000) + formatter = described_class.new(raw) + expect(formatter.id).to eq 100000 + end + end + + describe '#name' do + it 'returns raw id' do + raw = double(name: 'web') + formatter = described_class.new(raw) + expect(formatter.name).to eq 'web' + end + end + + describe '#config' do + it 'returns raw config.attrs' do + raw = double(config: double(attrs: { url: 'http://something.com/webhook' })) + formatter = described_class.new(raw) + expect(formatter.config).to eq({ url: 'http://something.com/webhook' }) + end + end + + describe '#valid?' do + it 'returns true when events contains the wildcard event' do + raw = double(events: ['*', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq true + end + + it 'returns true when events contains the create event' do + raw = double(events: ['create', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq true + end + + it 'returns true when events contains delete event' do + raw = double(events: ['delete', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq true + end + + it 'returns true when events contains pull_request event' do + raw = double(events: ['pull_request', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq true + end + + it 'returns false when events does not contains branch related events' do + raw = double(events: ['member', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq false + end + + it 'returns false when hook is not active' do + raw = double(events: ['pull_request', 'commit_comment'], active: false) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq false + end + end +end From 17152602b05021b90261fc8f82024e323724d458 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 3 Jun 2016 15:31:34 -0300 Subject: [PATCH 279/507] Add a message warning user that Webhooks will be disabled --- app/views/import/github/status.html.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 5b7f11440c..6c4a9d68d1 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -4,6 +4,10 @@ %i.fa.fa-github Import projects from GitHub +%p + %i.fa.fa-warning + To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process. + %p.light Select projects you want to import. %hr From 1fb1e64fdd3c608f265a59607c748ba4a999d928 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 6 Jun 2016 11:28:20 -0300 Subject: [PATCH 280/507] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index fe9b9bec86..e44ec02395 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ v 8.8.4 (unreleased) - Fix todos page throwing errors when you have a project pending deletion - Reduce number of SQL queries when rendering user references - Upgrade to jQuery 2 + - Disable Webhooks before proceeding with the GitHub import v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 From 9437b8a2e4ae4c688272d0febfbca4007232e4f5 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 18 May 2016 16:14:20 -0500 Subject: [PATCH 281/507] Import GitHub repositories respecting the API rate limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While Octokit auto pagination set the page size to the maximum 100, and seek to not overstep the rate limit. When the rate limit is reached its raises an exception, and stop doing new requests. Here we use a custom pattern for traversing large lists, so we can check if we’ll reach the rate limit and wait the API to reset the rate limit before making new requests. --- lib/gitlab/github_import/importer.rb | 112 ++++++++++++------ lib/gitlab/github_import/issue_formatter.rb | 4 + lib/gitlab/github_import/label_formatter.rb | 4 + .../github_import/milestone_formatter.rb | 4 + .../github_import/pull_request_formatter.rb | 4 + 5 files changed, 92 insertions(+), 36 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 9d077e79c3..a2ee56bee8 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,6 +3,9 @@ module Gitlab class Importer include Gitlab::ShellAdapter + GITHUB_SAFE_REMAINING_REQUESTS = 100 + GITHUB_SAFE_SLEEP_TIME = 500 + attr_reader :client, :project, :repo, :repo_url def initialize(project) @@ -25,14 +28,53 @@ module Gitlab private + def turn_auto_pagination_off! + client.auto_paginate = false + end + + def turn_auto_pagination_on! + client.auto_paginate = true + end + + def rate_limit + client.rate_limit! + end + + def rate_limit_exceed? + rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS + end + + def rate_limit_sleep_time + rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME + end + + def paginate + turn_auto_pagination_off! + + sleep rate_limit_sleep_time if rate_limit_exceed? + + data = yield + + last_response = client.last_response + + while last_response.rels[:next] + sleep rate_limit_sleep_time if rate_limit_exceed? + last_response = last_response.rels[:next].get + data.concat(last_response.data) if last_response.data.is_a?(Array) + end + + turn_auto_pagination_on! + + data + end + def credentials @credentials ||= project.import_data.credentials if project.import_data end def import_labels - client.labels(repo).each do |raw_data| - Label.create!(LabelFormatter.new(project, raw_data).attributes) - end + labels = paginate { client.labels(repo, per_page: 100) } + labels.each { |raw| LabelFormatter.new(project, raw).create! } true rescue ActiveRecord::RecordInvalid => e @@ -40,9 +82,8 @@ module Gitlab end def import_milestones - client.list_milestones(repo, state: :all).each do |raw_data| - Milestone.create!(MilestoneFormatter.new(project, raw_data).attributes) - end + milestones = paginate { client.milestones(repo, state: :all, per_page: 100) } + milestones.each { |raw| MilestoneFormatter.new(project, raw).create! } true rescue ActiveRecord::RecordInvalid => e @@ -50,16 +91,15 @@ module Gitlab end def import_issues - client.list_issues(repo, state: :all, sort: :created, direction: :asc).each do |raw_data| - gh_issue = IssueFormatter.new(project, raw_data) + data = paginate { client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) } + + data.each do |raw| + gh_issue = IssueFormatter.new(project, raw) if gh_issue.valid? - issue = Issue.create!(gh_issue.attributes) - apply_labels(gh_issue.number, issue) - - if gh_issue.has_comments? - import_comments(gh_issue.number, issue) - end + issue = gh_issue.create! + apply_labels(issue) + import_comments(issue) if gh_issue.has_comments? end end @@ -69,9 +109,8 @@ module Gitlab end def import_pull_requests - pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc) - .map { |raw| PullRequestFormatter.new(project, raw) } - .select(&:valid?) + pull_requests = paginate { client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) } + pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?) 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] } @@ -80,13 +119,10 @@ module Gitlab create_refs(branches_removed) 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 + merge_request = pull_request.create! + apply_labels(merge_request) + import_comments(merge_request) + import_comments_on_diff(merge_request) end true @@ -98,6 +134,7 @@ module Gitlab def create_refs(branches) branches.each do |name, sha| + sleep rate_limit_sleep_time if rate_limit_exceed? client.create_ref(repo, "refs/heads/#{name}", sha) end @@ -106,13 +143,16 @@ module Gitlab def delete_refs(branches) branches.each do |name, _| + sleep rate_limit_sleep_time if rate_limit_exceed? client.delete_ref(repo, "heads/#{name}") project.repository.rm_branch(project.creator, name) end end - def apply_labels(number, issuable) - issue = client.issue(repo, number) + def apply_labels(issuable) + sleep rate_limit_sleep_time if rate_limit_exceed? + + issue = client.issue(repo, issuable.iid) if issue.labels.count > 0 label_ids = issue.labels.map do |raw| @@ -123,20 +163,20 @@ module Gitlab end end - def import_comments(issue_number, noteable) - comments = client.issue_comments(repo, issue_number) - create_comments(comments, noteable) + def import_comments(issuable) + comments = paginate { client.issue_comments(repo, issuable.iid, per_page: 100) } + create_comments(issuable, comments) end - def import_comments_on_diff(pull_request_number, merge_request) - comments = client.pull_request_comments(repo, pull_request_number) - create_comments(comments, merge_request) + def import_comments_on_diff(merge_request) + comments = paginate { client.pull_request_comments(repo, merge_request.iid, per_page: 100) } + create_comments(merge_request, comments) end - def create_comments(comments, noteable) - comments.each do |raw_data| - comment = CommentFormatter.new(project, raw_data) - noteable.notes.create!(comment.attributes) + def create_comments(issuable, comments) + comments.each do |raw| + comment = CommentFormatter.new(project, raw) + issuable.notes.create!(comment.attributes) end end diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index c8173913b4..47f625efb3 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -16,6 +16,10 @@ module Gitlab } end + def create! + Issue.create!(self.attributes) + end + def has_comments? raw_data.comments > 0 end diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb index c2b9d40b51..87b51a0a17 100644 --- a/lib/gitlab/github_import/label_formatter.rb +++ b/lib/gitlab/github_import/label_formatter.rb @@ -9,6 +9,10 @@ module Gitlab } end + def create! + Label.create!(self.attributes) + end + private def color diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index e91a7e328c..a0d2e47c41 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -14,6 +14,10 @@ module Gitlab } end + def create! + Milestone.create!(self.attributes) + end + private def number diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index a2947b56ad..0d21c49035 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -24,6 +24,10 @@ module Gitlab } end + def create! + MergeRequest.create!(self.attributes) + end + def number raw_data.number end From 7c072c76fc05f7db51e7d7ba6cb0fad2850ecf85 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 6 Jun 2016 12:19:03 -0300 Subject: [PATCH 282/507] Fix importer for GitHub comments on diff Fix comments on diff after LegacyDiffNote was extracted from Note --- CHANGELOG | 1 + lib/gitlab/github_import/comment_formatter.rb | 5 +++++ spec/lib/gitlab/github_import/comment_formatter_spec.rb | 2 ++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index fe9b9bec86..9012f1363b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ v 8.8.4 (unreleased) - Fix todos page throwing errors when you have a project pending deletion - Reduce number of SQL queries when rendering user references - Upgrade to jQuery 2 + - Fix importer for GitHub comments on diff v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb index 7d679eaec6..2c1b94ef2c 100644 --- a/lib/gitlab/github_import/comment_formatter.rb +++ b/lib/gitlab/github_import/comment_formatter.rb @@ -8,6 +8,7 @@ module Gitlab commit_id: raw_data.commit_id, line_code: line_code, author_id: author_id, + type: type, created_at: raw_data.created_at, updated_at: raw_data.updated_at } @@ -53,6 +54,10 @@ module Gitlab def note formatter.author_line(author) + body end + + def type + 'LegacyDiffNote' if on_diff? + end end end end diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb index 55e86d4cea..9ae02a6c45 100644 --- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb @@ -29,6 +29,7 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do commit_id: nil, line_code: nil, author_id: project.creator_id, + type: nil, created_at: created_at, updated_at: updated_at } @@ -56,6 +57,7 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_4_3', author_id: project.creator_id, + type: 'LegacyDiffNote', created_at: created_at, updated_at: updated_at } From 07f49626d01ddffcd127e937c528b74b8248043b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 6 Jun 2016 17:40:26 +0200 Subject: [PATCH 283/507] Fix tests --- lib/gitlab/auth.rb | 42 +++++++++---------- .../{rate_limiter.rb => ip_rate_limiter.rb} | 0 spec/requests/jwt_controller_spec.rb | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) rename lib/gitlab/auth/{rate_limiter.rb => ip_rate_limiter.rb} (100%) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index bd129d7216..076e2af7d3 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -35,6 +35,27 @@ module Gitlab end end + def rate_limit!(ip, success:, login:) + rate_limiter = Gitlab::Auth::IpRateLimiter.new(ip) + return unless rate_limiter.enabled? + + if success + # Repeated login 'failures' are normal behavior for some Git clients so + # it is important to reset the ban counter once the client has proven + # they are not a 'bad guy'. + rate_limiter.reset! + else + # Register a login failure so that Rack::Attack can block the next + # request from this IP if needed. + rate_limiter.register_fail! + + if rate_limiter.banned? + Rails.logger.info "IP #{ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end + end + private def valid_ci_request?(login, password, project) @@ -61,27 +82,6 @@ module Gitlab token && token.accessible? && User.find_by(id: token.resource_owner_id) end end - - def rate_limit!(ip, success:, login:) - rate_limiter = IpRateLimiter.new(ip) - return unless rate_limiter.enabled? - - if success - # Repeated login 'failures' are normal behavior for some Git clients so - # it is important to reset the ban counter once the client has proven - # they are not a 'bad guy'. - rate_limiter.reset! - else - # Register a login failure so that Rack::Attack can block the next - # request from this IP if needed. - rate_limiter.register_fail!(ip, config) - - if rate_limiter.banned? - Rails.logger.info "IP #{ip} failed to login " \ - "as #{login} but has been temporarily banned from Git auth" - end - end - end end end end diff --git a/lib/gitlab/auth/rate_limiter.rb b/lib/gitlab/auth/ip_rate_limiter.rb similarity index 100% rename from lib/gitlab/auth/rate_limiter.rb rename to lib/gitlab/auth/ip_rate_limiter.rb diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index d006ff195c..c995993a85 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -44,7 +44,7 @@ describe JwtController do let(:user) { create(:user) } let(:headers) { { authorization: credentials('user', 'password') } } - before { expect_any_instance_of(Gitlab::Auth).to receive(:find).with('user', 'password').and_return(user) } + before { expect(Gitlab::Auth).to receive(:find_in_gitlab_or_ldap).with('user', 'password').and_return(user) } subject! { get '/jwt/auth', parameters, headers } From ff7c4e588ab4f7a397963d43becbe00d1bb584a1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 6 Jun 2016 17:40:30 +0200 Subject: [PATCH 284/507] Remove code duplication in JwtController --- app/controllers/jwt_controller.rb | 40 +------------------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index c05a55633b..131a16dad9 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -42,46 +42,8 @@ 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.find_in_gitlab_or_ldap(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" - return - end - end - end - + Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login) user end end From 0dba294f99e5ec672b4a8a97b5d51ac79a3efc8b Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 6 Jun 2016 18:50:38 +0300 Subject: [PATCH 285/507] Favor the ternary operator. :police_car: --- app/helpers/milestones_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 2abb0e9b17..f6a8ae3fd6 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -56,7 +56,7 @@ module MilestonesHelper def milestone_remaining_days(milestone, withContentTag = true) if milestone.expired? - if withContentTag then content_tag(:strong, 'expired') else 'expired' end + withContentTag ? content_tag(:strong, 'expired') : 'expired' elsif milestone.due_date days = milestone.remaining_days From 535d11302e73fe88702f7c65effc3cd443bf56fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Mon, 6 Jun 2016 12:01:50 -0400 Subject: [PATCH 286/507] Remove prev/next buttons on issues and merge requests The buttons were rarely used and added at least 1 query each on every page load. --- CHANGELOG | 1 + .../javascripts/shortcuts_issuable.coffee | 18 ------------------ app/helpers/issuables_helper.rb | 12 ------------ app/views/shared/issuable/_sidebar.html.haml | 15 --------------- features/project/issues/issues.feature | 7 ------- features/project/merge_requests.feature | 2 -- features/steps/shared/issuable.rb | 16 ---------------- 7 files changed, 1 insertion(+), 70 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fe9b9bec86..47e37152c1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ v 8.8.4 (unreleased) - Fix todos page throwing errors when you have a project pending deletion - Reduce number of SQL queries when rendering user references - Upgrade to jQuery 2 + - Remove prev/next buttons on issues and merge requests v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index ccb42ab216..c93bcf3cee 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation @replyWithSelectedText() return false ) - Mousetrap.bind('j', => - @prevIssue() - return false - ) - Mousetrap.bind('k', => - @nextIssue() - return false - ) Mousetrap.bind('e', => @editIssue() return false @@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation else @enabledHelp.push('.hidden-shortcut.issues') - prevIssue: -> - $prevBtn = $('.prev-btn') - if not $prevBtn.hasClass('disabled') - Turbolinks.visit($prevBtn.attr('href')) - - nextIssue: -> - $nextBtn = $('.next-btn') - if not $nextBtn.hasClass('disabled') - Turbolinks.visit($nextBtn.attr('href')) - replyWithSelectedText: -> if window.getSelection selected = window.getSelection().toString() diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 37b93f6314..40d8ce8a1d 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -8,14 +8,6 @@ module IssuablesHelper "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}" end - def issuables_count(issuable) - base_issuable_scope(issuable).maximum(:iid) - end - - def next_issuable_for(issuable) - base_issuable_scope(issuable).where('iid > ?', issuable.iid).last - end - def multi_label_name(current_labels, default_label) # current_labels may be a string from before if current_labels.is_a?(Array) @@ -45,10 +37,6 @@ module IssuablesHelper end end - def prev_issuable_for(issuable) - base_issuable_scope(issuable).where('iid < ?', issuable.iid).first - end - def user_dropdown_label(user_id, default_label) return default_label if user_id.nil? return "Unassigned" if user_id == "0" diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index d6552ae7f1..1ec2436c83 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -2,23 +2,8 @@ .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header - %span.issuable-count.hide-collapsed.pull-left - = issuable.iid - of - = issuables_count(issuable) %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'} = sidebar_gutter_toggle_icon - .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'} - - if prev_issuable = prev_issuable_for(issuable) - = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager' - - else - %a.btn.btn-default.issuable-pager.disabled{href: '#'} - Prev - - if next_issuable = next_issuable_for(issuable) - = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager' - - else - %a.btn.btn-default.issuable-pager.disabled{href: '#'} - Next = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| .block.assignee diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index de7e2b3772..2259b7125c 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -25,13 +25,6 @@ Feature: Project Issues Scenario: I visit issue page Given I click link "Release 0.4" Then I should see issue "Release 0.4" - And I should see "1 of 2" in the sidebar - - Scenario: I navigate between issues - Given I click link "Release 0.4" - Then I click link "Next" in the sidebar - Then I should see issue "Tweet control" - And I should see "2 of 2" in the sidebar @javascript Scenario: I filter by author diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index ecda4ea824..396eb7cc11 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -49,14 +49,12 @@ Feature: Project Merge Requests Scenario: I visit an open merge request page Given I click link "Bug NS-04" Then I should see merge request "Bug NS-04" - And I should see "1 of 1" in the sidebar Scenario: I visit a merged merge request page Given project "Shop" have "Feature NS-05" merged merge request And I click link "Merged" And I click link "Feature NS-05" Then I should see merge request "Feature NS-05" - And I should see "3 of 3" in the sidebar Scenario: I close merge request page Given I click link "Bug NS-04" diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index 733e80b727..c6572cf386 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -138,22 +138,6 @@ module SharedIssuable end end - step 'I should see "1 of 1" in the sidebar' do - expect_sidebar_content('1 of 1') - end - - step 'I should see "1 of 2" in the sidebar' do - expect_sidebar_content('1 of 2') - end - - step 'I should see "2 of 2" in the sidebar' do - expect_sidebar_content('2 of 2') - end - - step 'I should see "3 of 3" in the sidebar' do - expect_sidebar_content('3 of 3') - end - step 'I click link "Next" in the sidebar' do page.within '.issuable-sidebar' do click_link 'Next' From 9264203103bbd2b4f46ce777304f210b07765c43 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 6 Jun 2016 11:30:17 -0300 Subject: [PATCH 287/507] change add_concurrent_index function arguments --- lib/gitlab/database/migration_helpers.rb | 10 +++------- spec/lib/gitlab/database/migration_helpers_spec.rb | 13 ++++++++++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index b88e50748f..978c3f7896 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -11,7 +11,7 @@ module Gitlab # add_concurrent_index :users, :some_column # # See Rails' `add_index` for more info on the available arguments. - def add_concurrent_index(*args) + def add_concurrent_index(table_name, column_name, options = {}) if transaction_open? raise 'add_concurrent_index can not be run inside a transaction, ' \ 'you can disable transactions by calling disable_ddl_transaction! ' \ @@ -19,14 +19,10 @@ module Gitlab end if Database.postgresql? - if args[2].present? - args[2].merge!({ algorithm: :concurrently }) - else - args << { algorithm: :concurrently } - end + options = options.merge({ algorithm: :concurrently }) end - add_index(*args) + add_index(table_name, column_name, options) end # Updates the value of a column in batches. diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 35ade7a2be..83ddabe6b0 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -16,14 +16,21 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end context 'using PostgreSQL' do - it 'creates the index concurrently' do - expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) } + it 'creates the index concurrently' do expect(model).to receive(:add_index). with(:users, :foo, algorithm: :concurrently) model.add_concurrent_index(:users, :foo) end + + it 'creates unique index concurrently' do + expect(model).to receive(:add_index). + with(:users, :foo, { algorithm: :concurrently, unique: true }) + + model.add_concurrent_index(:users, :foo, unique: true) + end end context 'using MySQL' do @@ -31,7 +38,7 @@ describe Gitlab::Database::MigrationHelpers, lib: true do expect(Gitlab::Database).to receive(:postgresql?).and_return(false) expect(model).to receive(:add_index). - with(:users, :foo) + with(:users, :foo, {}) model.add_concurrent_index(:users, :foo) end From 0e2f26dd2a10ed876f96b0496dff2de6780eeaea Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 2 May 2016 18:19:46 -0500 Subject: [PATCH 288/507] Prioritize labels functionality --- app/assets/javascripts/LabelManager.js.coffee | 79 +++++++++++++++++++ app/assets/javascripts/dispatcher.js.coffee | 2 + app/assets/stylesheets/pages/labels.scss | 12 +++ app/controllers/projects/labels_controller.rb | 22 +++++- app/models/label.rb | 2 + app/views/projects/labels/_label.html.haml | 12 ++- app/views/projects/labels/index.html.haml | 27 ++++--- config/routes.rb | 2 + 8 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/LabelManager.js.coffee diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee new file mode 100644 index 0000000000..056a03651b --- /dev/null +++ b/app/assets/javascripts/LabelManager.js.coffee @@ -0,0 +1,79 @@ +class @LabelManager + constructor: (opts = {}) -> + # Defaults + { + @togglePriorityButton = $('.js-toggle-priority') + @prioritizedLabels = $('.js-prioritized-labels') + @otherLabels = $('.js-other-labels') + } = opts + + @prioritizedLabels.sortable( + items: 'li' + update: @onPrioritySortUpdate.bind(@) + ) + + @bindEvents() + + bindEvents: -> + @togglePriorityButton.on 'click', @, @onTogglePriorityClick + + onTogglePriorityClick: (e) -> + e.preventDefault() + _this = e.data + $btn = $(e.currentTarget) + $label = $("##{$btn.data('domId')}") + action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add' + _this.toggleLabelPriority($label, action) + + toggleLabelPriority: ($label, action, pasive = false) -> + _this = @ + url = $label.find('.js-toggle-priority').data 'url' + + $target = @prioritizedLabels + $from = @otherLabels + + # Optimistic update + if action is 'remove' + $target = @otherLabels + $from = @prioritizedLabels + + if $from.find('li').length is 1 + $from.find('.empty-message').show() + + if not $target.find('li').length + $target.find('.empty-message').hide() + + $label.detach().appendTo($target) + + # Return if we are not persisting state + return if pasive + + xhr = $.post url + + # If request fails, put label back to Other labels group + xhr.fail -> + _this.toggleLabelPriority($label, 'remove', true) + + # Show a message + new Flash('Unable to update label prioritization at this time' , 'alert') + + onPrioritySortUpdate: -> + @savePrioritySort() + + savePrioritySort: -> + xhr = $.post + url: @prioritizedLabels.data('url') + data: + label_ids: @getSortedLabelsIds() + + xhr.done -> + console.log 'done' + + xhr.fail -> + console.log 'fail' + + getSortedLabelsIds: -> + sortedIds = [] + @prioritizedLabels.find('li').each -> + sortedIds.push $(@).data 'id' + sortedIds \ No newline at end of file diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index ec54006045..49fc7ef2e1 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -100,6 +100,8 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'projects:labels:new', 'projects:labels:edit' new Labels() + when 'projects:labels:index' + new LabelManager() when 'projects:network:show' # Ensure we don't create a particular shortcut handler here. This is # already created, where the network graph is created. diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index e179bdf004..4b0b512db8 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -138,3 +138,15 @@ } } } + +.prioritized-labels { + .add-priority { + display: none; + } +} + +.other-labels { + .remove-priority { + display: none; + } +} diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index ff771ea6d9..88d745e6ba 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -11,7 +11,8 @@ class Projects::LabelsController < Projects::ApplicationController respond_to :js, :html def index - @labels = @project.labels.page(params[:page]) + @labels = @project.labels.prioritized(false).page(params[:page]) + @prioritized = @project.labels.prioritized respond_to do |format| format.html @@ -71,6 +72,25 @@ class Projects::LabelsController < Projects::ApplicationController end end + def toggle_priority + priority = label.priority + + respond_to do |format| + if label.update_attributes(priority: !priority) + format.json { render json: label } + else + message = label.errors.full_messages.uniq.join('. ') + format.json { render json: { message: message }, status: :unprocessable_entity } + end + end + end + + def set_sorting + respond_to do |format| + format.json { render json: {message: 'success'}} + end + end + protected def module_enabled diff --git a/app/models/label.rb b/app/models/label.rb index e5ad11983b..59e7afe53f 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -19,6 +19,7 @@ class Label < ActiveRecord::Base validates :color, color: true, allow_blank: false validates :project, presence: true, unless: Proc.new { |service| service.template? } + validates :priority, presence: false, default: false # Don't allow '?', '&', and ',' for label titles validates :title, @@ -29,6 +30,7 @@ class Label < ActiveRecord::Base default_scope { order(title: :asc) } scope :templates, -> { where(template: true) } + scope :prioritized, ->(value = true) { where(priority: value) } alias_attribute :name, :title diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 294fec422c..eda75b64e7 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,4 +1,12 @@ -%li{ id: dom_id(label), data: { id: label.id } } +- label_css_id = dom_id(label) +%li{id: label_css_id, :"data-id" => label.id} + %a.js-toggle-priority{:href => "#", + :"data-url" => toggle_priority_namespace_project_label_path(@project.namespace, @project, label), + :"data-dom-id" => "#{label_css_id}" } + %span.add-priority + (+) + %span.remove-priority + (-) = render "shared/label_row", label: label .pull-info-right %span.append-right-20 @@ -24,4 +32,4 @@ - if current_user :javascript - new Subscription('##{dom_id(label)} .label-subscription'); + new Subscription('##{label_css_id} .label-subscription'); diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 2557d1a4d5..d71db7545e 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -10,13 +10,22 @@ New label .labels - - if @labels.present? - %ul.content-list.manage-labels-list - = render @labels - = paginate @labels, theme: 'gitlab' - - else - .nothing-here-block - - if can? current_user, :admin_label, @project - Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. + .prioritized-labels + %h5 Prioritized Label + %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_sorting_namespace_project_labels_path(@project.namespace, @project) } + - if @prioritized.present? + = render @prioritized - else - No labels created + %p.empty-message No prioritized labels yet + .other-labels + %h5 Other Labels + - if @labels.present? + %ul.content-list.manage-labels-list.js-other-labels + = render @labels + = paginate @labels, theme: 'gitlab' + - else + .nothing-here-block + - if can? current_user, :admin_label, @project + Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. + - else + No labels created diff --git a/config/routes.rb b/config/routes.rb index 7e735541f7..5aa8a0fe8a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -719,10 +719,12 @@ Rails.application.routes.draw do resources :labels, constraints: { id: /\d+/ } do collection do post :generate + post :set_sorting end member do post :toggle_subscription + post :toggle_priority end end From d8263b285193d9163089683eb77825f1cd673b14 Mon Sep 17 00:00:00 2001 From: Thijs Wouters Date: Mon, 14 Mar 2016 10:46:26 +0100 Subject: [PATCH 289/507] Sort by label priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/controllers/projects/labels_controller.rb | 2 +- app/finders/issuable_finder.rb | 8 +++++-- app/helpers/sorting_helper.rb | 11 ++++++++- app/models/concerns/issuable.rb | 9 +++++++- app/models/label.rb | 23 +++++++++++++++++++ app/views/projects/labels/_form.html.haml | 4 ++++ app/views/shared/_sort_dropdown.html.haml | 2 ++ config/initializers/nulls_last.rb | 15 ++++++++++++ .../20160314094147_add_priority_to_label.rb | 6 +++++ db/schema.rb | 2 ++ 10 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 config/initializers/nulls_last.rb create mode 100644 db/migrate/20160314094147_add_priority_to_label.rb diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 88d745e6ba..0a60a80243 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -100,7 +100,7 @@ class Projects::LabelsController < Projects::ApplicationController end def label_params - params.require(:label).permit(:title, :description, :color) + params.require(:label).permit(:title, :description, :color, :priority) end def label diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 7d8c56f4c2..68ab6e8768 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -224,7 +224,7 @@ class IssuableFinder def sort(items) # Ensure we always have an explicit sort order (instead of inheriting # multiple orders when combining ActiveRecord::Relation objects). - params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc) + params[:sort] ? items.sort(params[:sort], label_names) : items.reorder(id: :desc) end def by_assignee(items) @@ -318,7 +318,11 @@ class IssuableFinder end def label_names - params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name] + if labels? + params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name] + else + [] + end end def current_user_related? diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 630e10ea89..d86f1999f5 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -14,7 +14,8 @@ module SortingHelper sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, sort_value_downvotes => sort_title_downvotes, - sort_value_upvotes => sort_title_upvotes + sort_value_upvotes => sort_title_upvotes, + sort_value_priority => sort_title_priority } end @@ -28,6 +29,10 @@ module SortingHelper } end + def sort_title_priority + 'Priority' + end + def sort_title_oldest_updated 'Oldest updated' end @@ -84,6 +89,10 @@ module SortingHelper 'Most popular' end + def sort_value_priority + 'priority' + end + def sort_value_oldest_updated 'updated_asc' end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5d279ae602..8871cb8a6c 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -48,6 +48,12 @@ module Issuable scope :non_archived, -> { join_project.where(projects: { archived: false }) } + def self.order_priority(labels) + select("#{table_name}.*, (#{Label.high_priority(name, table_name, labels).to_sql}) AS highest_priority") + .group("#{table_name}.id") + .reorder(nulls_last('highest_priority', 'ASC')) + end + delegate :name, :email, to: :author, @@ -105,12 +111,13 @@ module Issuable where(t[:title].matches(pattern).or(t[:description].matches(pattern))) end - def sort(method) + def sort(method, labels = []) case method.to_s when 'milestone_due_asc' then order_milestone_due_asc when 'milestone_due_desc' then order_milestone_due_desc when 'downvotes_desc' then order_downvotes_desc when 'upvotes_desc' then order_upvotes_desc + when 'priority' then order_priority(labels) else order_by(method) end diff --git a/app/models/label.rb b/app/models/label.rb index 59e7afe53f..4437ca393e 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -27,11 +27,28 @@ class Label < ActiveRecord::Base format: { with: /\A[^&\?,]+\z/ }, uniqueness: { scope: :project_id } + before_save :nillify_priority + default_scope { order(title: :asc) } scope :templates, -> { where(template: true) } scope :prioritized, ->(value = true) { where(priority: value) } + def self.high_priority(name, table_name, labels) + unfiltered = unscoped + .select("MIN(labels.priority)") + .joins("INNER JOIN label_links ON label_links.label_id = labels.id") + .where("label_links.target_type = '#{name}'") + .where("label_links.target_id = #{table_name}.id") + .where("labels.project_id = #{table_name}.project_id") + + if labels.empty? + unfiltered + else + unfiltered.where("labels.title NOT IN (?)", labels) + end + end + alias_attribute :name, :title def self.reference_prefix @@ -120,4 +137,10 @@ class Label < ActiveRecord::Base id end end + + def nillify_priority + unless self.priority.present? + self.priority = nil + end + end end diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index aa143e54ff..227ce5d231 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -24,6 +24,10 @@ - suggested_colors.each do |color| = link_to '#', style: "background-color: #{color}", data: { color: color } do   + .form-group + = f.label :priority, "Priority", class: 'control-label' + .col-sm-10 + = f.text_field :priority, class: "form-control" .form-actions - if @label.persisted? diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 1e0f075b30..249bce926c 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -8,6 +8,8 @@ %b.caret %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li + = link_to page_filter_path(sort: sort_value_priority) do + = sort_title_priority = link_to page_filter_path(sort: sort_value_recently_created) do = sort_title_recently_created = link_to page_filter_path(sort: sort_value_oldest_created) do diff --git a/config/initializers/nulls_last.rb b/config/initializers/nulls_last.rb new file mode 100644 index 0000000000..47b7b0bb3d --- /dev/null +++ b/config/initializers/nulls_last.rb @@ -0,0 +1,15 @@ +module ActiveRecord + class Base + def self.nulls_last(field, direction = 'ASC') + if Gitlab::Database.postgresql? + "#{field} #{direction} NULLS LAST" + else + if direction.upcase == 'ASC' + "-#{field} DESC" + else + "#{field} DESC" + end + end + end + end +end diff --git a/db/migrate/20160314094147_add_priority_to_label.rb b/db/migrate/20160314094147_add_priority_to_label.rb new file mode 100644 index 0000000000..8ddf778297 --- /dev/null +++ b/db/migrate/20160314094147_add_priority_to_label.rb @@ -0,0 +1,6 @@ +class AddPriorityToLabel < ActiveRecord::Migration + def change + add_column :labels, :priority, :integer + add_index :labels, :priority + end +end diff --git a/db/schema.rb b/db/schema.rb index 9b991f347a..69e37470de 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -496,8 +496,10 @@ ActiveRecord::Schema.define(version: 20160530150109) do t.datetime "updated_at" t.boolean "template", default: false t.string "description" + t.integer "priority" end + add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| From 499bb9f305e78d0e3488c2eee6328ce76af39920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 13 May 2016 17:26:18 +0200 Subject: [PATCH 290/507] Improve Issuable.order_labels_priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/assets/javascripts/LabelManager.js.coffee | 21 +++++++------ app/controllers/projects/labels_controller.rb | 15 ++++++---- app/finders/issuable_finder.rb | 2 +- app/models/concerns/issuable.rb | 30 ++++++++++++++----- app/models/issue.rb | 4 +-- app/models/label.rb | 21 +++---------- app/views/projects/labels/_label.html.haml | 2 +- config/initializers/nulls_last.rb | 15 ---------- config/routes.rb | 2 +- lib/gitlab/database.rb | 14 +++++++++ spec/lib/gitlab/database_spec.rb | 16 ++++++++++ 11 files changed, 83 insertions(+), 59 deletions(-) delete mode 100644 config/initializers/nulls_last.rb diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee index 056a03651b..ad2acb9d0b 100644 --- a/app/assets/javascripts/LabelManager.js.coffee +++ b/app/assets/javascripts/LabelManager.js.coffee @@ -25,7 +25,7 @@ class @LabelManager action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add' _this.toggleLabelPriority($label, action) - toggleLabelPriority: ($label, action, pasive = false) -> + toggleLabelPriority: ($label, action, persistState = false) -> _this = @ url = $label.find('.js-toggle-priority').data 'url' @@ -46,16 +46,19 @@ class @LabelManager $label.detach().appendTo($target) # Return if we are not persisting state - return if pasive + return if persistState - xhr = $.post url + if action is 'remove' + xhr = $.ajax url: url, type: 'DELETE' - # If request fails, put label back to Other labels group - xhr.fail -> - _this.toggleLabelPriority($label, 'remove', true) + # If request fails, put label back to Other labels group + xhr.fail -> + _this.toggleLabelPriority($label, 'remove', true) - # Show a message - new Flash('Unable to update label prioritization at this time' , 'alert') + # Show a message + new Flash('Unable to update label prioritization at this time' , 'alert') + else + @savePrioritySort() onPrioritySortUpdate: -> @savePrioritySort() @@ -76,4 +79,4 @@ class @LabelManager sortedIds = [] @prioritizedLabels.find('li').each -> sortedIds.push $(@).data 'id' - sortedIds \ No newline at end of file + sortedIds diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 0a60a80243..bd46a81ff1 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -72,11 +72,9 @@ class Projects::LabelsController < Projects::ApplicationController end end - def toggle_priority - priority = label.priority - + def remove_priority respond_to do |format| - if label.update_attributes(priority: !priority) + if label.update_attribute(:priority, nil) format.json { render json: label } else message = label.errors.full_messages.uniq.join('. ') @@ -86,8 +84,15 @@ class Projects::LabelsController < Projects::ApplicationController end def set_sorting + Label.transaction do + params[:label_ids].each_with_index do |label_id, index| + label = @project.labels.find_by_id(label_id) + label.update_attribute(:priority, index) if label + end + end + respond_to do |format| - format.json { render json: {message: 'success'}} + format.json { render json: { message: 'success' } } end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 68ab6e8768..a0932712bd 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -224,7 +224,7 @@ class IssuableFinder def sort(items) # Ensure we always have an explicit sort order (instead of inheriting # multiple orders when combining ActiveRecord::Relation objects). - params[:sort] ? items.sort(params[:sort], label_names) : items.reorder(id: :desc) + params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc) end def by_assignee(items) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 8871cb8a6c..92526a9914 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -48,12 +48,6 @@ module Issuable scope :non_archived, -> { join_project.where(projects: { archived: false }) } - def self.order_priority(labels) - select("#{table_name}.*, (#{Label.high_priority(name, table_name, labels).to_sql}) AS highest_priority") - .group("#{table_name}.id") - .reorder(nulls_last('highest_priority', 'ASC')) - end - delegate :name, :email, to: :author, @@ -111,18 +105,24 @@ module Issuable where(t[:title].matches(pattern).or(t[:description].matches(pattern))) end - def sort(method, labels = []) + def sort(method, excluded_labels: []) case method.to_s when 'milestone_due_asc' then order_milestone_due_asc when 'milestone_due_desc' then order_milestone_due_desc when 'downvotes_desc' then order_downvotes_desc when 'upvotes_desc' then order_upvotes_desc - when 'priority' then order_priority(labels) + when 'priority' then order_labels_priority(excluded_labels: excluded_labels) else order_by(method) end end + def order_labels_priority(excluded_labels: []) + select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority"). + group(arel_table[:id]). + reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) + end + def with_label(title, sort = nil) if title.is_a?(Array) && title.size > 1 joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}") @@ -146,6 +146,20 @@ module Issuable grouping_columns end + + private + + def highest_label_priority(excluded_labels) + query = Label.select(Label.arel_table[:priority].minimum). + joins(:label_links). + where(label_links: { target_type: name }). + where("label_links.target_id = #{table_name}.id"). + reorder(nil) + + query.where.not(title: excluded_labels) if excluded_labels.present? + + query + end end def today? diff --git a/app/models/issue.rb b/app/models/issue.rb index bd0fbc96d1..235922710a 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -75,10 +75,10 @@ class Issue < ActiveRecord::Base @link_reference_pattern ||= super("issues", /(?\d+)/) end - def self.sort(method) + def self.sort(method, excluded_labels: []) case method.to_s when 'due_date_asc' then order_due_date_asc - when 'due_date_desc' then order_due_date_desc + when 'due_date_desc' then order_due_date_desc else super end diff --git a/app/models/label.rb b/app/models/label.rb index 4437ca393e..7fd7788055 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -19,7 +19,6 @@ class Label < ActiveRecord::Base validates :color, color: true, allow_blank: false validates :project, presence: true, unless: Proc.new { |service| service.template? } - validates :priority, presence: false, default: false # Don't allow '?', '&', and ',' for label titles validates :title, @@ -32,21 +31,11 @@ class Label < ActiveRecord::Base default_scope { order(title: :asc) } scope :templates, -> { where(template: true) } - scope :prioritized, ->(value = true) { where(priority: value) } - def self.high_priority(name, table_name, labels) - unfiltered = unscoped - .select("MIN(labels.priority)") - .joins("INNER JOIN label_links ON label_links.label_id = labels.id") - .where("label_links.target_type = '#{name}'") - .where("label_links.target_id = #{table_name}.id") - .where("labels.project_id = #{table_name}.project_id") + def self.prioritized(bool = true) + query = bool ? where.not(priority: nil) : where(priority: nil) - if labels.empty? - unfiltered - else - unfiltered.where("labels.title NOT IN (?)", labels) - end + query.reorder(Gitlab::Database.nulls_last_order(:priority), :title) end alias_attribute :name, :title @@ -139,8 +128,6 @@ class Label < ActiveRecord::Base end def nillify_priority - unless self.priority.present? - self.priority = nil - end + self.priority = nil if priority.blank? end end diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index eda75b64e7..a22dee1a62 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,7 +1,7 @@ - label_css_id = dom_id(label) %li{id: label_css_id, :"data-id" => label.id} %a.js-toggle-priority{:href => "#", - :"data-url" => toggle_priority_namespace_project_label_path(@project.namespace, @project, label), + :"data-url" => remove_priority_namespace_project_label_path(@project.namespace, @project, label), :"data-dom-id" => "#{label_css_id}" } %span.add-priority (+) diff --git a/config/initializers/nulls_last.rb b/config/initializers/nulls_last.rb deleted file mode 100644 index 47b7b0bb3d..0000000000 --- a/config/initializers/nulls_last.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - class Base - def self.nulls_last(field, direction = 'ASC') - if Gitlab::Database.postgresql? - "#{field} #{direction} NULLS LAST" - else - if direction.upcase == 'ASC' - "-#{field} DESC" - else - "#{field} DESC" - end - end - end - end -end diff --git a/config/routes.rb b/config/routes.rb index 5aa8a0fe8a..93ff825d7b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -724,7 +724,7 @@ Rails.application.routes.draw do member do post :toggle_subscription - post :toggle_priority + delete :remove_priority end end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 42bec913a4..04fa6a3a5d 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -16,6 +16,20 @@ module Gitlab database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] end + def self.nulls_last_order(field, direction = 'ASC') + order = "#{field} #{direction}" + + if Gitlab::Database.postgresql? + order << ' NULLS LAST' + else + # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL + # columns. In the (default) ascending order, `0` comes first. + order.prepend("#{field} IS NULL, ") if direction == 'ASC' + end + + order + end + def true_value if Gitlab::Database.postgresql? "'t'" diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index d0a447753b..3031559c61 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -39,6 +39,22 @@ describe Gitlab::Database, lib: true do end end + describe '.nulls_last_order' do + context 'when using PostgreSQL' do + before { expect(described_class).to receive(:postgresql?).and_return(true) } + + it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column ASC NULLS LAST'} + it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC NULLS LAST'} + end + + context 'when using MySQL' do + before { expect(described_class).to receive(:postgresql?).and_return(false) } + + it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column IS NULL, column ASC'} + it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC'} + end + end + describe '#true_value' do it 'returns correct value for PostgreSQL' do expect(described_class).to receive(:postgresql?).and_return(true) From 25ee66e88cc6a3a32dea9a38157b3ee5709c5f9e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 19 May 2016 00:46:09 -0500 Subject: [PATCH 291/507] Sorting improvements and styling --- app/assets/javascripts/LabelManager.js.coffee | 2 ++ app/assets/stylesheets/framework/lists.scss | 12 +++++++++++ app/assets/stylesheets/pages/labels.scss | 21 ++++++++++++++++++- app/views/projects/labels/_label.html.haml | 7 ------- app/views/projects/labels/index.html.haml | 2 +- app/views/shared/_label_row.html.haml | 7 +++++++ 6 files changed, 42 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee index ad2acb9d0b..7c2e4ff46c 100644 --- a/app/assets/javascripts/LabelManager.js.coffee +++ b/app/assets/javascripts/LabelManager.js.coffee @@ -9,6 +9,8 @@ class @LabelManager @prioritizedLabels.sortable( items: 'li' + placeholder: 'list-placeholder' + axis: 'y' update: @onPrioritySortUpdate.bind(@) ) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index b17c8bcbb1..96e7aa4fb1 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -141,6 +141,18 @@ ul.content-list { padding: 10px 14px; } } + + // When dragging a list item + &.ui-sortable-helper { + border-bottom: none; + } + + &.list-placeholder { + background-color: $gray-light; + border: dotted 1px $gray-dark; + margin: 1px 0; + min-height: 30px; + } } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 4b0b512db8..2cd9d74b2d 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -51,7 +51,7 @@ .label-row { .label-name { display: inline-block; - width: 200px; + width: 170px; @media (max-width: $screen-xs-min) { display: block; @@ -140,8 +140,11 @@ } .prioritized-labels { + margin-bottom: 30px; + .add-priority { display: none; + color: $gray-light; } } @@ -150,3 +153,19 @@ display: none; } } + +.toggle-priority { + display: inline-block; + vertical-align: middle; + + button { + border-color: transparent; + padding: 5px 8px; + vertical-align: top; + font-size: 14px; + + &:hover { + border-color: transparent; + } + } +} diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index a22dee1a62..a637d64267 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,12 +1,5 @@ - label_css_id = dom_id(label) %li{id: label_css_id, :"data-id" => label.id} - %a.js-toggle-priority{:href => "#", - :"data-url" => remove_priority_namespace_project_label_path(@project.namespace, @project, label), - :"data-dom-id" => "#{label_css_id}" } - %span.add-priority - (+) - %span.remove-priority - (-) = render "shared/label_row", label: label .pull-info-right %span.append-right-20 diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index d71db7545e..325c50abaf 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -10,7 +10,7 @@ New label .labels - .prioritized-labels + .prioritized-labels{ class: ('hide' if params[:page].present?)} %h5 Prioritized Label %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_sorting_namespace_project_labels_path(@project.namespace, @project) } - if @prioritized.present? diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 9ce5562e66..b06a783e98 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,4 +1,11 @@ +- label_css_id = dom_id(label) %span.label-row + .js-toggle-priority.toggle-priority{ :"data-url" => remove_priority_namespace_project_label_path(@project.namespace, @project, label), + :"data-dom-id" => "#{label_css_id}" } + %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } + %i.fa.fa-star-o + %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' } + %i.fa.fa-star %span.label-name = link_to_label(label, tooltip: false) %span.prepend-left-10 From 0960cba0902776a5c41f55f3729260db4562236f Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 19 May 2016 16:21:26 -0500 Subject: [PATCH 292/507] Add tests for label prioritization --- .../labels/update_prioritization_spec.rb | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 spec/features/projects/labels/update_prioritization_spec.rb diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb new file mode 100644 index 0000000000..2fcaed8416 --- /dev/null +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +feature 'Prioritize labels', feature: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, name: 'test', namespace: user.namespace) } + + scenario 'user can prioritize a label', js: true do + bug = create(:label, title: 'bug') + wontfix = create(:label, title: 'wontfix') + + project.labels << bug + project.labels << wontfix + + login_as user + visit namespace_project_labels_path(project.namespace, project) + + expect(page).to have_content('No prioritized labels yet') + + page.within('.other-labels') do + first('.js-toggle-priority').click + wait_for_ajax + expect(page).not_to have_content('bug') + end + + page.within('.prioritized-labels') do + expect(page).not_to have_content('No prioritized labels yet') + expect(page).to have_content('bug') + end + end + + scenario 'user can unprioritize a label', js: true do + bug = create(:label, title: 'bug', priority: 1) + wontfix = create(:label, title: 'wontfix') + + project.labels << bug + project.labels << wontfix + + login_as user + visit namespace_project_labels_path(project.namespace, project) + + expect(page).to have_content('bug') + + page.within('.prioritized-labels') do + first('.js-toggle-priority').click + wait_for_ajax + expect(page).not_to have_content('bug') + end + + page.within('.other-labels') do + expect(page).to have_content('bug') + expect(page).to have_content('wontfix') + end + end + + scenario 'user can sort prioritized labels', js: true do + bug = create(:label, title: 'bug', priority: 1) + wontfix = create(:label, title: 'wontfix', priority: 2) + + project.labels << bug + project.labels << wontfix + + login_as user + visit namespace_project_labels_path(project.namespace, project) + + expect(page).to have_content 'bug' + expect(page).to have_content 'wontfix' + + # Sort labels + find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}") + + page.within('.prioritized-labels') do + expect(first('li')).to have_content('wontfix') + expect(page.all('li').last).to have_content('bug') + end + end +end From ae2fb991b0e56f0f9c232a5c7d1d3255c811092c Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 19 May 2016 16:21:56 -0500 Subject: [PATCH 293/507] Show proper error message when saving priority fails --- app/assets/javascripts/LabelManager.js.coffee | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee index 7c2e4ff46c..8a561ad1a4 100644 --- a/app/assets/javascripts/LabelManager.js.coffee +++ b/app/assets/javascripts/LabelManager.js.coffee @@ -71,11 +71,8 @@ class @LabelManager data: label_ids: @getSortedLabelsIds() - xhr.done -> - console.log 'done' - xhr.fail -> - console.log 'fail' + new Flash('Unable to update label prioritization at this time' , 'alert') getSortedLabelsIds: -> sortedIds = [] From 1648c395e5d65ec7fd5fe8810ac743355bee0f6a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 19 May 2016 16:49:41 -0500 Subject: [PATCH 294/507] Fix syntax --- app/views/projects/labels/_label.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index a637d64267..be367ab456 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -11,16 +11,16 @@ = pluralize label.open_issues_count(current_user), 'open issue' - if current_user - .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} - .subscription-status{data: {status: label_subscription_status(label)}} + .label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + .subscription-status{ data: { status: label_subscription_status(label) } } %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } } %span= label_subscription_toggle_button_text(label) - if can? current_user, :admin_label, @project - = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: {toggle: "tooltip"} do + = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: { toggle: 'tooltip' } do %i.fa.fa-pencil-square-o - = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do + = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: { confirm: 'Remove this label? Are you sure?', toggle: 'tooltip' } do %i.fa.fa-trash-o - if current_user From 91e6bc1258c5e9923482b7829ed38191a2f572d7 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 19 May 2016 19:52:40 -0500 Subject: [PATCH 295/507] Add tests for issue prioritization --- .../labels/issues_sorted_by_priority_spec.rb | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 spec/features/projects/labels/issues_sorted_by_priority_spec.rb diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb new file mode 100644 index 0000000000..097ee4350f --- /dev/null +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +feature 'Issue prioritization', feature: true do + + let(:user) { create(:user) } + let(:project) { create(:project, name: 'test', namespace: user.namespace) } + + # According to https://gitlab.com/gitlab-org/gitlab-ce/issues/14189#note_4360653 + context 'when assigned with prioritized labels' do + scenario 'Are sorted properly' do + login_as user + + label_1 = create(:label, title: 'label_1', priority: 1) + label_2 = create(:label, title: 'label_2', priority: 2) + label_3 = create(:label, title: 'label_3', priority: 3) + label_4 = create(:label, title: 'label_4', priority: 4) + label_5 = create(:label, title: 'label_5') # no priority + + project.labels << label_1 + project.labels << label_2 + project.labels << label_3 + project.labels << label_4 + project.labels << label_5 + + issue_1 = create(:issue, title: 'issue_1', project: project) + issue_2 = create(:issue, title: 'issue_2', project: project) + issue_3 = create(:issue, title: 'issue_3', project: project) + issue_4 = create(:issue, title: 'issue_4', project: project) + issue_5 = create(:issue, title: 'issue_5', project: project) + issue_6 = create(:issue, title: 'issue_6', project: project) + issue_7 = create(:issue, title: 'issue_7', project: project) + issue_8 = create(:issue, title: 'issue_8', project: project) + + # Assign labels to issues disorderly + issue_4.labels << label_1 + issue_3.labels << label_2 + issue_5.labels << label_3 + issue_2.labels << label_4 + issue_1.labels << label_5 + issue_6.labels << label_5 + issue_7.labels << label_5 + issue_8.labels << label_5 + + visit namespace_project_issues_path(project.namespace, project, sort: 'priority') + + # Ensure we are indicating that issues are sorted by priority + expect(page).to have_selector('.dropdown-toggle', text: 'Priority') + + page.within('.issues-list') do + expect(find('> li:nth-of-type(1)')).to have_content('issue_4') + expect(find('> li:nth-of-type(2)')).to have_content('issue_3') + expect(find('> li:nth-of-type(3)')).to have_content('issue_5') + expect(find('> li:nth-of-type(4)')).to have_content('issue_2') + + # the rest should be at the bottom + expect(find('> li:nth-of-type(5)')).to have_content('issue_7') + expect(find('> li:nth-of-type(6)')).to have_content('issue_8') + expect(find('> li:nth-of-type(7)')).to have_content('issue_1') + expect(find('> li:nth-of-type(8)')).to have_content('issue_6') + end + end + end +end From d6cecf7addc00e4cae61d4ecbb5b82e3187d0f44 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 20 May 2016 16:11:29 -0500 Subject: [PATCH 296/507] Fix failing tests --- features/steps/project/issues/labels.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 8d87f6a7a5..e02b57bbf8 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -60,25 +60,25 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see label \'feature\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).to have_content 'feature' end end step 'I should see label \'bug\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).to have_content 'bug' end end step 'I should not see label \'bug\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).not_to have_content 'bug' end end step 'I should see label \'support\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).to have_content 'support' end end @@ -90,7 +90,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see label \'fix\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).to have_content 'fix' end end From 6aea7666c336ba1dffa192249b391164d4b50d36 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 20 May 2016 19:01:01 -0500 Subject: [PATCH 297/507] Add tests for issues with multiple labels --- .../labels/issues_sorted_by_priority_spec.rb | 93 ++++++++++++------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index 097ee4350f..fac70087c0 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -5,23 +5,53 @@ feature 'Issue prioritization', feature: true do let(:user) { create(:user) } let(:project) { create(:project, name: 'test', namespace: user.namespace) } + # Labels + let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) } + let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } + let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) } + let(:label_4) { create(:label, title: 'label_4', project: project, priority: 4) } + let(:label_5) { create(:label, title: 'label_5', project: project) } # no priority + # According to https://gitlab.com/gitlab-org/gitlab-ce/issues/14189#note_4360653 - context 'when assigned with prioritized labels' do + context 'when issues have one label' do scenario 'Are sorted properly' do + + # Issues + issue_1 = create(:issue, title: 'issue_1', project: project) + issue_2 = create(:issue, title: 'issue_2', project: project) + issue_3 = create(:issue, title: 'issue_3', project: project) + issue_4 = create(:issue, title: 'issue_4', project: project) + issue_5 = create(:issue, title: 'issue_5', project: project) + + # Assign labels to issues disorderly + issue_4.labels << label_1 + issue_3.labels << label_2 + issue_5.labels << label_3 + issue_2.labels << label_4 + issue_1.labels << label_5 + login_as user + visit namespace_project_issues_path(project.namespace, project, sort: 'priority') - label_1 = create(:label, title: 'label_1', priority: 1) - label_2 = create(:label, title: 'label_2', priority: 2) - label_3 = create(:label, title: 'label_3', priority: 3) - label_4 = create(:label, title: 'label_4', priority: 4) - label_5 = create(:label, title: 'label_5') # no priority + # Ensure we are indicating that issues are sorted by priority + expect(page).to have_selector('.dropdown-toggle', text: 'Priority') - project.labels << label_1 - project.labels << label_2 - project.labels << label_3 - project.labels << label_4 - project.labels << label_5 + page.within('.issues-holder') do + expect(page).to have_selector('.issues-list > li:nth-of-type(1)', text: 'issue_4') + expect(page).to have_selector('.issues-list > li:nth-of-type(2)', text: 'issue_3') + expect(page).to have_selector('.issues-list > li:nth-of-type(3)', text: 'issue_5') + expect(page).to have_selector('.issues-list > li:nth-of-type(4)', text: 'issue_2') + # the rest should be at the bottom + expect(page).to have_selector('.issues-list > li:nth-of-type(5)', text: 'issue_1') + end + end + end + + context 'when issues have multiple labels' do + scenario 'Are sorted properly' do + + # Issues issue_1 = create(:issue, title: 'issue_1', project: project) issue_2 = create(:issue, title: 'issue_2', project: project) issue_3 = create(:issue, title: 'issue_3', project: project) @@ -32,31 +62,32 @@ feature 'Issue prioritization', feature: true do issue_8 = create(:issue, title: 'issue_8', project: project) # Assign labels to issues disorderly - issue_4.labels << label_1 - issue_3.labels << label_2 - issue_5.labels << label_3 - issue_2.labels << label_4 - issue_1.labels << label_5 - issue_6.labels << label_5 - issue_7.labels << label_5 - issue_8.labels << label_5 + issue_5.labels << label_1 # 1 + issue_5.labels << label_2 + issue_8.labels << label_1 # 2 + issue_1.labels << label_2 # 3 + issue_1.labels << label_3 + issue_3.labels << label_2 # 4 + issue_3.labels << label_4 + issue_7.labels << label_2 # 5 + issue_2.labels << label_3 # 6 + issue_4.labels << label_4 # 7 + issue_6.labels << label_5 # 8 – No priority + login_as user visit namespace_project_issues_path(project.namespace, project, sort: 'priority') - # Ensure we are indicating that issues are sorted by priority expect(page).to have_selector('.dropdown-toggle', text: 'Priority') - page.within('.issues-list') do - expect(find('> li:nth-of-type(1)')).to have_content('issue_4') - expect(find('> li:nth-of-type(2)')).to have_content('issue_3') - expect(find('> li:nth-of-type(3)')).to have_content('issue_5') - expect(find('> li:nth-of-type(4)')).to have_content('issue_2') - - # the rest should be at the bottom - expect(find('> li:nth-of-type(5)')).to have_content('issue_7') - expect(find('> li:nth-of-type(6)')).to have_content('issue_8') - expect(find('> li:nth-of-type(7)')).to have_content('issue_1') - expect(find('> li:nth-of-type(8)')).to have_content('issue_6') + page.within('.issues-holder') do + expect(page).to have_selector('.issues-list > li:nth-of-type(1)', text: 'issue_5') + expect(page).to have_selector('.issues-list > li:nth-of-type(2)', text: 'issue_8') + expect(page).to have_selector('.issues-list > li:nth-of-type(3)', text: 'issue_1') + expect(page).to have_selector('.issues-list > li:nth-of-type(4)', text: 'issue_3') + expect(page).to have_selector('.issues-list > li:nth-of-type(5)', text: 'issue_7') + expect(page).to have_selector('.issues-list > li:nth-of-type(6)', text: 'issue_2') + expect(page).to have_selector('.issues-list > li:nth-of-type(7)', text: 'issue_4') + expect(page).to have_selector('.issues-list > li:nth-of-type(8)', text: 'issue_6') end end end From b28237b4d269e1e191597324769113ee4254d7c3 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 30 May 2016 17:53:08 +0100 Subject: [PATCH 298/507] Don't try to count a relation with aliases Work around ActiveRecord count and column aliases issue as described at: AR doesn't promise that `#count` (which is called by `#any?`) will work for relations using `#select`, which means we need a workaround instead. --- app/views/shared/_issues.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 8ff9d4c1c7..1150614c32 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,4 +1,4 @@ -- if @issues.any? +- if @issues.reorder('').any? - @issues.group_by(&:project).each do |group| .panel.panel-default.panel-small - project = group[0] From 1cc0209a8057c32cb7b073b822410f8b4c4ad3c9 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 1 Jun 2016 13:35:43 +0100 Subject: [PATCH 299/507] Fix style - `reorder(nil)` is better than `reorder('')` - Only use ASCII in comments --- app/views/shared/_issues.html.haml | 2 +- .../features/projects/labels/issues_sorted_by_priority_spec.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 1150614c32..a5df502d7b 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,4 +1,4 @@ -- if @issues.reorder('').any? +- if @issues.reorder(nil).any? - @issues.group_by(&:project).each do |group| .panel.panel-default.panel-small - project = group[0] diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index fac70087c0..453ded8ee7 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' feature 'Issue prioritization', feature: true do @@ -72,7 +73,7 @@ feature 'Issue prioritization', feature: true do issue_7.labels << label_2 # 5 issue_2.labels << label_3 # 6 issue_4.labels << label_4 # 7 - issue_6.labels << label_5 # 8 – No priority + issue_6.labels << label_5 # 8 - No priority login_as user visit namespace_project_issues_path(project.namespace, project, sort: 'priority') From 91b475a9c0ab880c533b4d802afdc2db97ed1abb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 1 Jun 2016 13:18:33 -0500 Subject: [PATCH 300/507] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index fe9b9bec86..8e8daf35ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.9.0 (unreleased) - Bulk assign/unassign labels to issues. + - Ability to prioritize labels !4009 / !3205 (Thijs Wouters) - Allow enabling wiki page events from Webhook management UI - Make EmailsOnPushWorker use Sidekiq mailers queue - Fix wiki page events' webhook to point to the wiki repository From bf0c4426ffb1994b647a4f7e8c74b0fef29645d2 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 2 Jun 2016 14:48:18 -0500 Subject: [PATCH 301/507] Feedback --- app/views/projects/labels/_form.html.haml | 4 ---- app/views/projects/labels/_label.html.haml | 2 +- app/views/projects/labels/index.html.haml | 5 +++-- app/views/shared/_label_row.html.haml | 11 +++++------ 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 227ce5d231..aa143e54ff 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -24,10 +24,6 @@ - suggested_colors.each do |color| = link_to '#', style: "background-color: #{color}", data: { color: color } do   - .form-group - = f.label :priority, "Priority", class: 'control-label' - .col-sm-10 - = f.text_field :priority, class: "form-control" .form-actions - if @label.persisted? diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index be367ab456..b7df0aa36a 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,5 +1,5 @@ - label_css_id = dom_id(label) -%li{id: label_css_id, :"data-id" => label.id} +%li{id: label_css_id, data: {id: label.id } } = render "shared/label_row", label: label .pull-info-right %span.append-right-20 diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 325c50abaf..4b2fb04323 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -10,7 +10,8 @@ New label .labels - .prioritized-labels{ class: ('hide' if params[:page].present?)} + - hide_class = 'hide' if ((params[:page].present? and params[:page] != '1') or @labels.blank?) + .prioritized-labels{ class: hide_class } %h5 Prioritized Label %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_sorting_namespace_project_labels_path(@project.namespace, @project) } - if @prioritized.present? @@ -18,7 +19,7 @@ - else %p.empty-message No prioritized labels yet .other-labels - %h5 Other Labels + %h5{ class: hide_class } Other Labels - if @labels.present? %ul.content-list.manage-labels-list.js-other-labels = render @labels diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index b06a783e98..41d7411f3f 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,12 +1,11 @@ -- label_css_id = dom_id(label) %span.label-row - .js-toggle-priority.toggle-priority{ :"data-url" => remove_priority_namespace_project_label_path(@project.namespace, @project, label), - :"data-dom-id" => "#{label_css_id}" } + .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), + dom_id: dom_id(label) } } %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } - %i.fa.fa-star-o + = icon('star-o') %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' } - %i.fa.fa-star + = icon('star') %span.label-name = link_to_label(label, tooltip: false) %span.prepend-left-10 - = markdown(label.description, pipeline: :single_line) \ No newline at end of file + = markdown(label.description, pipeline: :single_line) From 8f0bf297676dac33ecc8ced10da5ae88f36a9ab0 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 2 Jun 2016 14:57:31 -0500 Subject: [PATCH 302/507] Add unprioritized scope --- app/controllers/projects/labels_controller.rb | 4 ++-- app/models/label.rb | 8 +++++--- app/views/projects/labels/index.html.haml | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index bd46a81ff1..13f634fdd0 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -11,8 +11,8 @@ class Projects::LabelsController < Projects::ApplicationController respond_to :js, :html def index - @labels = @project.labels.prioritized(false).page(params[:page]) - @prioritized = @project.labels.prioritized + @labels = @project.labels.unprioritized.page(params[:page]) + @prioritized_labels = @project.labels.prioritized respond_to do |format| format.html diff --git a/app/models/label.rb b/app/models/label.rb index 7fd7788055..9e04c5263b 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -32,10 +32,12 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } - def self.prioritized(bool = true) - query = bool ? where.not(priority: nil) : where(priority: nil) + def self.prioritized + where.not(priority: nil).reorder(Gitlab::Database.nulls_last_order(:priority), :title) + end - query.reorder(Gitlab::Database.nulls_last_order(:priority), :title) + def self.unprioritized + where(priority: nil).reorder(Gitlab::Database.nulls_last_order(:priority), :title) end alias_attribute :name, :title diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 4b2fb04323..77c42ec356 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -14,8 +14,8 @@ .prioritized-labels{ class: hide_class } %h5 Prioritized Label %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_sorting_namespace_project_labels_path(@project.namespace, @project) } - - if @prioritized.present? - = render @prioritized + - if @prioritized_labels.present? + = render @prioritized_labels - else %p.empty-message No prioritized labels yet .other-labels From f011f038cd6cd1fcb01a18de5774ef7be34d6cef Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 2 Jun 2016 15:21:00 -0500 Subject: [PATCH 303/507] Update method name --- app/models/label.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/label.rb b/app/models/label.rb index 9e04c5263b..e4fc4f251d 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -26,7 +26,7 @@ class Label < ActiveRecord::Base format: { with: /\A[^&\?,]+\z/ }, uniqueness: { scope: :project_id } - before_save :nillify_priority + before_save :nullify_priority default_scope { order(title: :asc) } @@ -129,7 +129,7 @@ class Label < ActiveRecord::Base end end - def nillify_priority + def nullify_priority self.priority = nil if priority.blank? end end From fa6593b226ab4eae267a45716dde65b641532674 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 2 Jun 2016 15:21:15 -0500 Subject: [PATCH 304/507] Update method and route --- app/controllers/projects/labels_controller.rb | 4 ++-- config/routes.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 13f634fdd0..885dd0eb45 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -5,7 +5,7 @@ class Projects::LabelsController < Projects::ApplicationController before_action :label, only: [:edit, :update, :destroy] before_action :authorize_read_label! before_action :authorize_admin_labels!, only: [ - :new, :create, :edit, :update, :generate, :destroy + :new, :create, :edit, :update, :generate, :destroy, :remove_priority ] respond_to :js, :html @@ -83,7 +83,7 @@ class Projects::LabelsController < Projects::ApplicationController end end - def set_sorting + def set_priorities Label.transaction do params[:label_ids].each_with_index do |label_id, index| label = @project.labels.find_by_id(label_id) diff --git a/config/routes.rb b/config/routes.rb index 93ff825d7b..240dcc74b0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -719,7 +719,7 @@ Rails.application.routes.draw do resources :labels, constraints: { id: /\d+/ } do collection do post :generate - post :set_sorting + post :set_priorities end member do From b0bfd789480d52f7783ecd2a9c4ea61c690c83a7 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 2 Jun 2016 15:21:39 -0500 Subject: [PATCH 305/507] Typo --- app/views/projects/labels/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 77c42ec356..3ba6ccd443 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -12,8 +12,8 @@ .labels - hide_class = 'hide' if ((params[:page].present? and params[:page] != '1') or @labels.blank?) .prioritized-labels{ class: hide_class } - %h5 Prioritized Label - %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_sorting_namespace_project_labels_path(@project.namespace, @project) } + %h5 Prioritized Labels + %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } - if @prioritized_labels.present? = render @prioritized_labels - else From 221d2e3aa0f100b9d6da74ebe08dbab35ac5297f Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 2 Jun 2016 16:33:53 -0500 Subject: [PATCH 306/507] Remove unnecesary param --- app/controllers/projects/labels_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 885dd0eb45..ec8e1497d8 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -105,7 +105,7 @@ class Projects::LabelsController < Projects::ApplicationController end def label_params - params.require(:label).permit(:title, :description, :color, :priority) + params.require(:label).permit(:title, :description, :color) end def label From d4bcf51692122abaef1272fe80e8897b5be0f178 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 2 Jun 2016 16:38:55 -0500 Subject: [PATCH 307/507] Show prioritized labels only on the first page and when there's labels created --- app/views/projects/labels/index.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 3ba6ccd443..82fb602983 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -10,7 +10,10 @@ New label .labels - - hide_class = 'hide' if ((params[:page].present? and params[:page] != '1') or @labels.blank?) + - hide_class = '' + -# Only show it in the first page + - if (params[:page].present? and params[:page] != '1') or @project.labels.blank? or (params[:page] == nil and @project.labels.blank?) + - hide_class = 'hide' .prioritized-labels{ class: hide_class } %h5 Prioritized Labels %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } From 4ac907ee77784006b32a6c918792d3a82a0bb5c8 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 2 Jun 2016 18:42:13 -0500 Subject: [PATCH 308/507] typo --- app/models/label.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/label.rb b/app/models/label.rb index e4fc4f251d..aed74d0bde 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -33,7 +33,7 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } def self.prioritized - where.not(priority: nil).reorder(Gitlab::Database.nulls_last_order(:priority), :title) + where.not(priority: nil).reorder(Gitlab::Database.nulls_last_order(:priority), :title) end def self.unprioritized From 8b3ed9c35513a8d8ee576ffdb69630a08c53875b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 3 Jun 2016 01:13:15 -0500 Subject: [PATCH 309/507] add :set_priorities to :authorize_admin_labels! --- app/controllers/projects/labels_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index ec8e1497d8..0ca675623e 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -5,7 +5,7 @@ class Projects::LabelsController < Projects::ApplicationController before_action :label, only: [:edit, :update, :destroy] before_action :authorize_read_label! before_action :authorize_admin_labels!, only: [ - :new, :create, :edit, :update, :generate, :destroy, :remove_priority + :new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities ] respond_to :js, :html From e487b0995cef810fead3f03cce45bc220a6111c2 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 3 Jun 2016 01:15:32 -0500 Subject: [PATCH 310/507] Improve functionality --- app/assets/javascripts/LabelManager.js.coffee | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee index 8a561ad1a4..365a062bb8 100644 --- a/app/assets/javascripts/LabelManager.js.coffee +++ b/app/assets/javascripts/LabelManager.js.coffee @@ -1,4 +1,6 @@ class @LabelManager + errorMessage: 'Unable to update label prioritization at this time' + constructor: (opts = {}) -> # Defaults { @@ -27,7 +29,7 @@ class @LabelManager action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add' _this.toggleLabelPriority($label, action) - toggleLabelPriority: ($label, action, persistState = false) -> + toggleLabelPriority: ($label, action, persistState = true) -> _this = @ url = $label.find('.js-toggle-priority').data 'url' @@ -48,31 +50,32 @@ class @LabelManager $label.detach().appendTo($target) # Return if we are not persisting state - return if persistState + return unless persistState if action is 'remove' xhr = $.ajax url: url, type: 'DELETE' - - # If request fails, put label back to Other labels group - xhr.fail -> - _this.toggleLabelPriority($label, 'remove', true) - - # Show a message - new Flash('Unable to update label prioritization at this time' , 'alert') else - @savePrioritySort() + xhr = @savePrioritySort($label, action) + + xhr.fail @rollbackLabelPosition.bind(@, $label, action) onPrioritySortUpdate: -> - @savePrioritySort() - - savePrioritySort: -> - xhr = $.post - url: @prioritizedLabels.data('url') - data: - label_ids: @getSortedLabelsIds() + xhr = @savePrioritySort() xhr.fail -> - new Flash('Unable to update label prioritization at this time' , 'alert') + new Flash(@errorMessage, 'alert') + + savePrioritySort: () -> + $.post + url: @prioritizedLabels.data('url') + data: + label_ids: @getSortedLabelsIds() + + rollbackLabelPosition: ($label, originalAction)-> + action = if originalAction is 'remove' then 'add' else 'remove' + @toggleLabelPriority($label, action, false) + + new Flash(@errorMessage, 'alert') getSortedLabelsIds: -> sortedIds = [] From b4e4e61184b017ecbe3c3664ba916388947b49d2 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 3 Jun 2016 01:36:09 -0500 Subject: [PATCH 311/507] Show functionality only for users with the ability to edit labels --- app/views/projects/labels/index.html.haml | 26 ++++++++++++----------- app/views/shared/_label_row.html.haml | 13 ++++++------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 82fb602983..0450f51204 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,4 +1,5 @@ - page_title "Labels" +- hide_class = '' .top-area .nav-text @@ -10,19 +11,20 @@ New label .labels - - hide_class = '' - -# Only show it in the first page - - if (params[:page].present? and params[:page] != '1') or @project.labels.blank? or (params[:page] == nil and @project.labels.blank?) - - hide_class = 'hide' - .prioritized-labels{ class: hide_class } - %h5 Prioritized Labels - %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } - - if @prioritized_labels.present? - = render @prioritized_labels - - else - %p.empty-message No prioritized labels yet + - if can?(current_user, :admin_label, @project) + -# Only show it in the first page + - if (params[:page].present? and params[:page] != '1') or @project.labels.blank? or (params[:page] == nil and @project.labels.blank?) + - hide_class = 'hide' + .prioritized-labels{ class: hide_class } + %h5 Prioritized Labels + %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } + - if @prioritized_labels.present? + = render @prioritized_labels + - else + %p.empty-message No prioritized labels yet .other-labels - %h5{ class: hide_class } Other Labels + - if can?(current_user, :admin_label, @project) + %h5{ class: hide_class } Other Labels - if @labels.present? %ul.content-list.manage-labels-list.js-other-labels = render @labels diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 41d7411f3f..c53f32d97e 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,10 +1,11 @@ %span.label-row - .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), - dom_id: dom_id(label) } } - %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } - = icon('star-o') - %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' } - = icon('star') + - if can? current_user, :admin_label, @project + .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), + dom_id: dom_id(label) } } + %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } + = icon('star-o') + %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' } + = icon('star') %span.label-name = link_to_label(label, tooltip: false) %span.prepend-left-10 From a966e6e4512e8073d7193c540fafba3fe86ea228 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 3 Jun 2016 01:37:23 -0500 Subject: [PATCH 312/507] Initialize LabelManager only when necessary --- app/assets/javascripts/dispatcher.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 49fc7ef2e1..5d6ac6e757 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -101,7 +101,7 @@ class Dispatcher when 'projects:labels:new', 'projects:labels:edit' new Labels() when 'projects:labels:index' - new LabelManager() + new LabelManager() if $('.prioritized-labels').length when 'projects:network:show' # Ensure we don't create a particular shortcut handler here. This is # already created, where the network graph is created. From 2ca32a09b68b1b74e8142d6211ffc8f70acf472e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 3 Jun 2016 02:14:39 -0500 Subject: [PATCH 313/507] Add test for the case when user can't prioritize labels --- .../labels/update_prioritization_spec.rb | 130 +++++++++++------- 1 file changed, 80 insertions(+), 50 deletions(-) diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 2fcaed8416..2a0bf6548d 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -3,76 +3,106 @@ require 'spec_helper' feature 'Prioritize labels', feature: true do include WaitForAjax - let(:user) { create(:user) } - let(:project) { create(:project, name: 'test', namespace: user.namespace) } + context 'when project belongs to user' do + let(:user) { create(:user) } + let(:project) { create(:project, name: 'test', namespace: user.namespace) } - scenario 'user can prioritize a label', js: true do - bug = create(:label, title: 'bug') - wontfix = create(:label, title: 'wontfix') + scenario 'user can prioritize a label', js: true do + bug = create(:label, title: 'bug') + wontfix = create(:label, title: 'wontfix') - project.labels << bug - project.labels << wontfix + project.labels << bug + project.labels << wontfix - login_as user - visit namespace_project_labels_path(project.namespace, project) + login_as user + visit namespace_project_labels_path(project.namespace, project) - expect(page).to have_content('No prioritized labels yet') + expect(page).to have_content('No prioritized labels yet') - page.within('.other-labels') do - first('.js-toggle-priority').click - wait_for_ajax - expect(page).not_to have_content('bug') + page.within('.other-labels') do + first('.js-toggle-priority').click + wait_for_ajax + expect(page).not_to have_content('bug') + end + + page.within('.prioritized-labels') do + expect(page).not_to have_content('No prioritized labels yet') + expect(page).to have_content('bug') + end end - page.within('.prioritized-labels') do - expect(page).not_to have_content('No prioritized labels yet') + scenario 'user can unprioritize a label', js: true do + bug = create(:label, title: 'bug', priority: 1) + wontfix = create(:label, title: 'wontfix') + + project.labels << bug + project.labels << wontfix + + login_as user + visit namespace_project_labels_path(project.namespace, project) + expect(page).to have_content('bug') + + page.within('.prioritized-labels') do + first('.js-toggle-priority').click + wait_for_ajax + expect(page).not_to have_content('bug') + end + + page.within('.other-labels') do + expect(page).to have_content('bug') + expect(page).to have_content('wontfix') + end + end + + scenario 'user can sort prioritized labels', js: true do + bug = create(:label, title: 'bug', priority: 1) + wontfix = create(:label, title: 'wontfix', priority: 2) + + project.labels << bug + project.labels << wontfix + + login_as user + visit namespace_project_labels_path(project.namespace, project) + + expect(page).to have_content 'bug' + expect(page).to have_content 'wontfix' + + # Sort labels + find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}") + + page.within('.prioritized-labels') do + expect(first('li')).to have_content('wontfix') + expect(page.all('li').last).to have_content('bug') + end end end - scenario 'user can unprioritize a label', js: true do - bug = create(:label, title: 'bug', priority: 1) - wontfix = create(:label, title: 'wontfix') + context 'as a guest' do + it 'can not prioritize labels' do + user = create(:user) + guest = create(:user) + project = create(:project, name: 'test', namespace: user.namespace) - project.labels << bug - project.labels << wontfix + create(:label, title: 'bug') - login_as user - visit namespace_project_labels_path(project.namespace, project) + login_as guest + visit namespace_project_labels_path(project.namespace, project) - expect(page).to have_content('bug') - - page.within('.prioritized-labels') do - first('.js-toggle-priority').click - wait_for_ajax - expect(page).not_to have_content('bug') - end - - page.within('.other-labels') do - expect(page).to have_content('bug') - expect(page).to have_content('wontfix') + expect(page).not_to have_css('.prioritized-labels') end end - scenario 'user can sort prioritized labels', js: true do - bug = create(:label, title: 'bug', priority: 1) - wontfix = create(:label, title: 'wontfix', priority: 2) + context 'as a non signed in user' do + it 'can not prioritize labels' do + user = create(:user) + project = create(:project, name: 'test', namespace: user.namespace) - project.labels << bug - project.labels << wontfix + create(:label, title: 'bug') - login_as user - visit namespace_project_labels_path(project.namespace, project) + visit namespace_project_labels_path(project.namespace, project) - expect(page).to have_content 'bug' - expect(page).to have_content 'wontfix' - - # Sort labels - find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}") - - page.within('.prioritized-labels') do - expect(first('li')).to have_content('wontfix') - expect(page.all('li').last).to have_content('bug') + expect(page).not_to have_css('.prioritized-labels') end end end From b0ec4b9cc37f30c90d4bceff5e8b8efbac25dedc Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 3 Jun 2016 18:06:23 -0500 Subject: [PATCH 314/507] Remove comment Just to trigger CI --- spec/features/projects/labels/issues_sorted_by_priority_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index 453ded8ee7..323b266599 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -1,4 +1,3 @@ -# coding: utf-8 require 'spec_helper' feature 'Issue prioritization', feature: true do From 77f30af0179164fe3e893df616eb60beb8a1d9fe Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 6 Jun 2016 15:40:35 +0100 Subject: [PATCH 315/507] Tidy up Ruby style in templates --- app/views/projects/labels/index.html.haml | 9 ++++----- app/views/shared/_label_row.html.haml | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 0450f51204..5deb84ad41 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -13,9 +13,8 @@ .labels - if can?(current_user, :admin_label, @project) -# Only show it in the first page - - if (params[:page].present? and params[:page] != '1') or @project.labels.blank? or (params[:page] == nil and @project.labels.blank?) - - hide_class = 'hide' - .prioritized-labels{ class: hide_class } + - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1') + .prioritized-labels{ class: ('hide' if hide) } %h5 Prioritized Labels %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } - if @prioritized_labels.present? @@ -24,14 +23,14 @@ %p.empty-message No prioritized labels yet .other-labels - if can?(current_user, :admin_label, @project) - %h5{ class: hide_class } Other Labels + %h5{ class: ('hide' if hide } Other Labels - if @labels.present? %ul.content-list.manage-labels-list.js-other-labels = render @labels = paginate @labels, theme: 'gitlab' - else .nothing-here-block - - if can? current_user, :admin_label, @project + - if can?(current_user, :admin_label, @project) Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. - else No labels created diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index c53f32d97e..d315a3fe93 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,5 +1,5 @@ %span.label-row - - if can? current_user, :admin_label, @project + - if can?(current_user, :admin_label, @project) .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), dom_id: dom_id(label) } } %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } From bf193eb78bd7022b58d6ceebd928f2b140205f65 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 6 Jun 2016 15:41:33 +0100 Subject: [PATCH 316/507] Remove unnecessary null-specific order --- app/models/label.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/label.rb b/app/models/label.rb index aed74d0bde..9760f1aefa 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -33,11 +33,11 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } def self.prioritized - where.not(priority: nil).reorder(Gitlab::Database.nulls_last_order(:priority), :title) + where.not(priority: nil).reorder(:title) end def self.unprioritized - where(priority: nil).reorder(Gitlab::Database.nulls_last_order(:priority), :title) + where(priority: nil).reorder(:title) end alias_attribute :name, :title From 7bc65b2bef1aaacb0122eabca23e1db0849321b0 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 6 Jun 2016 16:12:32 +0100 Subject: [PATCH 317/507] Clarify issue priority spec We don't care particularly about the ordering within a priority level, just that the levels are in the right order. --- .../labels/issues_sorted_by_priority_spec.rb | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index 323b266599..461f173792 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -37,13 +37,9 @@ feature 'Issue prioritization', feature: true do expect(page).to have_selector('.dropdown-toggle', text: 'Priority') page.within('.issues-holder') do - expect(page).to have_selector('.issues-list > li:nth-of-type(1)', text: 'issue_4') - expect(page).to have_selector('.issues-list > li:nth-of-type(2)', text: 'issue_3') - expect(page).to have_selector('.issues-list > li:nth-of-type(3)', text: 'issue_5') - expect(page).to have_selector('.issues-list > li:nth-of-type(4)', text: 'issue_2') + issue_titles = all('.issues-list .issue-title-text').map(&:text) - # the rest should be at the bottom - expect(page).to have_selector('.issues-list > li:nth-of-type(5)', text: 'issue_1') + expect(issue_titles).to eq(['issue_4', 'issue_3', 'issue_5', 'issue_2', 'issue_1']) end end end @@ -80,14 +76,11 @@ feature 'Issue prioritization', feature: true do expect(page).to have_selector('.dropdown-toggle', text: 'Priority') page.within('.issues-holder') do - expect(page).to have_selector('.issues-list > li:nth-of-type(1)', text: 'issue_5') - expect(page).to have_selector('.issues-list > li:nth-of-type(2)', text: 'issue_8') - expect(page).to have_selector('.issues-list > li:nth-of-type(3)', text: 'issue_1') - expect(page).to have_selector('.issues-list > li:nth-of-type(4)', text: 'issue_3') - expect(page).to have_selector('.issues-list > li:nth-of-type(5)', text: 'issue_7') - expect(page).to have_selector('.issues-list > li:nth-of-type(6)', text: 'issue_2') - expect(page).to have_selector('.issues-list > li:nth-of-type(7)', text: 'issue_4') - expect(page).to have_selector('.issues-list > li:nth-of-type(8)', text: 'issue_6') + issue_titles = all('.issues-list .issue-title-text').map(&:text) + + expect(issue_titles[0..1]).to contain_exactly('issue_5', 'issue_8') + expect(issue_titles[2..4]).to contain_exactly('issue_1', 'issue_3', 'issue_7') + expect(issue_titles[5..-1]).to eq(['issue_2', 'issue_4', 'issue_6']) end end end From a04897b76b00e4a099faf55e30443378928e28e1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 6 Jun 2016 10:59:22 -0500 Subject: [PATCH 318/507] Typo --- app/views/projects/labels/_label.html.haml | 4 ++-- app/views/projects/labels/index.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index b7df0aa36a..1c51ea676c 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,5 +1,5 @@ - label_css_id = dom_id(label) -%li{id: label_css_id, data: {id: label.id } } +%li{id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label .pull-info-right %span.append-right-20 @@ -17,7 +17,7 @@ %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } } %span= label_subscription_toggle_button_text(label) - - if can? current_user, :admin_label, @project + - if can?(current_user, :admin_label, @project) = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: { toggle: 'tooltip' } do %i.fa.fa-pencil-square-o = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: { confirm: 'Remove this label? Are you sure?', toggle: 'tooltip' } do diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 5deb84ad41..c72eddba37 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -5,7 +5,7 @@ .nav-text Labels can be applied to issues and merge requests. .nav-controls - - if can? current_user, :admin_label, @project + - if can?(current_user, :admin_label, @project) = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do = icon('plus') New label @@ -23,7 +23,7 @@ %p.empty-message No prioritized labels yet .other-labels - if can?(current_user, :admin_label, @project) - %h5{ class: ('hide' if hide } Other Labels + %h5{ class: ('hide' if hide) } Other Labels - if @labels.present? %ul.content-list.manage-labels-list.js-other-labels = render @labels From 67c02edccf2dc3d42126dcf4269862c2a1622fff Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 6 Jun 2016 14:00:44 -0300 Subject: [PATCH 319/507] Move create! method from formatters to the BaseFormatter --- lib/gitlab/github_import/base_formatter.rb | 4 ++++ lib/gitlab/github_import/issue_formatter.rb | 8 ++++---- lib/gitlab/github_import/label_formatter.rb | 4 ++-- lib/gitlab/github_import/milestone_formatter.rb | 4 ++-- lib/gitlab/github_import/pull_request_formatter.rb | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 202263c674..72992baffd 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -9,6 +9,10 @@ module Gitlab @formatter = Gitlab::ImportFormatter.new end + def create! + self.klass.create!(self.attributes) + end + private def gl_user_id(github_id) diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index 47f625efb3..835ec858b3 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -16,14 +16,14 @@ module Gitlab } end - def create! - Issue.create!(self.attributes) - end - def has_comments? raw_data.comments > 0 end + def klass + Issue + end + def number raw_data.number end diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb index 87b51a0a17..9f18244e7d 100644 --- a/lib/gitlab/github_import/label_formatter.rb +++ b/lib/gitlab/github_import/label_formatter.rb @@ -9,8 +9,8 @@ module Gitlab } end - def create! - Label.create!(self.attributes) + def klass + Label end private diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index a0d2e47c41..53d4b3102d 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -14,8 +14,8 @@ module Gitlab } end - def create! - Milestone.create!(self.attributes) + def klass + Milestone end private diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 0d21c49035..498b00cb65 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -24,8 +24,8 @@ module Gitlab } end - def create! - MergeRequest.create!(self.attributes) + def klass + MergeRequest end def number From 246410003f2a100547c3abd3d75947946f2fae77 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 6 Jun 2016 13:45:33 -0300 Subject: [PATCH 320/507] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index fe9b9bec86..243dc29b8c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ v 8.8.4 (unreleased) - Fix todos page throwing errors when you have a project pending deletion - Reduce number of SQL queries when rendering user references - Upgrade to jQuery 2 + - Import GitHub repositories respecting the API rate limit v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 From 456a75fb7baa13da26921618b7c7800252a011f7 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 6 Jun 2016 12:11:49 -0600 Subject: [PATCH 321/507] Improve input padding, fix #18237. --- app/assets/stylesheets/framework/forms.scss | 1 + app/assets/stylesheets/framework/selects.scss | 2 +- app/assets/stylesheets/framework/variables.scss | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 46acc3b772..43d5566154 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -76,6 +76,7 @@ label { .form-control { @include box-shadow(none); border-radius: 3px; + padding: $gl-vert-padding $gl-input-padding; } .select-wrapper { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 6efc6ec1e4..f242706ebe 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -8,7 +8,7 @@ background: #fff; border-color: $input-border; height: 35px; - padding: $gl-vert-padding $gl-btn-padding; + padding: $gl-vert-padding $gl-input-padding; font-size: $gl-font-size; line-height: 1.42857143; border-radius: $border-radius-base; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f253da814b..1c2e259a63 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -57,6 +57,7 @@ $code_line_height: 1.5; */ $gl-padding: 16px; $gl-btn-padding: 10px; +$gl-input-padding: 10px; $gl-vert-padding: 6px; $gl-padding-top: 10px; From aebfdcd8513b5513f8631f7e67d7f2900f093278 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Jun 2016 20:19:39 +0200 Subject: [PATCH 322/507] Install bundler --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ac522160d6..7bece719b0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -74,6 +74,7 @@ update-knapsack: - export KNAPSACK_REPORT_PATH=knapsack/${JOB_NAME}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - cp knapsack/${JOB_NAME}_report.json ${KNAPSACK_REPORT_PATH} + - bundle exec gem install bundler - knapsack ${JOB_NAME[0]} artifacts: paths: From d2b7c39a55913a1ba416dae2711059e656a7d10a Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 6 Jun 2016 12:22:59 -0600 Subject: [PATCH 323/507] Add padding to bottom of wiki page, fix #12921. --- app/views/projects/wikis/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 1cb48a1e85..9166c0edb3 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -18,7 +18,7 @@ You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. -.wiki-holder.prepend-top-default +.wiki-holder.prepend-top-default.append-bottom-default .wiki = preserve do = render_wiki_content(@page) From b51b14507c06ed898ae4a63ffda0e84ca40342d3 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sun, 17 Apr 2016 15:35:18 -0600 Subject: [PATCH 324/507] Add License Finder gem. In order to rehost all our gems in our own gem host, we need to have the legal rights to do so for every gem should they be taken down from RubyGems. License Finder automates checking of gems to ensure that we're in the clear legally. Approved the MIT License because it essentially allows us to do "whatever" with those gems. I am not a lawyer. https://github.com/pivotal/LicenseFinder --- Gemfile | 2 ++ Gemfile.lock | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/Gemfile b/Gemfile index 38ff536fd7..51ef0bd1b4 100644 --- a/Gemfile +++ b/Gemfile @@ -306,6 +306,8 @@ group :development, :test do gem 'bundler-audit', require: false gem 'benchmark-ips', require: false + + gem "license_finder", require: false end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 5f1dbd431e..86e102d6fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -366,6 +366,12 @@ GEM actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) + license_finder (2.1.0) + bundler + httparty + rubyzip + thor + xml-simple licensee (8.0.0) rugged (>= 0.24b) listen (3.0.5) @@ -618,6 +624,7 @@ GEM sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) + rubyzip (1.2.0) rufus-scheduler (3.1.10) rugged (0.24.0) safe_yaml (1.0.4) @@ -875,6 +882,7 @@ DEPENDENCIES jwt kaminari (~> 0.17.0) letter_opener_web (~> 1.3.0) + license_finder licensee (~> 8.0.0) loofah (~> 2.0.3) mail_room (~> 0.7) From 9442482d091cd4fabd760797aa9455f96e5abbb6 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sun, 17 Apr 2016 15:56:10 -0600 Subject: [PATCH 325/507] Add some gems' licenses not caught by License Finder. Also add License Finder to CI (only runs on master). --- .gitlab-ci.yml | 5 +++ config/.decisions.yml | 89 +++++++++++++++++++++++++++++++++++++++ config/license_finder.yml | 2 + 3 files changed, 96 insertions(+) create mode 100644 config/.decisions.yml create mode 100644 config/license_finder.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85730e1b68..e4d81ccf35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -93,6 +93,11 @@ scss-lint: script: - bundle exec rake scss_lint +license-finder: + stage: test + script: + - bundle exec license_finder + brakeman: stage: test script: diff --git a/config/.decisions.yml b/config/.decisions.yml new file mode 100644 index 0000000000..7e4a8be6c4 --- /dev/null +++ b/config/.decisions.yml @@ -0,0 +1,89 @@ +--- +- - :whitelist + - MIT + - :who: + :why: + :versions: [] + :when: 2016-04-17 21:12:24.558441000 Z +- - :ignore_group + - development + - :who: + :why: + :versions: [] + :when: 2016-04-17 21:27:01.054140000 Z +- - :ignore_group + - test + - :who: + :why: + :versions: [] + :when: 2016-04-17 21:27:06.250326000 Z +- - :license + - raphael-rails + - MIT + - :who: + :why: https://github.com/mockdeep/raphael-rails/blob/master/license.txt + :versions: [] + :when: 2016-04-17 21:30:07.575392000 Z +- - :license + - rouge + - MIT + - :who: + :why: https://github.com/jneen/rouge/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:31:29.490394000 Z +- - :license + - pyu-ruby-sasl + - MIT + - :who: + :why: https://github.com/pyu10055/ruby-sasl/blob/master/MIT-LICENSE + :versions: [] + :when: 2016-04-17 21:41:55.266420000 Z +- - :license + - six + - MIT + - :who: + :why: https://github.com/randx/six/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:42:31.420186000 Z +- - :license + - rdoc + - GPLv2 + - :who: + :why: https://github.com/rdoc/rdoc/blob/master/LICENSE.rdoc + :versions: [] + :when: 2016-04-17 21:43:30.480413000 Z +- - :license + - rubypants + - unknown + - :who: + :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc + :versions: [] + :when: 2016-04-17 21:44:49.443453000 Z +- - :license + - expression_parser + - MIT + - :who: + :why: https://github.com/nricciar/expression_parser/blob/master/MIT-LICENSE + :versions: [] + :when: 2016-04-17 21:45:41.829912000 Z +- - :license + - ace-rails-ap + - MIT + - :who: + :why: https://github.com/codykrieger/ace-rails-ap/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:46:19.767922000 Z +- - :license + - jquery-scrollto-rails + - MIT, GPLv2 + - :who: + :why: https://github.com/JohnColvin/jquery-scrollto-rails/blob/master/MIT%20License + :versions: [] + :when: 2016-04-17 21:47:56.967946000 Z +- - :license + - creole + - ruby + - :who: + :why: https://github.com/minad/creole#license + :versions: [] + :when: 2016-04-17 21:49:10.329759000 Z diff --git a/config/license_finder.yml b/config/license_finder.yml new file mode 100644 index 0000000000..d641563719 --- /dev/null +++ b/config/license_finder.yml @@ -0,0 +1,2 @@ +--- +decisions_file: './config/.decisions.yml' From ddca2806c754b9be86138dfeddb4b581d9b6a40f Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sun, 1 May 2016 23:59:43 -0600 Subject: [PATCH 326/507] Add gems and licenses that were previously missing. Approve a number of licenses after a bunch of research today. --- Gemfile.lock | 1 + config/.decisions.yml | 123 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 86e102d6fb..53685058fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -796,6 +796,7 @@ GEM builder expression_parser rinku + xml-simple (1.1.5) xpath (2.0.0) nokogiri (~> 1.3) diff --git a/config/.decisions.yml b/config/.decisions.yml index 7e4a8be6c4..9cfc9a186b 100644 --- a/config/.decisions.yml +++ b/config/.decisions.yml @@ -47,18 +47,11 @@ :when: 2016-04-17 21:42:31.420186000 Z - - :license - rdoc - - GPLv2 + - ruby - :who: :why: https://github.com/rdoc/rdoc/blob/master/LICENSE.rdoc :versions: [] :when: 2016-04-17 21:43:30.480413000 Z -- - :license - - rubypants - - unknown - - :who: - :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc - :versions: [] - :when: 2016-04-17 21:44:49.443453000 Z - - :license - expression_parser - MIT @@ -87,3 +80,117 @@ :why: https://github.com/minad/creole#license :versions: [] :when: 2016-04-17 21:49:10.329759000 Z +- - :license + - eventmachine + - ruby + - :who: + :why: https://github.com/eventmachine/eventmachine/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:49:10.329759001 Z +- - :whitelist + - Apache 2.0 + - :who: Connor Shea + :why: http://choosealicense.com/licenses/apache-2.0/ + :versions: [] + :when: 2016-05-02 05:27:43.762702000 Z +- - :blacklist + - GPLv2 + - :who: Connor Shea + :why: GPL-licensed libraries cannot be linked to from non-GPL projects. + :versions: [] + :when: 2016-05-02 05:29:27.637336000 Z +- - :blacklist + - GPLv3 + - :who: Connor Shea + :why: GPL-licensed libraries cannot be linked to from non-GPL projects. + :versions: [] + :when: 2016-05-02 05:29:43.904715000 Z +- - :whitelist + - ruby + - :who: Connor Shea + :why: https://github.com/ruby/ruby/blob/ruby_1_8_6/COPYING + :versions: [] + :when: 2016-05-02 05:31:54.498490000 Z +- - :whitelist + - LGPL + - :who: Connor Shea + :why: http://www.gnu.org/licenses/license-list.html#LGPLv2.1 + :versions: [] + :when: 2016-05-02 05:32:48.645841000 Z +- - :whitelist + - ISC + - :who: Connor Shea + :why: http://www.gnu.org/licenses/license-list.html#ISC + :versions: [] + :when: 2016-05-02 05:42:01.894452000 Z +- - :whitelist + - New BSD + - :who: Connor Shea + :why: https://opensource.org/licenses/BSD-3-Clause + :versions: [] + :when: 2016-05-02 05:44:38.246021000 Z +- - :license + - unicorn + - ruby + - :who: + :why: + :versions: [] + :when: 2016-05-02 05:45:28.817510000 Z +- - :license + - unicorn-worker-killer + - ruby + - :who: + :why: + :versions: [] + :when: 2016-05-02 05:45:38.323867000 Z +- - :license + - json + - ruby + - :who: + :why: + :versions: [] + :when: 2016-05-02 05:50:07.826564000 Z +- - :license + - unf + - BSD + - :who: + :why: + :versions: [] + :when: 2016-05-02 05:51:46.886872000 Z +- - :whitelist + - LGPL-2.1+ + - :who: + :why: Equivalent to LGPL. + :versions: [] + :when: 2016-05-02 05:52:56.303239000 Z +- - :whitelist + - BSD + - :who: + :why: https://opensource.org/licenses/BSD-2-Clause + :versions: [] + :when: 2016-05-02 05:55:09.796363000 Z +- - :license + - rubypants + - MIT + - :who: + :why: + :versions: [] + :when: 2016-05-02 05:56:50.696858000 Z +- - :ignore + - bundler + - :who: + :why: + :versions: [] + :when: 2016-05-02 06:41:31.504230000 Z +- - :heed + - bundler + - :who: + :why: + :versions: [] + :when: 2016-05-02 06:41:38.703526000 Z +- - :ignore + - bundler + - :who: + :why: Bundler is MIT licensed but will sometimes fail in CI. + :versions: [] + :when: 2016-05-02 06:42:08.045090000 Z From 4cff270f20cbf09641b3c65086a769f684cf8755 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 2 Jun 2016 11:08:59 -0600 Subject: [PATCH 327/507] Fix up all the decisions in the YML file. --- config/.decisions.yml | 263 ++++++++++++++++++++---------------------- 1 file changed, 122 insertions(+), 141 deletions(-) diff --git a/config/.decisions.yml b/config/.decisions.yml index 9cfc9a186b..2327b2d17a 100644 --- a/config/.decisions.yml +++ b/config/.decisions.yml @@ -1,110 +1,37 @@ --- -- - :whitelist - - MIT - - :who: - :why: - :versions: [] - :when: 2016-04-17 21:12:24.558441000 Z +# IGNORED GROUPS AND GEMS - - :ignore_group - development - - :who: - :why: + - :who: Connor Shea + :why: Development gems are not distributed with the final product and are therefore exempt. :versions: [] :when: 2016-04-17 21:27:01.054140000 Z - - :ignore_group - test - - :who: - :why: + - :who: Connor Shea + :why: Test gems are not distributed with the final product and are therefore exempt. :versions: [] :when: 2016-04-17 21:27:06.250326000 Z -- - :license - - raphael-rails +- - :ignore + - bundler + - :who: Connor Shea + :why: Bundler is MIT licensed but will sometimes fail in CI. + :versions: [] + :when: 2016-05-02 06:42:08.045090000 Z + +# LICENSE WHITELIST +- - :whitelist - MIT - - :who: - :why: https://github.com/mockdeep/raphael-rails/blob/master/license.txt + - :who: Connor Shea + :why: Compatible with itself. :versions: [] - :when: 2016-04-17 21:30:07.575392000 Z -- - :license - - rouge - - MIT - - :who: - :why: https://github.com/jneen/rouge/blob/master/LICENSE - :versions: [] - :when: 2016-04-17 21:31:29.490394000 Z -- - :license - - pyu-ruby-sasl - - MIT - - :who: - :why: https://github.com/pyu10055/ruby-sasl/blob/master/MIT-LICENSE - :versions: [] - :when: 2016-04-17 21:41:55.266420000 Z -- - :license - - six - - MIT - - :who: - :why: https://github.com/randx/six/blob/master/LICENSE - :versions: [] - :when: 2016-04-17 21:42:31.420186000 Z -- - :license - - rdoc - - ruby - - :who: - :why: https://github.com/rdoc/rdoc/blob/master/LICENSE.rdoc - :versions: [] - :when: 2016-04-17 21:43:30.480413000 Z -- - :license - - expression_parser - - MIT - - :who: - :why: https://github.com/nricciar/expression_parser/blob/master/MIT-LICENSE - :versions: [] - :when: 2016-04-17 21:45:41.829912000 Z -- - :license - - ace-rails-ap - - MIT - - :who: - :why: https://github.com/codykrieger/ace-rails-ap/blob/master/LICENSE - :versions: [] - :when: 2016-04-17 21:46:19.767922000 Z -- - :license - - jquery-scrollto-rails - - MIT, GPLv2 - - :who: - :why: https://github.com/JohnColvin/jquery-scrollto-rails/blob/master/MIT%20License - :versions: [] - :when: 2016-04-17 21:47:56.967946000 Z -- - :license - - creole - - ruby - - :who: - :why: https://github.com/minad/creole#license - :versions: [] - :when: 2016-04-17 21:49:10.329759000 Z -- - :license - - eventmachine - - ruby - - :who: - :why: https://github.com/eventmachine/eventmachine/blob/master/LICENSE - :versions: [] - :when: 2016-04-17 21:49:10.329759001 Z + :when: 2016-04-17 21:12:24.558441000 Z - - :whitelist - Apache 2.0 - :who: Connor Shea :why: http://choosealicense.com/licenses/apache-2.0/ :versions: [] :when: 2016-05-02 05:27:43.762702000 Z -- - :blacklist - - GPLv2 - - :who: Connor Shea - :why: GPL-licensed libraries cannot be linked to from non-GPL projects. - :versions: [] - :when: 2016-05-02 05:29:27.637336000 Z -- - :blacklist - - GPLv3 - - :who: Connor Shea - :why: GPL-licensed libraries cannot be linked to from non-GPL projects. - :versions: [] - :when: 2016-05-02 05:29:43.904715000 Z - - :whitelist - ruby - :who: Connor Shea @@ -129,68 +56,122 @@ :why: https://opensource.org/licenses/BSD-3-Clause :versions: [] :when: 2016-05-02 05:44:38.246021000 Z -- - :license - - unicorn - - ruby - - :who: - :why: - :versions: [] - :when: 2016-05-02 05:45:28.817510000 Z -- - :license - - unicorn-worker-killer - - ruby - - :who: - :why: - :versions: [] - :when: 2016-05-02 05:45:38.323867000 Z -- - :license - - json - - ruby - - :who: - :why: - :versions: [] - :when: 2016-05-02 05:50:07.826564000 Z -- - :license - - unf - - BSD - - :who: - :why: - :versions: [] - :when: 2016-05-02 05:51:46.886872000 Z - - :whitelist - LGPL-2.1+ - - :who: + - :who: Connor Shea :why: Equivalent to LGPL. :versions: [] :when: 2016-05-02 05:52:56.303239000 Z - - :whitelist - BSD - - :who: + - :who: Connor Shea :why: https://opensource.org/licenses/BSD-2-Clause :versions: [] :when: 2016-05-02 05:55:09.796363000 Z + +# LICENSE BLACKLIST +- - :blacklist + - GPLv2 + - :who: Connor Shea + :why: GPL-licensed libraries cannot be linked to from non-GPL projects. + :versions: [] + :when: 2016-05-02 05:29:27.637336000 Z +- - :blacklist + - GPLv3 + - :who: Connor Shea + :why: GPL-licensed libraries cannot be linked to from non-GPL projects. + :versions: [] + :when: 2016-05-02 05:29:43.904715000 Z + +# GEM LICENSES +- - :license + - raphael-rails + - MIT + - :who: Connor Shea + :why: https://github.com/mockdeep/raphael-rails/blob/master/license.txt + :versions: [] + :when: 2016-04-17 21:30:07.575392000 Z +- - :license + - rouge + - MIT + - :who: Connor Shea + :why: https://github.com/jneen/rouge/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:31:29.490394000 Z +- - :license + - pyu-ruby-sasl + - MIT + - :who: Connor Shea + :why: https://github.com/pyu10055/ruby-sasl/blob/master/MIT-LICENSE + :versions: [] + :when: 2016-04-17 21:41:55.266420000 Z +- - :license + - six + - MIT + - :who: Connor Shea + :why: https://github.com/randx/six/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:42:31.420186000 Z +- - :license + - rdoc + - ruby + - :who: Connor Shea + :why: https://github.com/rdoc/rdoc/blob/master/LICENSE.rdoc + :versions: [] + :when: 2016-04-17 21:43:30.480413000 Z +- - :license + - expression_parser + - MIT + - :who: Connor Shea + :why: https://github.com/nricciar/expression_parser/blob/master/MIT-LICENSE + :versions: [] + :when: 2016-04-17 21:45:41.829912000 Z +- - :license + - creole + - ruby + - :who: Connor Shea + :why: https://github.com/minad/creole#license + :versions: [] + :when: 2016-04-17 21:49:10.329759000 Z +- - :license + - eventmachine + - ruby + - :who: Connor Shea + :why: https://github.com/eventmachine/eventmachine/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:49:10.329759001 Z +- - :license + - unicorn + - ruby + - :who: Connor Shea + :why: http://unicorn.bogomips.org/LICENSE.html + :versions: [] + :when: 2016-05-02 05:45:28.817510000 Z +- - :license + - unicorn-worker-killer + - ruby + - :who: Connor Shea + :why: https://github.com/kzk/unicorn-worker-killer/blob/master/LICENSE + :versions: [] + :when: 2016-05-02 05:45:38.323867000 Z +- - :license + - json + - ruby + - :who: Connor Shea + :why: https://github.com/flori/json/tree/master#license + :versions: [] + :when: 2016-05-02 05:50:07.826564000 Z +- - :license + - unf + - BSD + - :who: Connor Shea + :why: https://github.com/knu/ruby-unf/blob/master/LICENSE + :versions: [] + :when: 2016-05-02 05:51:46.886872000 Z - - :license - rubypants - - MIT - - :who: - :why: + - BSD + - :who: Connor Shea + :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc :versions: [] :when: 2016-05-02 05:56:50.696858000 Z -- - :ignore - - bundler - - :who: - :why: - :versions: [] - :when: 2016-05-02 06:41:31.504230000 Z -- - :heed - - bundler - - :who: - :why: - :versions: [] - :when: 2016-05-02 06:41:38.703526000 Z -- - :ignore - - bundler - - :who: - :why: Bundler is MIT licensed but will sometimes fail in CI. - :versions: [] - :when: 2016-05-02 06:42:08.045090000 Z From 3c3234121575ce271f8b50f3dcf1880db328fed1 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 2 Jun 2016 12:04:19 -0600 Subject: [PATCH 328/507] Add Licensing information and guidelines to the Documentation. --- config/.decisions.yml | 2 +- doc/development/README.md | 2 ++ doc/development/licensing.md | 57 ++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 doc/development/licensing.md diff --git a/config/.decisions.yml b/config/.decisions.yml index 2327b2d17a..273796f391 100644 --- a/config/.decisions.yml +++ b/config/.decisions.yml @@ -23,7 +23,7 @@ - - :whitelist - MIT - :who: Connor Shea - :why: Compatible with itself. + :why: http://choosealicense.com/licenses/mit/ :versions: [] :when: 2016-04-17 21:12:24.558441000 Z - - :whitelist diff --git a/doc/development/README.md b/doc/development/README.md index aa7d54c01d..cf1bd493a9 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -7,9 +7,11 @@ - [Gotchas](gotchas.md) to avoid - [How to dump production data to staging](db_dump.md) - [Instrumentation](instrumentation.md) +- [Licensing](licensing.md) for ensuring license compliance - [Migration Style Guide](migration_style_guide.md) for creating safe migrations - [Performance guidelines](performance.md) - [Rake tasks](rake_tasks.md) for development +- [SCSS Style Guide](scss_styleguide.md) - [Shell commands](shell_commands.md) in the GitLab codebase - [Sidekiq debugging](sidekiq_debugging.md) - [SQL guidelines](sql.md) for SQL guidelines diff --git a/doc/development/licensing.md b/doc/development/licensing.md new file mode 100644 index 0000000000..87841f8c39 --- /dev/null +++ b/doc/development/licensing.md @@ -0,0 +1,57 @@ +# GitLab Licensing and Compatibility + +GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed under "The GitLab Enterprise Edition (EE) license" wherein there are more restrictions. See their respective LICENSE files ([CE][CE], [EE][EE]) for more information. + +## Automated Testing + +In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition. + +There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them. + +Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually. [License Finder's README][license_finder] includes information and guidance on manually adding a gem's license. Make sure to include a link to the library's license in the "why" section. + +## Acceptable Licenses + +The following are licenses which are acceptable to use: + +- [The MIT License][MIT] (the MIT Expat License specifically): The MIT License requires that the license itself is included with all copies of the source. It is a permissive (non-copyleft) license as defined by the Open Source Initiative. +- [LGPL][LGPL] (version 2, version 3): GPL constraints regarding modification and redistribution under the same license are not required of projects using an LGPL library, only upon modification of the LGPL-licensed library itself. +- [Apache 2.0 License][apache-2]: A permissive license that also provides an express grant of patent rights from contributors to users. +- [Ruby 1.8 License][ruby-1.8]: Dual-licensed under either itself or the GPLv2, defer to the Ruby License itself. Acceptable because of point 3b: "You may distribute the software in object code or binary form, provided that you do at least ONE of the following: b) accompany the distribution with the machine-readable source of the software." +- [Ruby 1.9 License][ruby-1.9]: Dual-licensed under either itself or the BSD 2-Clause License, defer to BSD 2-Clause. +- [BSD 2-Clause License][BSD-2-Clause]: A permissive (non-copyleft) license as defined by the Open Source Initiative. +- [BSD 3-Clause License][BSD-3-Clause] (also known as New BSD or Modified BSD): A permissive (non-copyleft) license as defined by the Open Source Initiative +- [ISC License][ISC] (also known as the OpenBSD License): A permissive (non-copyleft) license as defined by the Open Source Initiative. + +## Unacceptable Licenses + +The following are licenses which are *not* acceptable to use: + +- [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects. +- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. + +## Notes + +If a gem uses a license which is not listed above, open an issue and ask. If a license is not included in the "acceptable" list, operate under the assumption that it is not acceptable. + +Keep in mind that each license has its own restrictions (typically defined in their body text). Please make sure to comply with those restrictions at all times whenever an external library is used. + +Gems which are included only in the "development" or "test" groups by Bundler are exempt from license requirements, as they're not distributed for use in production. + +**NOTE:** This document is **not** legal advice, nor is it comprehensive. It should not be taken as such. + +[CE]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/LICENSE +[EE]: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/LICENSE +[license_finder]: https://github.com/pivotal/LicenseFinder +[MIT]: http://choosealicense.com/licenses/mit/ +[LGPL]: http://choosealicense.com/licenses/lgpl-3.0/ +[apache-2]: http://choosealicense.com/licenses/apache-2.0/ +[ruby-1.8]: https://github.com/ruby/ruby/blob/ruby_1_8_6/COPYING +[ruby-1.9]: https://www.ruby-lang.org/en/about/license.txt +[BSD-2-Clause]: https://opensource.org/licenses/BSD-2-Clause +[BSD-3-Clause]: https://opensource.org/licenses/BSD-3-Clause +[ISC]: https://opensource.org/licenses/ISC +[GPL]: http://choosealicense.com/licenses/gpl-3.0/ +[GPLv2]: http://www.gnu.org/licenses/gpl-2.0.txt +[GPLv3]: http://www.gnu.org/licenses/gpl-3.0.txt +[AGPLv3]: http://choosealicense.com/licenses/agpl-3.0/ From 7045b9e908a6ea4f76b5e6ae09c5ab1385685c5a Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 2 Jun 2016 12:27:42 -0600 Subject: [PATCH 329/507] Make sure to mention the GNU Project and OSI-provided information regarding the GPL so no one tries to disagree with that decision. --- doc/development/licensing.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/development/licensing.md b/doc/development/licensing.md index 87841f8c39..abfd992652 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -12,7 +12,7 @@ Some gems may not include their license information in their `gemspec` file. The ## Acceptable Licenses -The following are licenses which are acceptable to use: +Libraries with the following licenses are acceptable for use: - [The MIT License][MIT] (the MIT Expat License specifically): The MIT License requires that the license itself is included with all copies of the source. It is a permissive (non-copyleft) license as defined by the Open Source Initiative. - [LGPL][LGPL] (version 2, version 3): GPL constraints regarding modification and redistribution under the same license are not required of projects using an LGPL library, only upon modification of the LGPL-licensed library itself. @@ -25,13 +25,15 @@ The following are licenses which are acceptable to use: ## Unacceptable Licenses -The following are licenses which are *not* acceptable to use: +Libraries with the following licenses are acceptable for use: - [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects. - [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. ## Notes +Decisions regarding the GNU GPL licenses are based on information provided by [The GNU Project][GNU-GPL-FAQ], as well as [the Open Source Initiative][OSI-GPL], which both state that linking GPL libraries makes the program itself GPL. + If a gem uses a license which is not listed above, open an issue and ask. If a license is not included in the "acceptable" list, operate under the assumption that it is not acceptable. Keep in mind that each license has its own restrictions (typically defined in their body text). Please make sure to comply with those restrictions at all times whenever an external library is used. @@ -55,3 +57,5 @@ Gems which are included only in the "development" or "test" groups by Bundler ar [GPLv2]: http://www.gnu.org/licenses/gpl-2.0.txt [GPLv3]: http://www.gnu.org/licenses/gpl-3.0.txt [AGPLv3]: http://choosealicense.com/licenses/agpl-3.0/ +[GNU-GPL-FAQ]: http://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL +[OSI-GPL]: https://opensource.org/faq#linking-proprietary-code From b2e85b796835b024d9436aae43b3a7c65fab66de Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 4 Jun 2016 21:32:59 -0600 Subject: [PATCH 330/507] Add relevant commands to the licensing document, resolve some feedback. --- doc/development/README.md | 1 - doc/development/licensing.md | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/doc/development/README.md b/doc/development/README.md index cf1bd493a9..c5d5af4386 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -11,7 +11,6 @@ - [Migration Style Guide](migration_style_guide.md) for creating safe migrations - [Performance guidelines](performance.md) - [Rake tasks](rake_tasks.md) for development -- [SCSS Style Guide](scss_styleguide.md) - [Shell commands](shell_commands.md) in the GitLab codebase - [Sidekiq debugging](sidekiq_debugging.md) - [SQL guidelines](sql.md) for SQL guidelines diff --git a/doc/development/licensing.md b/doc/development/licensing.md index abfd992652..8c8c7486ff 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -8,7 +8,39 @@ In order to comply with the terms the libraries we use are licensed under, we ha There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them. -Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually. [License Finder's README][license_finder] includes information and guidance on manually adding a gem's license. Make sure to include a link to the library's license in the "why" section. +Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually. + +### License Finder commands + +There are a few basic commands License Finder provides that you'll need in order to manage license detection. + +To verify that the checks are passing, and/or to see what dependencies are causing the checks to fail: + +``` +bundle exec license_finder +``` + +To whitelist a new license: + +``` +license_finder whitelist add MIT +``` + +To blacklist a new license: + +``` +license_finder blacklist add GPLv2 +``` + +To tell License Finder about a dependency's license if it isn't auto-detected: + +``` +license_finder licenses add my_unknown_dependency MIT +``` + +For all of the above, please include `--why "Reason"` and `--who "My Name"` so the `decisions.yml` file can keep track of when, why, and who approved of a dependency. + +More detailed information on how the gem and its commands work is available in the [License Finder README][license_finder]. ## Acceptable Licenses @@ -25,7 +57,7 @@ Libraries with the following licenses are acceptable for use: ## Unacceptable Licenses -Libraries with the following licenses are acceptable for use: +Libraries with the following licenses are unacceptable for use: - [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects. - [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. From 7900c035bf3f0cf96740227aebadbb3cad276ba0 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 6 Jun 2016 10:34:44 -0600 Subject: [PATCH 331/507] Resolve feedback. --- config/{.decisions.yml => dependency_decisions.yml} | 2 +- config/license_finder.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename config/{.decisions.yml => dependency_decisions.yml} (98%) diff --git a/config/.decisions.yml b/config/dependency_decisions.yml similarity index 98% rename from config/.decisions.yml rename to config/dependency_decisions.yml index 273796f391..a49d805e4f 100644 --- a/config/.decisions.yml +++ b/config/dependency_decisions.yml @@ -35,7 +35,7 @@ - - :whitelist - ruby - :who: Connor Shea - :why: https://github.com/ruby/ruby/blob/ruby_1_8_6/COPYING + :why: https://github.com/ruby/ruby/blob/ruby_2_1/COPYING :versions: [] :when: 2016-05-02 05:31:54.498490000 Z - - :whitelist diff --git a/config/license_finder.yml b/config/license_finder.yml index d641563719..e01ebec329 100644 --- a/config/license_finder.yml +++ b/config/license_finder.yml @@ -1,2 +1,2 @@ --- -decisions_file: './config/.decisions.yml' +decisions_file: './config/dependency_decisions.yml' From efb7da68e686e501abcfb8b0aea241c7b625fac5 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 6 Jun 2016 15:17:42 -0600 Subject: [PATCH 332/507] Fix missed colorize methods. --- db/fixtures/production/001_admin.rb | 12 ++++++------ lib/tasks/gitlab/setup.rake | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb index 78746c8322..b37dc79401 100644 --- a/db/fixtures/production/001_admin.rb +++ b/db/fixtures/production/001_admin.rb @@ -16,21 +16,21 @@ user = User.new(user_args) user.skip_confirmation! if user.save - puts "Administrator account created:".green + puts "Administrator account created:".color(:green) puts - puts "login: root".green + puts "login: root".color(:green) if user_args.key?(:password) - puts "password: #{user_args[:password]}".green + puts "password: #{user_args[:password]}".color(:green) else - puts "password: You'll be prompted to create one on your first visit.".green + puts "password: You'll be prompted to create one on your first visit.".color(:green) end puts else - puts "Could not create the default administrator account:".red + puts "Could not create the default administrator account:".color(:red) puts user.errors.full_messages.map do |message| - puts "--> #{message}".red + puts "--> #{message}".color(:red) end puts diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 48baecfd2a..05fcb8e3da 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -19,7 +19,7 @@ namespace :gitlab do Rake::Task["setup_postgresql"].invoke Rake::Task["db:seed_fu"].invoke rescue Gitlab::TaskAbortedByUserError - puts "Quitting...".red + puts "Quitting...".color(:red) exit 1 end end From fbd2169f9948ee039efe96f48229f4c9ced8c412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Mon, 6 Jun 2016 18:27:26 -0400 Subject: [PATCH 333/507] Use better_errors editor links in sherlock Remember to configure your `better_errors` editor to point to your program of preference --- app/views/sherlock/queries/_backtrace.html.haml | 6 +++++- app/views/sherlock/queries/_general.html.haml | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml index 5c9294c0ab..30e956e5f4 100644 --- a/app/views/sherlock/queries/_backtrace.html.haml +++ b/app/views/sherlock/queries/_backtrace.html.haml @@ -6,7 +6,11 @@ %ul.well-list - @query.application_backtrace.each do |location| %li - = location.path + %strong + - if defined?(BetterErrors) + = link_to(location.path, BetterErrors.editor[location.path, location.line]) + - else + = location.path %small.light = t('sherlock.line') = location.line diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml index 549b47430e..7073c0f4d9 100644 --- a/app/views/sherlock/queries/_general.html.haml +++ b/app/views/sherlock/queries/_general.html.haml @@ -11,13 +11,17 @@ = @query.duration.round(4) = t('sherlock.milliseconds') %li + - frame = @query.last_application_frame %span.light #{t('sherlock.origin')}: %strong - = @query.last_application_frame.path + - if defined?(BetterErrors) + = link_to(frame.path, BetterErrors.editor[frame.path, frame.line]) + - else + = frame.path %small.light = t('sherlock.line') - = @query.last_application_frame.line + = frame.line .panel.panel-default .panel-heading From 7038440e342a521807b1e5ffb6d47d4c0b13048d Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 6 Jun 2016 18:47:49 -0500 Subject: [PATCH 334/507] Adjust the SAML control flow to allow LDAP identities to be added to an existing SAML user. --- lib/gitlab/o_auth/user.rb | 2 +- lib/gitlab/saml/user.rb | 28 ++++++++++++++++++++++++++-- spec/lib/gitlab/saml/user_spec.rb | 19 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 356e96fcba..268ee11502 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -96,7 +96,7 @@ module Gitlab # Look for a corresponding person with same uid in any of the configured LDAP providers Gitlab::LDAP::Config.providers.each do |provider| adapter = Gitlab::LDAP::Adapter.new(provider) - @ldap_person = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) + @ldap_person = Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter) break if @ldap_person end @ldap_person diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index dba4bbfc89..6f7d4825ae 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -12,12 +12,12 @@ module Gitlab end def gl_user - @user ||= find_by_uid_and_provider - if auto_link_ldap_user? @user ||= find_or_create_ldap_user end + @user ||= find_by_uid_and_provider + if auto_link_saml_user? @user ||= find_by_email end @@ -62,6 +62,30 @@ module Gitlab !Gitlab::Saml::Config.external_groups.nil? end + def find_or_create_ldap_user + return unless ldap_person + + # If a corresponding person exists with same uid in a LDAP server, + # check if the user already has a GitLab account + user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider) + if user + # Case when a LDAP user already exists in Gitlab. Add the SAML identity to existing account. + user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider) + else + # No account found using LDAP in Gitlab yet: check if there is a SAML account with + # the passed uid and provider + user = find_by_uid_and_provider + if user.nil? + # No SAML account found, build a new user. + user = build_new_user + end + # Correct account is present, add the LDAP Identity to the user. + user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn) + end + + user + end + def auth_hash=(auth_hash) @auth_hash = Gitlab::Saml::AuthHash.new(auth_hash) end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index c2a51d9249..f0a17244ff 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -145,6 +145,7 @@ describe Gitlab::Saml::User, lib: true do allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) } allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user) end context 'and no account for the LDAP user' do @@ -177,6 +178,24 @@ describe Gitlab::Saml::User, lib: true do ]) end end + + context 'user has SAML user, and wants to add their LDAP identity' do + it 'adds the LDAP identity to the existing SAML user' do + create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john') + local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) + local_saml_user = described_class.new(local_hash) + + local_saml_user.save + local_gl_user = local_saml_user.gl_user + expect(local_gl_user).to be_valid + expect(local_gl_user.identities.length).to eql 2 + identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' } + ]) + end + + end end end end From 8f28dc950ac21feba4c187bf96fed5615943ad76 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 6 Jun 2016 11:55:23 -0700 Subject: [PATCH 335/507] Bump rouge to 1.11.0 --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bee1a82497..7809fef170 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.9.0 (unreleased) - Bulk assign/unassign labels to issues. - Allow enabling wiki page events from Webhook management UI + - Bump rouge to 1.11.0 - Make EmailsOnPushWorker use Sidekiq mailers queue - Fix wiki page events' webhook to point to the wiki repository - Fix issue todo not remove when leave project !4150 (Long Nguyen) diff --git a/Gemfile b/Gemfile index 38ff536fd7..fe02b2a65d 100644 --- a/Gemfile +++ b/Gemfile @@ -111,7 +111,7 @@ gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' -gem 'rouge', '~> 1.10.1' +gem 'rouge', '~> 1.11' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM diff --git a/Gemfile.lock b/Gemfile.lock index 5f1dbd431e..21d8f345cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -569,7 +569,7 @@ GEM railties (>= 4.2.0, < 5.1) rinku (1.7.3) rotp (2.1.2) - rouge (1.10.1) + rouge (1.11.0) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -926,7 +926,7 @@ DEPENDENCIES request_store (~> 1.3.0) rerun (~> 0.11.0) responders (~> 2.0) - rouge (~> 1.10.1) + rouge (~> 1.11) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.4.0) rspec-retry From dbf235f514cc919a10fee1d9ab8dc1c75bc25238 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 10:25:57 +0200 Subject: [PATCH 336/507] Fix tests failures --- app/services/ci/create_builds_service.rb | 2 +- .../merge_requests/created_from_fork_spec.rb | 4 +- spec/features/projects/commit/builds_spec.rb | 4 +- spec/lib/gitlab/badge/build_spec.rb | 4 +- spec/models/build_spec.rb | 2 +- spec/models/project_spec.rb | 2 +- .../create_commit_builds_service_spec.rb | 70 +++++++++---------- 7 files changed, 44 insertions(+), 44 deletions(-) diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 41ce6982cb..64bcdac5c6 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -5,7 +5,7 @@ module Ci end def execute(stage, user, status, trigger_request = nil) - builds_attrs = config_processor.builds_for_stage_and_ref(stage, @commit.ref, @commit.tag, trigger_request) + builds_attrs = config_processor.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request) # check when to create next build builds_attrs = builds_attrs.select do |build_attrs| diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index 12d7e52629..b4d2201c72 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -30,8 +30,8 @@ feature 'Merge request created from fork' do given(:pipeline) do create(:ci_pipeline_with_two_job, project: fork_project, - sha: merge_request.last_commit.id, - ref: merge_request.source_branch) + sha: merge_request.last_commit.id, + ref: merge_request.source_branch) end background { pipeline.create_builds(user) } diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index 73dd568929..15c381c0f5 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -12,8 +12,8 @@ feature 'project commit builds' do context 'when no builds triggered yet' do background do create(:ci_pipeline, project: project, - sha: project.commit.sha, - ref: 'master') + sha: project.commit.sha, + ref: 'master') end scenario 'user views commit builds page' do diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index aec5a5bc3b..2034445a19 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -110,8 +110,8 @@ describe Gitlab::Badge::Build do def create_build(project, sha, branch) pipeline = create(:ci_pipeline, project: project, - sha: sha, - ref: branch) + sha: sha, + ref: branch) create(:ci_build, pipeline: pipeline) end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 2beb6cc598..7660ea2659 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -219,7 +219,7 @@ describe Ci::Build, models: true do context 'and trigger variables' do let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, commit: pipeline, trigger: trigger) } let(:trigger_variables) do [ { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 89f66092b1..3431d0435b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -401,7 +401,7 @@ describe Project, models: true do describe :pipeline do let(:project) { create :project } - let(:pipeline) { create :pipeline, project: project, ref: 'master' } + let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' } subject { project.pipeline(pipeline.sha, 'master') } diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 706319b63e..77247a1d5a 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -70,11 +70,11 @@ describe CreateCommitBuildsService, services: true do stub_ci_pipeline_yaml_file('invalid: file: file') commits = [{ message: message }] pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) expect(pipeline).to be_persisted expect(pipeline.builds.any?).to be false expect(pipeline.status).to eq('failed') @@ -91,11 +91,11 @@ describe CreateCommitBuildsService, services: true do it "skips builds creation if there is [ci skip] tag in commit message" do commits = [{ message: message }] pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) expect(pipeline).to be_persisted expect(pipeline.builds.any?).to be false expect(pipeline.status).to eq("skipped") @@ -106,11 +106,11 @@ describe CreateCommitBuildsService, services: true do commits = [{ message: "some message" }] pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) expect(pipeline).to be_persisted expect(pipeline.builds.first.name).to eq("staging") @@ -120,11 +120,11 @@ describe CreateCommitBuildsService, services: true do stub_ci_pipeline_yaml_file('invalid: file: fiile') commits = [{ message: message }] pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) expect(pipeline).to be_persisted expect(pipeline.builds.any?).to be false expect(pipeline.status).to eq("skipped") @@ -137,20 +137,20 @@ describe CreateCommitBuildsService, services: true do commits = [{ message: "message" }] pipeline = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits + ) expect(pipeline).to be_persisted expect(pipeline.builds.count(:all)).to eq(2) pipeline = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits + ) expect(pipeline).to be_persisted expect(pipeline.builds.count(:all)).to eq(2) end @@ -161,11 +161,11 @@ describe CreateCommitBuildsService, services: true do commits = [{ message: "some message" }] pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) expect(pipeline).to be_persisted expect(pipeline.status).to eq("failed") From fa097c678cdfead0dc1344e6d32569266da53465 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 7 Jun 2016 10:26:38 +0200 Subject: [PATCH 337/507] Remove duplicated exception in Ci config This is a temporary refactoring stub, that is planned to be removed after removing legacy config processor. --- lib/ci/gitlab_ci_yaml_processor.rb | 2 +- lib/gitlab/ci/config.rb | 5 ----- spec/lib/gitlab/ci/config_spec.rb | 3 ++- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 46a923161c..130f5b0892 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -18,7 +18,7 @@ module Ci initial_parsing validate! - rescue Gitlab::Ci::Config::LoaderError => e + rescue Gitlab::Ci::Config::Loader::FormatError => e raise ValidationError, e.message end diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 5fc4894311..b6ce791c0f 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -5,11 +5,6 @@ module Gitlab def initialize(config) loader = Loader.new(config) - - unless loader.valid? - raise LoaderError, 'Invalid configuration format!' - end - @config = loader.load end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 52aafbcaaa..4d46abe520 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -37,7 +37,8 @@ describe Gitlab::Ci::Config do describe '.new' do it 'raises error' do expect { config }.to raise_error( - Gitlab::Ci::Config::LoaderError, /Invalid configuration format/ + Gitlab::Ci::Config::Loader::FormatError, + /Invalid configuration format/ ) end end From b09a329fd8b314fc48114ce15d451a18d3fcb70f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 10:31:02 +0200 Subject: [PATCH 338/507] Use ruby:2.1 and ruby:2.2 images --- .gitlab-ci.yml | 4 ++-- scripts/prepare_build.sh | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 172fbaf52b..23bc2c2f83 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.1 +image: "ruby:2.1" services: - mysql:latest @@ -116,7 +116,7 @@ spinach 9 10: *knapsack .knapsack-ruby22: &knapsack-ruby22 <<: *knapsack - image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.2 + image: "ruby:2.2" only: - master cache: diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 9540d7d128..247383aa46 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -12,6 +12,20 @@ retry() { } if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then + mkdir -p vendor + + # Install phantomjs package + pushd vendor + if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then + wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb + fi + dpkg -i phantomjs_1.9.8-0jessie_amd64.deb + popd + + # Try to install packages + retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ + libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip' + cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml sed -i 's/password:.*/password:/g' config/database.yml From 0f97357145fa951e206536f11ca1d52efabe4675 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 10:45:02 +0200 Subject: [PATCH 339/507] Install knapsack --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 23bc2c2f83..53184f8f90 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,6 +23,7 @@ before_script: - cp config/gitlab.yml.example config/gitlab.yml - bundle --version - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' + - retry gem install knapsack - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' stages: @@ -74,7 +75,6 @@ update-knapsack: - export KNAPSACK_REPORT_PATH=knapsack/${JOB_NAME}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - cp knapsack/${JOB_NAME}_report.json ${KNAPSACK_REPORT_PATH} - - bundle exec gem install bundler - knapsack ${JOB_NAME[0]} artifacts: paths: From 498f8d13d003b1d9a50babfd45911ef55e905ba5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 11:23:53 +0200 Subject: [PATCH 340/507] Fix license_finder --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 53184f8f90..476815de71 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -170,7 +170,7 @@ rake brakeman: *exec rake flog: *exec rake flay: *exec rake db:migrate:reset: *exec -license-finder: *exec +license_finder: *exec bundler:audit: stage: test From 5ef104df59211b022ed42e38e1cdbe950ff54388 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 7 Jun 2016 12:53:46 +0200 Subject: [PATCH 341/507] Improve Ci config loader by changing method signature --- lib/gitlab/ci/config.rb | 2 +- lib/gitlab/ci/config/loader.rb | 2 +- spec/lib/gitlab/ci/config/loader_spec.rb | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index b6ce791c0f..ffe633d4b6 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -5,7 +5,7 @@ module Gitlab def initialize(config) loader = Loader.new(config) - @config = loader.load + @config = loader.load! end def to_hash diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb index ed9cc16702..dbf6eb0edb 100644 --- a/lib/gitlab/ci/config/loader.rb +++ b/lib/gitlab/ci/config/loader.rb @@ -12,7 +12,7 @@ module Gitlab @config.is_a?(Hash) end - def load + def load! unless valid? raise FormatError, 'Invalid configuration format' end diff --git a/spec/lib/gitlab/ci/config/loader_spec.rb b/spec/lib/gitlab/ci/config/loader_spec.rb index 6f1a10085d..2d44b1f60f 100644 --- a/spec/lib/gitlab/ci/config/loader_spec.rb +++ b/spec/lib/gitlab/ci/config/loader_spec.rb @@ -12,9 +12,9 @@ describe Gitlab::Ci::Config::Loader do end end - describe '#load' do + describe '#load!' do it 'returns a valid hash' do - expect(loader.load).to eq(image: 'ruby:2.2') + expect(loader.load!).to eq(image: 'ruby:2.2') end end end @@ -28,9 +28,9 @@ describe Gitlab::Ci::Config::Loader do end end - describe '#load' do + describe '#load!' do it 'raises error' do - expect { loader.load }.to raise_error( + expect { loader.load! }.to raise_error( Gitlab::Ci::Config::Loader::FormatError, 'Invalid configuration format' ) From 03a7569ea6952b47c3f25294e94cb3abf1877d5d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 13:06:24 +0200 Subject: [PATCH 342/507] Rerun failed spinach tests --- .gitlab-ci.yml | 160 ++++++++++++++++++++++----------------- scripts/prepare_build.sh | 10 ++- 2 files changed, 99 insertions(+), 71 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 476815de71..095c9005d1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -48,7 +48,7 @@ stages: - knapsack/ knapsack: - <<: *knapsack_state + <<: *rspec-knapsack_state stage: prepare script: - mkdir -p knapsack/ @@ -56,7 +56,7 @@ knapsack: - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json' update-knapsack: - <<: *knapsack_state + <<: *rspec-knapsack_state stage: post-test script: - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json @@ -65,57 +65,73 @@ update-knapsack: # Execute all testing suites -.knapsack: &knapsack +.rspec-knapsack: &knapsack stage: test script: - bundle exec rake assets:precompile 2>/dev/null - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} - - export KNAPSACK_REPORT_PATH=knapsack/${JOB_NAME}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - - cp knapsack/${JOB_NAME}_report.json ${KNAPSACK_REPORT_PATH} - - knapsack ${JOB_NAME[0]} + - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH} + - knapsack rspec artifacts: paths: - knapsack/ -rspec 0 20: *knapsack -rspec 1 20: *knapsack -rspec 2 20: *knapsack -rspec 3 20: *knapsack -rspec 4 20: *knapsack -rspec 5 20: *knapsack -rspec 6 20: *knapsack -rspec 7 20: *knapsack -rspec 8 20: *knapsack -rspec 9 20: *knapsack -rspec 10 20: *knapsack -rspec 11 20: *knapsack -rspec 12 20: *knapsack -rspec 13 20: *knapsack -rspec 14 20: *knapsack -rspec 15 20: *knapsack -rspec 16 20: *knapsack -rspec 17 20: *knapsack -rspec 18 20: *knapsack -rspec 19 20: *knapsack +.spinach-knapsack: &knapsack + stage: test + script: + - bundle exec rake assets:precompile 2>/dev/null + - JOB_NAME=( $CI_BUILD_NAME ) + - export CI_NODE_INDEX=${JOB_NAME[1]} + - export CI_NODE_TOTAL=${JOB_NAME[2]} + - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_GENERATE_REPORT=true + - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH} + - knapsack spinach[-r rerun] + # retry failed tests 3 times + - retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)' + artifacts: + paths: + - knapsack/ -spinach 0 10: *knapsack -spinach 1 10: *knapsack -spinach 2 10: *knapsack -spinach 3 10: *knapsack -spinach 4 10: *knapsack -spinach 5 10: *knapsack -spinach 6 10: *knapsack -spinach 7 10: *knapsack -spinach 8 10: *knapsack -spinach 9 10: *knapsack +rspec 0 20: *rspec-knapsack +rspec 1 20: *rspec-knapsack +rspec 2 20: *rspec-knapsack +rspec 3 20: *rspec-knapsack +rspec 4 20: *rspec-knapsack +rspec 5 20: *rspec-knapsack +rspec 6 20: *rspec-knapsack +rspec 7 20: *rspec-knapsack +rspec 8 20: *rspec-knapsack +rspec 9 20: *rspec-knapsack +rspec 10 20: *rspec-knapsack +rspec 11 20: *rspec-knapsack +rspec 12 20: *rspec-knapsack +rspec 13 20: *rspec-knapsack +rspec 14 20: *rspec-knapsack +rspec 15 20: *rspec-knapsack +rspec 16 20: *rspec-knapsack +rspec 17 20: *rspec-knapsack +rspec 18 20: *rspec-knapsack +rspec 19 20: *rspec-knapsack + +spinach 0 10: *spinach-knapsack +spinach 1 10: *spinach-knapsack +spinach 2 10: *spinach-knapsack +spinach 3 10: *spinach-knapsack +spinach 4 10: *spinach-knapsack +spinach 5 10: *spinach-knapsack +spinach 6 10: *spinach-knapsack +spinach 7 10: *spinach-knapsack +spinach 8 10: *spinach-knapsack +spinach 9 10: *spinach-knapsack # Execute all testing suites against Ruby 2.2 -.knapsack-ruby22: &knapsack-ruby22 - <<: *knapsack +.ruby-22: &ruby22 image: "ruby:2.2" only: - master @@ -124,37 +140,45 @@ spinach 9 10: *knapsack paths: - vendor -rspec 0 20 ruby22: *knapsack-ruby22 -rspec 1 20 ruby22: *knapsack-ruby22 -rspec 2 20 ruby22: *knapsack-ruby22 -rspec 3 20 ruby22: *knapsack-ruby22 -rspec 4 20 ruby22: *knapsack-ruby22 -rspec 5 20 ruby22: *knapsack-ruby22 -rspec 6 20 ruby22: *knapsack-ruby22 -rspec 7 20 ruby22: *knapsack-ruby22 -rspec 8 20 ruby22: *knapsack-ruby22 -rspec 9 20 ruby22: *knapsack-ruby22 -rspec 10 20 ruby22: *knapsack-ruby22 -rspec 11 20 ruby22: *knapsack-ruby22 -rspec 12 20 ruby22: *knapsack-ruby22 -rspec 13 20 ruby22: *knapsack-ruby22 -rspec 14 20 ruby22: *knapsack-ruby22 -rspec 15 20 ruby22: *knapsack-ruby22 -rspec 16 20 ruby22: *knapsack-ruby22 -rspec 17 20 ruby22: *knapsack-ruby22 -rspec 18 20 ruby22: *knapsack-ruby22 -rspec 19 20 ruby22: *knapsack-ruby22 +.rspec-knapsack-ruby22: &rspec-knapsack-ruby22 + <<: *rspec-knapsack + <<: *ruby-22 -spinach 0 10 ruby22: *knapsack-ruby22 -spinach 1 10 ruby22: *knapsack-ruby22 -spinach 2 10 ruby22: *knapsack-ruby22 -spinach 3 10 ruby22: *knapsack-ruby22 -spinach 4 10 ruby22: *knapsack-ruby22 -spinach 5 10 ruby22: *knapsack-ruby22 -spinach 6 10 ruby22: *knapsack-ruby22 -spinach 7 10 ruby22: *knapsack-ruby22 -spinach 8 10 ruby22: *knapsack-ruby22 -spinach 9 10 ruby22: *knapsack-ruby22 +.spinach-knapsack-ruby22: &spinach-knapsack-ruby22 + <<: *rspec-knapsack + <<: *ruby-22 + +rspec 0 20 ruby22: *rspec-knapsack-ruby22 +rspec 1 20 ruby22: *rspec-knapsack-ruby22 +rspec 2 20 ruby22: *rspec-knapsack-ruby22 +rspec 3 20 ruby22: *rspec-knapsack-ruby22 +rspec 4 20 ruby22: *rspec-knapsack-ruby22 +rspec 5 20 ruby22: *rspec-knapsack-ruby22 +rspec 6 20 ruby22: *rspec-knapsack-ruby22 +rspec 7 20 ruby22: *rspec-knapsack-ruby22 +rspec 8 20 ruby22: *rspec-knapsack-ruby22 +rspec 9 20 ruby22: *rspec-knapsack-ruby22 +rspec 10 20 ruby22: *rspec-knapsack-ruby22 +rspec 11 20 ruby22: *rspec-knapsack-ruby22 +rspec 12 20 ruby22: *rspec-knapsack-ruby22 +rspec 13 20 ruby22: *rspec-knapsack-ruby22 +rspec 14 20 ruby22: *rspec-knapsack-ruby22 +rspec 15 20 ruby22: *rspec-knapsack-ruby22 +rspec 16 20 ruby22: *rspec-knapsack-ruby22 +rspec 17 20 ruby22: *rspec-knapsack-ruby22 +rspec 18 20 ruby22: *rspec-knapsack-ruby22 +rspec 19 20 ruby22: *rspec-knapsack-ruby22 + +spinach 0 10 ruby22: *spinach-knapsack-ruby22 +spinach 1 10 ruby22: *spinach-knapsack-ruby22 +spinach 2 10 ruby22: *spinach-knapsack-ruby22 +spinach 3 10 ruby22: *spinach-knapsack-ruby22 +spinach 4 10 ruby22: *spinach-knapsack-ruby22 +spinach 5 10 ruby22: *spinach-knapsack-ruby22 +spinach 6 10 ruby22: *spinach-knapsack-ruby22 +spinach 7 10 ruby22: *spinach-knapsack-ruby22 +spinach 8 10 ruby22: *spinach-knapsack-ruby22 +spinach 9 10 ruby22: *spinach-knapsack-ruby22 # Other generic tests diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 247383aa46..d6fb1a34e8 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,12 +1,16 @@ #!/bin/bash retry() { - for i in $(seq 1 3); do + if eval "$@"; then + return 0 + fi + + for i in 2 1; do + sleep 3s + echo "Retrying $i..." if eval "$@"; then return 0 fi - sleep 3s - echo "Retrying..." done return 1 } From 9b76bb30e834d2fc78f628cd16b00f2e77f305fe Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 13:10:40 +0200 Subject: [PATCH 343/507] Fix .gitlab-ci.yml --- .gitlab-ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 095c9005d1..764b82889e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,7 +34,7 @@ stages: # Prepare and merge knapsack tests -.knapsack_state: &knapsack_state +.knapsack-state: &knapsack-state services: [] variables: USE_DB: "false" @@ -48,7 +48,7 @@ stages: - knapsack/ knapsack: - <<: *rspec-knapsack_state + <<: *knapsack-state stage: prepare script: - mkdir -p knapsack/ @@ -56,7 +56,7 @@ knapsack: - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json' update-knapsack: - <<: *rspec-knapsack_state + <<: *knapsack-state stage: post-test script: - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json @@ -65,7 +65,7 @@ update-knapsack: # Execute all testing suites -.rspec-knapsack: &knapsack +.rspec-knapsack: &rspec-knapsack stage: test script: - bundle exec rake assets:precompile 2>/dev/null @@ -80,7 +80,7 @@ update-knapsack: paths: - knapsack/ -.spinach-knapsack: &knapsack +.spinach-knapsack: &spinach-knapsack stage: test script: - bundle exec rake assets:precompile 2>/dev/null @@ -131,7 +131,7 @@ spinach 9 10: *spinach-knapsack # Execute all testing suites against Ruby 2.2 -.ruby-22: &ruby22 +.ruby-22: &ruby-22 image: "ruby:2.2" only: - master From 8988c8743d2f74b7939a39fbd1c6ebcaa5ac6647 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 13:14:37 +0200 Subject: [PATCH 344/507] Fix remaining test offenses --- spec/models/project_spec.rb | 2 +- spec/services/create_commit_builds_service_spec.rb | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3431d0435b..553556ed32 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -408,7 +408,7 @@ describe Project, models: true do it { is_expected.to eq(pipeline) } context 'return latest' do - let(:pipeline2) { create :pipeline, project: project, ref: 'master' } + let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' } before do pipeline diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 77247a1d5a..a5b4d9f05d 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -74,7 +74,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(pipeline).to be_persisted expect(pipeline.builds.any?).to be false expect(pipeline.status).to eq('failed') @@ -95,7 +95,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(pipeline).to be_persisted expect(pipeline.builds.any?).to be false expect(pipeline.status).to eq("skipped") @@ -110,7 +110,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(pipeline).to be_persisted expect(pipeline.builds.first.name).to eq("staging") @@ -124,7 +124,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(pipeline).to be_persisted expect(pipeline.builds.any?).to be false expect(pipeline.status).to eq("skipped") @@ -141,7 +141,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(pipeline).to be_persisted expect(pipeline.builds.count(:all)).to eq(2) @@ -150,7 +150,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(pipeline).to be_persisted expect(pipeline.builds.count(:all)).to eq(2) end @@ -165,7 +165,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) expect(pipeline).to be_persisted expect(pipeline.status).to eq("failed") From b0eb4cb4a73d78b45cc86b3714d7493484b2b5e7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 13:19:15 +0200 Subject: [PATCH 345/507] Rename @ci_commit to @pipeline in MergeRequestController --- app/controllers/projects/merge_requests_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e96e816bcd..0de3442088 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -317,8 +317,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff - @ci_commit = @merge_request.pipeline - @statuses = @ci_commit.statuses if @ci_commit + @pipeline = @merge_request.pipeline + @statuses = @pipeline.statuses if @pipeline if @merge_request.locked_long_ago? @merge_request.unlock_mr @@ -327,8 +327,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_widget_vars - @ci_commit = @merge_request.pipeline - @ci_commits = [@ci_commit].compact + @pipeline = @merge_request.pipeline + @pipelines = [@pipeline].compact closes_issues end From f4cedacc7bf8209aa43c6d1407acf999ec64475d Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 6 Jun 2016 15:14:36 +0100 Subject: [PATCH 346/507] Optimise email CSS for speed with Premailer Remove all descendant selectors from the push email styling, to drastically reduce CPU time when inlining the CSS for syntax-highlighted diffs. Background: Premailer is a Ruby gem that inlines CSS styles from an external stylesheet before emails are sent, so that they are compatible with Gmail. At a high level, it parses the CSS files it finds, and parses the email body with Nokogiri. It then loops through the selectors in the CSS, using Nokogiri to find matching elements, and adds inline styles. (It does more than this, like merging styles applied to the same element, but that's not relevant to this issue.) Nokogiri converts CSS selectors to XPath first, like so: Nokogiri::CSS.xpath_for('foo bar') # => ["//foo//bar"] On documents with high node counts (say, a syntax-highlighted copy of jQuery), having both descendant selectors is very expensive. Both `//foo/bar` and `//bar` will be much more efficient, although neither are directly equivalent. An example, on a document containing two syntax-highlighted copies of jQuery: Benchmark.realtime { p doc.search('.o').count } # 9476 # => 0.3462457580026239 Benchmark.realtime { p doc.search('.code.white .o').count } # 9476 # => 85.51952634402551 The performance is similar for selectors which _don't_ match any elements, and as Premailer loops through all the available selectors, we want to avoid all descendant selectors in push emails. Because of the theming support in the web UI, all syntax highlighting selectors are descendant selectors of classes like `.code.white` or `.code.monokai`. There are over 60 CSS classes for syntax highlighting styles alone, all of which are expressed in the inefficient form above. In emails we always use the white theme, and were reusing the same CSS file. But in emails, we don't need to descend from `.code.white` as that will always be the theme, and we can also remove some other selectors that are only applicable to the web UI. For the remaining descendant selectors, we can convert them to child selectors, type selectors, or class selectors as appropriate. As in the example above, having no descendant selectors at all in the push email CSS can provide a drastic (and surprising) performance improvement. --- .../mailers/repository_push_email.scss | 183 +++++++++++++++--- app/assets/stylesheets/notify.scss | 16 +- 2 files changed, 169 insertions(+), 30 deletions(-) diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss index 001994db97..7f645d3089 100644 --- a/app/assets/stylesheets/mailers/repository_push_email.scss +++ b/app/assets/stylesheets/mailers/repository_push_email.scss @@ -1,5 +1,15 @@ @import "framework/variables"; +// This file is largely copied from `highlight/white.scss`, but modified to +// avoid all descendant selectors (`table td`). This is because the CSS inlining +// we use performs dramatically worse on descendant selectors than the +// alternatives. +// +// +// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of +// preference): plain class selectors, type (element name) selectors, or +// explicit child selectors. + table.code { width: 100%; font-family: monospace; @@ -11,33 +21,162 @@ table.code { -premailer-cellspacing: 0; -premailer-width: 100%; - td { + > tr > td { line-height: $code_line_height; font-family: monospace; font-size: $code_font_size; - } - td.diff-line-num { - margin: 0; - padding: 0; - border: none; - background: $background-color; - color: rgba(0, 0, 0, 0.3); - padding: 0 5px; - border-right: 1px solid $border-color; - text-align: right; - min-width: 35px; - max-width: 50px; - width: 35px; - } + &.diff-line-num { + margin: 0; + padding: 0; + border: none; + padding: 0 5px; + border-right: 1px solid; + text-align: right; + min-width: 35px; + max-width: 50px; + width: 35px; + } - td.line_content { - display: block; - margin: 0; - padding: 0 0.5em; - border: none; - white-space: pre; + &.line_content { + display: block; + margin: 0; + padding: 0 0.5em; + border: none; + white-space: pre; + } } } -@import "highlight/white"; +.line-numbers, .diff-line-num { + background-color: $background-color; +} + +.diff-line-num, .diff-line-num a { + color: $black-transparent; +} + +pre.code, .diff-line-num { + border-color: $table-border-gray; +} + +.code.white, pre.code, .line_content { + background-color: #fff; + color: #333; +} + +.diff-line-num { + &.old { + background-color: $line-number-old; + border-color: $line-removed-dark; + } + + &.new { + background-color: $line-number-new; + border-color: $line-added-dark; + } + + &.hll:not(.empty-cell) { + background-color: $line-number-select; + border-color: $line-select-yellow-dark; + } +} + +.line_content { + &.old { + background-color: $line-removed; + + > .line > span.idiff, > .line > span > span.idiff { + background-color: $line-removed-dark; + } + } + + &.new { + background-color: $line-added; + + > .line > span.idiff, > .line > span > span.idiff { + background-color: $line-added-dark; + } + } + + &.match { + color: $black-transparent; + background-color: $match-line; + } + + &.hll:not(.empty-cell) { + background-color: $line-select-yellow; + } +} + +pre > .hll { + background-color: #f8eec7 !important; +} + +span.highlight_word { + background-color: #fafe3d !important; +} + +.hll { background-color: #f8f8f8 } +.c { color: #998; font-style: italic; } +.err { color: #a61717; background-color: #e3d2d2; } +.k { font-weight: bold; } +.o { font-weight: bold; } +.cm { color: #998; font-style: italic; } +.cp { color: #999; font-weight: bold; } +.c1 { color: #998; font-style: italic; } +.cs { color: #999; font-weight: bold; font-style: italic; } +.gd { color: #000; background-color: #fdd; } +.gd .x { color: #000; background-color: #faa; } +.ge { font-style: italic; } +.gr { color: #a00; } +.gh { color: #999; } +.gi { color: #000; background-color: #dfd; } +.gi .x { color: #000; background-color: #afa; } +.go { color: #888; } +.gp { color: #555; } +.gs { font-weight: bold; } +.gu { color: #800080; font-weight: bold; } +.gt { color: #a00; } +.kc { font-weight: bold; } +.kd { font-weight: bold; } +.kn { font-weight: bold; } +.kp { font-weight: bold; } +.kr { font-weight: bold; } +.kt { color: #458; font-weight: bold; } +.m { color: #099; } +.s { color: #d14; } +.n { color: #333; } +.na { color: teal; } +.nb { color: #0086b3; } +.nc { color: #458; font-weight: bold; } +.no { color: teal; } +.ni { color: purple; } +.ne { color: #900; font-weight: bold; } +.nf { color: #900; font-weight: bold; } +.nn { color: #555; } +.nt { color: navy; } +.nv { color: teal; } +.ow { font-weight: bold; } +.w { color: #bbb; } +.mf { color: #099; } +.mh { color: #099; } +.mi { color: #099; } +.mo { color: #099; } +.sb { color: #d14; } +.sc { color: #d14; } +.sd { color: #d14; } +.s2 { color: #d14; } +.se { color: #d14; } +.sh { color: #d14; } +.si { color: #d14; } +.sx { color: #d14; } +.sr { color: #009926; } +.s1 { color: #d14; } +.ss { color: #990073; } +.bp { color: #999; } +.vc { color: teal; } +.vg { color: teal; } +.vi { color: teal; } +.il { color: #099; } +.gc { color: #999; background-color: #eaf2f5; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index 0a13a7e0b5..fc12964872 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -6,19 +6,19 @@ p.details { font-style: italic; color: #777 } -.footer p { +.footer > p { font-size: small; color: #777 } pre.commit-message { white-space: pre-wrap; } -.file-stats a { +.file-stats > a { text-decoration: none; -} -.file-stats .new-file { - color: #090; -} -.file-stats .deleted-file { - color: #b00; + > .new-file { + color: #090; + } + > .deleted-file { + color: #b00; + } } From 59376bb3604924fd9e91311eab24ab6454363cfa Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 14:35:17 +0200 Subject: [PATCH 347/507] Fix knapsack spinach execution --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 764b82889e..d098bf73ff 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -90,7 +90,7 @@ update-knapsack: - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH} - - knapsack spinach[-r rerun] + - knapsack spinach "-r rerun" # retry failed tests 3 times - retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: From ee26c3cab4651c8876efc45b6a63539727e6a42e Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 7 Jun 2016 11:57:09 +0100 Subject: [PATCH 348/507] Fix label order by priority on labels page --- app/models/label.rb | 4 +- .../projects/labels_controller_spec.rb | 53 +++++++++++++++++++ .../labels/update_prioritization_spec.rb | 9 +++- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 spec/controllers/projects/labels_controller_spec.rb diff --git a/app/models/label.rb b/app/models/label.rb index 9760f1aefa..49c352cc23 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -33,11 +33,11 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } def self.prioritized - where.not(priority: nil).reorder(:title) + where.not(priority: nil).reorder(:priority, :title) end def self.unprioritized - where(priority: nil).reorder(:title) + where(priority: nil) end alias_attribute :name, :title diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb new file mode 100644 index 0000000000..ab1dd34ed5 --- /dev/null +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Projects::LabelsController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe 'GET #index' do + def create_label(attributes) + create(:label, attributes.merge(project: project)) + end + + before do + 15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") } + 5.times { |i| create_label(title: "label #{100 - i}") } + + + get :index, namespace_id: project.namespace.to_param, project_id: project.to_param + end + + context '@prioritized_labels' do + let(:prioritized_labels) { assigns(:prioritized_labels) } + + it 'contains only prioritized labels' do + expect(prioritized_labels).to all(have_attributes(priority: a_value > 0)) + end + + it 'is sorted by priority, then label title' do + priorities_and_titles = prioritized_labels.pluck(:priority, :title) + + expect(priorities_and_titles.sort).to eq(priorities_and_titles) + end + end + + context '@labels' do + let(:labels) { assigns(:labels) } + + it 'contains only unprioritized labels' do + expect(labels).to all(have_attributes(priority: nil)) + end + + it 'is sorted by label title' do + titles = labels.pluck(:title) + + expect(titles.sort).to eq(titles) + end + end + end +end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 2a0bf6548d..8550d279d0 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -55,7 +55,7 @@ feature 'Prioritize labels', feature: true do end end - scenario 'user can sort prioritized labels', js: true do + scenario 'user can sort prioritized labels and persist across reloads', js: true do bug = create(:label, title: 'bug', priority: 1) wontfix = create(:label, title: 'wontfix', priority: 2) @@ -75,6 +75,13 @@ feature 'Prioritize labels', feature: true do expect(first('li')).to have_content('wontfix') expect(page.all('li').last).to have_content('bug') end + + visit current_url + + page.within('.prioritized-labels') do + expect(first('li')).to have_content('wontfix') + expect(page.all('li').last).to have_content('bug') + end end end From ca05ea6b732cad9d7b9d809f6b66ba4fca904857 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 15:15:32 +0200 Subject: [PATCH 349/507] Remove knapsack_merger since we don't use it anyway --- spec/knapsack_merger.rb | 41 ----------------------------------------- spec/spec_helper.rb | 1 - 2 files changed, 42 deletions(-) delete mode 100644 spec/knapsack_merger.rb diff --git a/spec/knapsack_merger.rb b/spec/knapsack_merger.rb deleted file mode 100644 index c6bcefe846..0000000000 --- a/spec/knapsack_merger.rb +++ /dev/null @@ -1,41 +0,0 @@ -begin - class Knapsack::Report - alias_method :save_without_leading_existing_report, :save - - def load_existing_report - Knapsack::Presenter.existing_report = open - rescue - false - end - - def save - load_existing_report - save_without_leading_existing_report - end - end - - class << Knapsack::Presenter - attr_accessor :existing_report - - def initialize - @existing_report = [] - end - - def report_hash - return current_report_hash unless existing_report - existing_report.merge(current_report_hash).sort.to_h - end - - def current_report_hash - Knapsack.tracker.test_files_with_time - end - - def report_yml - report_hash.to_yaml - end - - def report_json - JSON.pretty_generate(report_hash) - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 84b9ee75f6..a20f4c0597 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,7 +16,6 @@ require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'rspec/retry' require 'knapsack' -require_relative 'knapsack_merger' Knapsack::Adapters::RSpecAdapter.bind From b5464726aae70f8b3344afe6eaf3970589bf9e9a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 7 Jun 2016 15:19:15 +0200 Subject: [PATCH 350/507] Added 8.9 install/update guides [ci skip] --- doc/install/installation.md | 6 +- doc/update/8.8-to-8.9.md | 162 ++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 doc/update/8.8-to-8.9.md diff --git a/doc/install/installation.md b/doc/install/installation.md index 1318b3d1fa..d9290b1fa7 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -269,9 +269,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-8-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-9-stable gitlab -**Note:** You can change `8-8-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-9-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -394,7 +394,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout v0.7.4 + sudo -u git -H git checkout v0.7.5 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md new file mode 100644 index 0000000000..67a986ead5 --- /dev/null +++ b/doc/update/8.8-to-8.9.md @@ -0,0 +1,162 @@ +# From 8.8 to 8.9 + +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-9-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-9-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 v3.0.0 +``` + +### 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.5 +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 + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-8-stable:config/gitlab.yml.example origin/8-9-stable:config/gitlab.yml.example +``` + +#### 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-8-stable:lib/support/nginx/gitlab-ssl origin/8-9-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-8-stable:lib/support/nginx/gitlab origin/8-9-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.7) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.7 to 8.8](8.7-to-8.8.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 12dd7bd7e59c8ce95c29f4ab2f08a6d9c513b734 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 15:42:03 +0200 Subject: [PATCH 351/507] Remove stage notifications [ci skip] --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d098bf73ff..fc1e43fcd4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,7 +30,6 @@ stages: - prepare - test - post-test -- notifications # Prepare and merge knapsack tests From 59e5f4705b37014a2b3cc12959c3ac7e65328ec3 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 7 Jun 2016 10:57:58 -0300 Subject: [PATCH 352/507] Check if GitHub rate limite API was reached before update Webhooks --- lib/gitlab/github_import/importer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 442b4c389f..5ef9d66ba6 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -146,6 +146,7 @@ module Gitlab def update_webhooks(hooks, options) hooks.each do |hook| + sleep rate_limit_sleep_time if rate_limit_exceed? client.edit_hook(repo, hook.id, hook.name, hook.config, options) end end From 664afebbe9e62f13074c999d84674a0e37fa8cc2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 3 Jun 2016 09:53:04 -0600 Subject: [PATCH 353/507] Start styling sub nav --- app/assets/stylesheets/framework/nav.scss | 21 +++++++++ app/views/projects/commits/_head.html.haml | 39 ++++++++-------- app/views/projects/commits/show.html.haml | 53 ++++++++++++---------- app/views/projects/tree/show.html.haml | 19 ++++---- 4 files changed, 80 insertions(+), 52 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 7eb7a8e454..6d44ee6c26 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -67,6 +67,27 @@ color: #78a; } } + + &.sub-nav { + background-color: $background-color; + + .container-fluid { + background-color: $background-color; + } + + li { + + a { + margin: 0; + padding: 11px 10px 9px; + } + + &.active a { + border-bottom: none; + color: $link-underline-blue; + } + } + } } .top-area { diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 1c136133ab..73af27a8a3 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,24 +1,25 @@ -%ul.nav-links - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_files_path(@project) do - Files +%ul.nav-links.sub-nav + %div{ class: (container_class) } + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project) do + Files - = nav_link(controller: [:commit, :commits]) do - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - Commits + = nav_link(controller: [:commit, :commits]) do + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + Commits - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do - Network + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + Network - = nav_link(controller: :compare) do - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do - Compare + = nav_link(controller: :compare) do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do + Compare - = nav_link(html_options: {class: branches_tab_class}) do - = link_to namespace_project_branches_path(@project.namespace, @project) do - Branches + = nav_link(html_options: {class: branches_tab_class}) do + = link_to namespace_project_branches_path(@project.namespace, @project) do + Branches - = nav_link(controller: [:tags, :releases]) do - = link_to namespace_project_tags_path(@project.namespace, @project) do - Tags + = nav_link(controller: [:tags, :releases]) do + = link_to namespace_project_tags_path(@project.namespace, @project) do + Tags diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 2c21923ed4..288a912f40 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,3 +1,5 @@ +- @no_container = true + - page_title "Commits", @ref = content_for :meta_tags do - if current_user @@ -5,37 +7,38 @@ = render "head" -.row-content-block.second-block - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'commits' +%div{ class: (container_class) } + .row-content-block.second-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'commits' + + .block-controls.hidden-xs.hidden-sm + - if @merge_request.present? + .control + = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' + - elsif create_mr_button?(@repository.root_ref, @ref) + .control + = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do + = icon('plus') + Create Merge Request - .block-controls.hidden-xs.hidden-sm - - if @merge_request.present? .control - = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' - - elsif create_mr_button?(@repository.root_ref, @ref) - .control - = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do - = icon('plus') - Create Merge Request + = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do + = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - .control - = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do - = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - - - if current_user && current_user.private_token - .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do - = icon("rss") + - if current_user && current_user.private_token + .control + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do + = icon("rss") - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs -%div{id: dom_id(@project)} - #commits-list.content_list= render "commits", project: @project -.clear -= spinner + %div{id: dom_id(@project)} + #commits-list.content_list= render "commits", project: @project + .clear + = spinner :javascript CommitsList.init(#{@limit}); diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 59f60c4687..2abcfcdd7b 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -1,3 +1,5 @@ +- @no_container = true + - page_title @path.presence || "Files", @ref = content_for :meta_tags do - if current_user @@ -5,13 +7,14 @@ = render 'projects/last_push' = render "projects/commits/head" -.tree-controls - = render 'projects/find_file_link' - - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true +%div{ class: (container_class) } + .tree-controls + = render 'projects/find_file_link' + - if can? current_user, :download_code, @project + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true -#tree-holder.tree-holder.clearfix - .nav-block - = render 'projects/tree/tree_header', tree: @tree + #tree-holder.tree-holder.clearfix + .nav-block + = render 'projects/tree/tree_header', tree: @tree - = render 'projects/tree/tree_content', tree: @tree + = render 'projects/tree/tree_content', tree: @tree From e141a1c6f14df7b83cbb24190f9786b6fc36cd49 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 27 May 2016 15:56:43 +0100 Subject: [PATCH 354/507] Notifications dropdown on project page now has descriptions This is part of #12758 --- .../stylesheets/framework/dropdowns.scss | 26 +++++++++++++++---- app/helpers/notifications_helper.rb | 22 +++++++++++++--- app/models/notification_setting.rb | 2 +- .../projects/buttons/_notifications.html.haml | 4 +-- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 28634d0c59..cf664627f8 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -122,10 +122,8 @@ a { display: block; position: relative; - padding-left: 10px; - padding-right: 10px; + padding: 5px 10px; color: $dropdown-link-color; - line-height: 34px; text-overflow: ellipsis; border-radius: 2px; white-space: nowrap; @@ -162,6 +160,16 @@ } } +.dropdown-menu-large { + width: 340px; +} + +.dropdown-menu-no-wrap { + a { + white-space: normal; + } +} + .dropdown-menu-full-width { width: 100%; } @@ -236,8 +244,7 @@ &::before { position: absolute; left: 5px; - top: 50%; - margin-top: -7px; + top: 8px; font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; @@ -532,3 +539,12 @@ background-color: $calendar-unselectable-bg; } } + +.dropdown-menu-inner-title { + display: block; + font-weight: 600; +} + +.dropdown-menu-inner-content { + display: block; +} diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 54ab9179ef..b8e64b3890 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -31,6 +31,21 @@ module NotificationsHelper end end + def notification_description(level) + case level.to_sym + when :participating + 'You will only receive notifications from related resources' + when :mention + 'You will receive notifications only for comments in which you were @mentioned' + when :watch + 'You will receive notifications for any activity' + when :disabled + 'You will not get any notifications via email' + when :global + 'Use your global notification setting' + end + end + def notification_list_item(level, setting) title = notification_title(level) @@ -39,9 +54,10 @@ module NotificationsHelper notification_title: title } - content_tag(:li, class: ('active' if setting.level == level)) do - link_to '#', class: 'update-notification', data: data do - notification_icon(level, title) + content_tag(:li, role: "menuitem") do + link_to '#', class: "update-notification #{('is-active' if setting.level == level)}", data: data do + link_output = content_tag(:strong, title, class: 'dropdown-menu-inner-title') + link_output << content_tag(:span, notification_description(level), class: 'dropdown-menu-inner-content') end end end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 5001738f41..17fb15b08d 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,5 +1,5 @@ class NotificationSetting < ActiveRecord::Base - enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 } + enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0 } default_value_for :level, NotificationSetting.levels[:global] diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 1d05da5058..2348ceaa92 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -2,10 +2,10 @@ = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f| = f.hidden_field :level .dropdown - %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} + %button.btn.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } } = icon('bell') = notification_title(@notification_setting.level) = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown + %ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-large{ role: "menu" } - NotificationSetting.levels.each do |level| = notification_list_item(level.first, @notification_setting) From 8fa8cba02ecdeecbbe020a049f5923afeceb6a1b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 31 May 2016 09:36:59 +0100 Subject: [PATCH 355/507] CHANGELOG item --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index ce8f2823c2..c55eb09ef8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -52,6 +52,7 @@ v 8.8.4 (unreleased) - Import GitHub repositories respecting the API rate limit - Fix importer for GitHub comments on diff - Disable Webhooks before proceeding with the GitHub import + - Added descriptions to notification settings dropdown v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 From 51a62fce040a307955d6e66d0a22f1bbfa068b38 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 31 May 2016 11:38:54 +0100 Subject: [PATCH 356/507] Fixed failing tests --- app/views/projects/buttons/_notifications.html.haml | 2 +- features/steps/project/project.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 2348ceaa92..3b97dc9328 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -2,7 +2,7 @@ = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f| = f.hidden_field :level .dropdown - %button.btn.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } } + %button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } } = icon('bell') = notification_title(@notification_setting.level) = icon('caret-down') diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index a1785311c2..2a1a8e776f 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -126,7 +126,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I click notifications drop down button' do - click_link 'notifications-button' + find('#notifications-button').click end step 'I choose Mention setting' do From f62df363b5daa83b5ba81d75c63816976648446d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 6 Jun 2016 08:39:05 +0100 Subject: [PATCH 357/507] Updated colors --- app/assets/stylesheets/framework/dropdowns.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cf664627f8..1ce7c57ebc 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -542,9 +542,11 @@ .dropdown-menu-inner-title { display: block; + color: $gl-title-color; font-weight: 600; } .dropdown-menu-inner-content { display: block; + color: $gl-placeholder-color; } From 5db95f5e6991f61193b57aa22447479ff5dede93 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 3 Jun 2016 12:09:45 -0600 Subject: [PATCH 358/507] Finish styling sub nav --- app/assets/stylesheets/framework/blocks.scss | 5 + app/assets/stylesheets/pages/projects.scss | 8 +- app/views/projects/branches/index.html.haml | 61 +++++------ app/views/projects/builds/index.html.haml | 102 ++++++++++--------- app/views/projects/commits/show.html.haml | 2 +- app/views/projects/compare/index.html.haml | 26 ++--- app/views/projects/network/_head.html.haml | 13 ++- app/views/projects/network/show.html.haml | 27 ++--- app/views/projects/pipelines/_head.html.haml | 27 ++--- app/views/projects/pipelines/index.html.haml | 96 ++++++++--------- app/views/projects/tags/index.html.haml | 44 ++++---- 11 files changed, 217 insertions(+), 194 deletions(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 6981f834d3..fab96404a6 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -61,6 +61,11 @@ margin-bottom: -$gl-padding; } + &.content-component-block { + padding: 11px 0; + background-color: $white-light; + } + .title { color: $gl-text-color; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index edef336481..ad78eb6576 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -489,9 +489,11 @@ pre.light-well { margin: 0; } -.project-show-activity { - .activity-filter-block { - margin-top: -1px; + +.activity-filter-block { + .controls { + padding-bottom: 10px; + border-bottom: 1px solid $border-color; } } diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 08148b1a18..0d59c3884c 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,32 +1,35 @@ +- @no_container = true - page_title "Branches" = render "projects/commits/head" -.row-content-block - .pull-right - - if can? current_user, :push_code, @project - = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do - = icon('plus') - New branch -   - .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = @sort.humanize - - else - Name - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to namespace_project_branches_path(sort: nil) do + +%div{ class: (container_class) } + .row-content-block.second-block.content-component-block + .pull-right + - if can? current_user, :push_code, @project + = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do + = icon('plus') + New branch +   + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + - if @sort.present? + = @sort.humanize + - else Name - = link_to namespace_project_branches_path(sort: 'recently_updated') do - = sort_title_recently_updated - = link_to namespace_project_branches_path(sort: 'last_updated') do - = sort_title_oldest_updated - .oneline - Protected branches can be managed in project settings -- unless @branches.empty? - %ul.content-list.all-branches - - @branches.each do |branch| - = render "projects/branches/branch", branch: branch - = paginate @branches, theme: 'gitlab' + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to namespace_project_branches_path(sort: nil) do + Name + = link_to namespace_project_branches_path(sort: 'recently_updated') do + = sort_title_recently_updated + = link_to namespace_project_branches_path(sort: 'last_updated') do + = sort_title_oldest_updated + .oneline + Protected branches can be managed in project settings + - unless @branches.empty? + %ul.content-list.all-branches + - @branches.each do |branch| + = render "projects/branches/branch", branch: branch + = paginate @branches, theme: 'gitlab' diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 818d5d28f0..55d2ac89eb 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,62 +1,64 @@ +- @no_container = true - page_title "Builds" = render "projects/pipelines/head" -.top-area - %ul.nav-links - %li{class: ('active' if @scope.nil?)} - = link_to project_builds_path(@project) do - All - %span.badge.js-totalbuilds-count - = number_with_delimiter(@all_builds.count(:id)) +%div{ class: (container_class) } + .top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to project_builds_path(@project) do + All + %span.badge.js-totalbuilds-count + = number_with_delimiter(@all_builds.count(:id)) - %li{class: ('active' if @scope == 'running')} - = link_to project_builds_path(@project, scope: :running) do - Running - %span.badge.js-running-count - = number_with_delimiter(@all_builds.running_or_pending.count(:id)) + %li{class: ('active' if @scope == 'running')} + = link_to project_builds_path(@project, scope: :running) do + Running + %span.badge.js-running-count + = number_with_delimiter(@all_builds.running_or_pending.count(:id)) - %li{class: ('active' if @scope == 'finished')} - = link_to project_builds_path(@project, scope: :finished) do - Finished - %span.badge.js-running-count - = number_with_delimiter(@all_builds.finished.count(:id)) + %li{class: ('active' if @scope == 'finished')} + = link_to project_builds_path(@project, scope: :finished) do + Finished + %span.badge.js-running-count + = number_with_delimiter(@all_builds.finished.count(:id)) - .nav-controls - - if can?(current_user, :update_build, @project) - - if @all_builds.running_or_pending.any? - = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), - data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + .nav-controls + - if can?(current_user, :update_build, @project) + - if @all_builds.running_or_pending.any? + = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), + data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - - unless @repository.gitlab_ci_yml - = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + - unless @repository.gitlab_ci_yml + = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' - = link_to ci_lint_path, class: 'btn btn-default' do - = icon('wrench') - %span CI Lint + = link_to ci_lint_path, class: 'btn btn-default' do + = icon('wrench') + %span CI Lint -%ul.content-list - - if @builds.blank? - %li - .nothing-here-block No builds to show - - else - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Commit - %th Ref - %th Stage - %th Name - %th Tags - %th Duration - %th Finished at - - if @project.build_coverage_enabled? - %th Coverage - %th + %ul.content-list + - if @builds.blank? + %li + .nothing-here-block No builds to show + - else + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Commit + %th Ref + %th Stage + %th Name + %th Tags + %th Duration + %th Finished at + - if @project.build_coverage_enabled? + %th Coverage + %th - = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? + = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? - = paginate @builds, theme: 'gitlab' + = paginate @builds, theme: 'gitlab' diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 288a912f40..76ba0bea36 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -8,7 +8,7 @@ = render "head" %div{ class: (container_class) } - .row-content-block.second-block + .row-content-block.second-block.content-component-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 0b8ed23b30..c322942aeb 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -1,16 +1,18 @@ +- @no_container = true - page_title "Compare" = render "projects/commits/head" -.row-content-block - Compare branches, tags or commit ranges. - %br - Fill input field with commit id like - %code.label-branch 4eedf23 - or branch/tag name like - %code.label-branch master - and press compare button for the commits list and a code diff. - %br - Changes are shown from the version in the first field to the version in the second field. +%div{ class: (container_class) } + .row-content-block.second-block.content-component-block + Compare branches, tags or commit ranges. + %br + Fill input field with commit id like + %code.label-branch 4eedf23 + or branch/tag name like + %code.label-branch master + and press compare button for the commits list and a code diff. + %br + Changes are shown from the version in the first field to the version in the second field. -.prepend-top-20 - = render "form" + .prepend-top-20 + = render "form" diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml index c609c505de..86295a3d01 100644 --- a/app/views/projects/network/_head.html.haml +++ b/app/views/projects/network/_head.html.haml @@ -1,6 +1,9 @@ -.row-content-block.append-bottom-default - .tree-ref-holder - = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} +- @no_container = true - .oneline - You can move around the graph by using the arrow keys. +%div{ class: (container_class) } + .row-content-block.second-block.content-component-block + .tree-ref-holder + = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} + + .oneline + You can move around the graph by using the arrow keys. diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 326180ebe4..bf9baaea88 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,20 +1,21 @@ - page_title "Network", @ref = render "projects/commits/head" = render "head" -.project-network - .controls - = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f| - = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha' - = button_tag class: 'btn btn-success' do - = icon('search') - .inline.prepend-left-20 - .checkbox.light - = label_tag :filter_ref do - = check_box_tag :filter_ref, 1, @options[:filter_ref] - %span Begin with the selected commit +%div{ class: (container_class) } + .project-network + .controls + = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f| + = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha' + = button_tag class: 'btn btn-success' do + = icon('search') + .inline.prepend-left-20 + .checkbox.light + = label_tag :filter_ref do + = check_box_tag :filter_ref, 1, @options[:filter_ref] + %span Begin with the selected commit - .network-graph - = spinner nil, true + .network-graph + = spinner nil, true :javascript network_graph = new Network({ diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index d284694efb..f278d4e053 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -1,14 +1,15 @@ -%ul.nav-links - - if project_nav_tab? :pipelines - = nav_link(controller: :pipelines) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines - %span.badge.count.ci_counter= number_with_delimiter(@project.pipelines.running_or_pending.count) +%ul.nav-links.sub-nav + %div{ class: (container_class) } + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines + %span.badge.count.ci_counter= number_with_delimiter(@project.pipelines.running_or_pending.count) - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - %span - Builds - %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + %span + Builds + %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 453767920b..a78450e09d 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -1,58 +1,60 @@ +- @no_container = true - page_title "Pipelines" = render "projects/pipelines/head" -.top-area - %ul.nav-links - %li{class: ('active' if @scope.nil?)} - = link_to project_pipelines_path(@project) do - All - %span.badge.js-totalbuilds-count - = number_with_delimiter(@pipelines_count) +%div{ class: (container_class) } + .top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to project_pipelines_path(@project) do + All + %span.badge.js-totalbuilds-count + = number_with_delimiter(@pipelines_count) - %li{class: ('active' if @scope == 'running')} - = link_to project_pipelines_path(@project, scope: :running) do - Running - %span.badge.js-running-count - = number_with_delimiter(@running_or_pending_count) + %li{class: ('active' if @scope == 'running')} + = link_to project_pipelines_path(@project, scope: :running) do + Running + %span.badge.js-running-count + = number_with_delimiter(@running_or_pending_count) - %li{class: ('active' if @scope == 'branches')} - = link_to project_pipelines_path(@project, scope: :branches) do - Branches + %li{class: ('active' if @scope == 'branches')} + = link_to project_pipelines_path(@project, scope: :branches) do + Branches - %li{class: ('active' if @scope == 'tags')} - = link_to project_pipelines_path(@project, scope: :tags) do - Tags + %li{class: ('active' if @scope == 'tags')} + = link_to project_pipelines_path(@project, scope: :tags) do + Tags - .nav-controls - - if can? current_user, :create_pipeline, @project - = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do - = icon('plus') - New pipeline + .nav-controls + - if can? current_user, :create_pipeline, @project + = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do + = icon('plus') + New pipeline - - unless @repository.gitlab_ci_yml - = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + - unless @repository.gitlab_ci_yml + = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' - = link_to ci_lint_path, class: 'btn btn-default' do - = icon('wrench') - %span CI Lint + = link_to ci_lint_path, class: 'btn btn-default' do + = icon('wrench') + %span CI Lint -%ul.content-list.pipelines - - stages = @pipelines.stages - - if @pipelines.blank? - %li - .nothing-here-block No pipelines to show - - else - .table-holder - %table.table.builds - %tbody - %th ID - %th Commit - - stages.each do |stage| - %th.stage - %span.has-tooltip{ title: "#{stage.titleize}" } - = stage.titleize.pluralize - %th Duration - %th - = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages + %ul.content-list.pipelines + - stages = @pipelines.stages + - if @pipelines.blank? + %li + .nothing-here-block No pipelines to show + - else + .table-holder + %table.table.builds + %tbody + %th ID + %th Commit + - stages.each do |stage| + %th.stage + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize.pluralize + %th Duration + %th + = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages - = paginate @pipelines, theme: 'gitlab' + = paginate @pipelines, theme: 'gitlab' diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 8f381663e6..9ff805a898 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,28 +1,30 @@ +- @no_container = true - page_title "Tags" = render "projects/commits/head" -.row-content-block - - if can? current_user, :push_code, @project - .pull-right - = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do - = icon('plus') - New tag - .oneline - Tags give the ability to mark specific points in history as being important +%div{ class: (container_class) } + .row-content-block.second-block.content-component-block + - if can? current_user, :push_code, @project + .pull-right + = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do + = icon('plus') + New tag + .oneline + Tags give the ability to mark specific points in history as being important -.tags - - unless @tags.empty? - %ul.content-list - - @tags.each do |tag| - = render 'tag', tag: @repository.find_tag(tag) + .tags + - unless @tags.empty? + %ul.content-list + - @tags.each do |tag| + = render 'tag', tag: @repository.find_tag(tag) - = paginate @tags, theme: 'gitlab' + = paginate @tags, theme: 'gitlab' - - else - .nothing-here-block - Repository has no tags yet. - %br - %small - Use git tag command to add a new one: + - else + .nothing-here-block + Repository has no tags yet. %br - %span.monospace git tag -a v1.4 -m 'version 1.4' + %small + Use git tag command to add a new one: + %br + %span.monospace git tag -a v1.4 -m 'version 1.4' From a528b649bfaec17acc7ecef60e5850eae6be5870 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 3 Jun 2016 12:39:04 -0600 Subject: [PATCH 359/507] Add scrolling tabs to code subnav --- app/assets/stylesheets/framework/nav.scss | 18 +++++++++ app/views/projects/commits/_head.html.haml | 43 ++++++++++++---------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 6d44ee6c26..2c158b813c 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -342,6 +342,24 @@ } } +.scrolling-tabs-container { + position: relative; + + .nav-links { + @include scrolling-links(); + + .fade-right { + @include fade(left, rgba(255, 255, 255, 0.4), $background-color); + right: 0; + } + + .fade-left { + @include fade(right, rgba(255, 255, 255, 0.4), $background-color); + left: 0; + } + } +} + .nav-block { position: relative; diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 73af27a8a3..a72e8ba73a 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,25 +1,28 @@ -%ul.nav-links.sub-nav - %div{ class: (container_class) } - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_files_path(@project) do - Files +.scrolling-tabs-container + %ul.nav-links.sub-nav.scrolling-tabs + %div{ class: (container_class) } + .fade-left + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project) do + Files - = nav_link(controller: [:commit, :commits]) do - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - Commits + = nav_link(controller: [:commit, :commits]) do + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + Commits - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do - Network + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + Network - = nav_link(controller: :compare) do - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do - Compare + = nav_link(controller: :compare) do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do + Compare - = nav_link(html_options: {class: branches_tab_class}) do - = link_to namespace_project_branches_path(@project.namespace, @project) do - Branches + = nav_link(html_options: {class: branches_tab_class}) do + = link_to namespace_project_branches_path(@project.namespace, @project) do + Branches - = nav_link(controller: [:tags, :releases]) do - = link_to namespace_project_tags_path(@project.namespace, @project) do - Tags + = nav_link(controller: [:tags, :releases]) do + = link_to namespace_project_tags_path(@project.namespace, @project) do + Tags + .fade-right From 04dba0e5f80659c28010f3eb1de1aa970f6a1bae Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 6 Jun 2016 17:01:16 -0500 Subject: [PATCH 360/507] Align links and tabs --- app/assets/stylesheets/framework/nav.scss | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 2c158b813c..1b22c88070 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -41,8 +41,7 @@ a { display: inline-block; - padding: 14px; - padding-top: $gl-padding; + padding: $gl-btn-padding; padding-bottom: 11px; margin-bottom: -1px; font-size: 15px; @@ -125,6 +124,10 @@ margin-bottom: 0; border-bottom: none; + li a { + padding: 16px 10px 11px; + } + /* Small devices (phones, tablets, 768px and lower) */ @media (max-width: $screen-sm-max) { width: 100%; @@ -330,8 +333,8 @@ } .nav-control { - .fade-right { + .fade-right { @media (min-width: $screen-xs-max) { right: 67px; } From 8cb00a97ec481aa652d94fcc9e3d61f4215a6298 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 16:16:16 +0200 Subject: [PATCH 361/507] Fix knapsack for master --- .gitlab-ci.yml | 2 +- CHANGELOG | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fc1e43fcd4..5ef3081395 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -144,7 +144,7 @@ spinach 9 10: *spinach-knapsack <<: *ruby-22 .spinach-knapsack-ruby22: &spinach-knapsack-ruby22 - <<: *rspec-knapsack + <<: *spinach-knapsack <<: *ruby-22 rspec 0 20 ruby22: *rspec-knapsack-ruby22 diff --git a/CHANGELOG b/CHANGELOG index 7809fef170..2067017f25 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.9.0 (unreleased) - Fix groups API to list only user's accessible projects - Redesign account and email confirmation emails - Use gitlab-shell v3.0.0 + - Use Knapsack to evenly distribute tests across multiple nodes - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged - Don't allow MRs to be merged when commits were added since the last review / page load - Add DB index on users.state From 7c501895a33e2771097e14ddf7171164ccc27d01 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 2 Jun 2016 15:24:34 -0600 Subject: [PATCH 362/507] Implement compact side nav --- app/assets/stylesheets/framework/header.scss | 36 ++---- app/assets/stylesheets/framework/sidebar.scss | 114 +++--------------- .../stylesheets/framework/variables.scss | 2 +- app/views/layouts/_page.html.haml | 14 +-- app/views/layouts/nav/_dashboard.html.haml | 24 ++-- 5 files changed, 47 insertions(+), 143 deletions(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 0da96c4017..3be83b0fc6 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -79,13 +79,17 @@ header { &.header-collapsed { padding: 0 16px; + + .side-nav-toggle { + display: block; + } } .side-nav-toggle { display: none; position: absolute; left: -10px; - margin: 6px 0; + margin: 8px 0; padding: 6px 10px; border: none; background-color: $background-color; @@ -97,10 +101,6 @@ header { &:focus { outline: none; } - - @media (max-width: $screen-xs-min) { - display: block; - } } } @@ -171,31 +171,21 @@ header { } } -@mixin collapsed-header { - margin-left: $sidebar_collapsed_width; -} - .header-collapsed { - margin-left: $sidebar_collapsed_width; + margin-left: 0; - @media (min-width: $screen-md-min) { - @include collapsed-header; - } - - @media (max-width: $screen-xs-min) { - margin-left: 0; + .header-content { + padding-left: 30px; + transition-duration: .3s; } } .header-expanded { - margin-left: $sidebar_collapsed_width; + margin-left: 0; - @media (min-width: $screen-md-min) { - margin-left: $sidebar_width; - } - - @media (max-width: $screen-xs-min) { - margin-left: 0; + .header-content { + padding-left: $sidebar_width; + transition-duration: .3s; } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 67f491b6d9..0acb010b23 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,9 +1,7 @@ #logo { z-index: 2; - position: absolute; - width: 58px; + position: relative; cursor: pointer; - margin-top: 8px; } .page-with-sidebar { @@ -20,12 +18,6 @@ height: 100%; transition-duration: .3s; } - - .gitlab-text-container-link { - z-index: 1; - position: absolute; - left: 0; - } } .sidebar-wrapper { @@ -50,47 +42,8 @@ .sidebar-wrapper { .header-logo { - border-bottom: 1px solid transparent; - float: left; height: $header-height; - width: $sidebar_width; - position: fixed; - z-index: 999; - overflow: hidden; - transition-duration: .3s; - - a { - float: left; - height: $header-height; - width: 100%; - padding-left: 22px; - overflow: hidden; - outline: none; - transition-duration: .3s; - - img { - width: 36px; - height: 36px; - } - - #tanuki-logo, img { - float: left; - } - - .gitlab-text-container { - width: 230px; - - h3 { - width: 158px; - float: left; - margin: 0; - margin-left: 50px; - font-size: 19px; - line-height: 50px; - font-weight: normal; - } - } - } + padding: 8px 26px; &:hover { background-color: #eee; @@ -98,7 +51,7 @@ } .sidebar-user { - padding: 7px 22px; + padding: 15px 22px; position: fixed; bottom: 40px; width: $sidebar_width; @@ -126,8 +79,7 @@ .nav-sidebar { - margin-top: 14 + $header-height; - margin-bottom: 100px; + margin: 22px 0; transition-duration: .3s; list-style: none; overflow: hidden; @@ -145,13 +97,13 @@ } a { - padding: 7px 15px; + text-align: center; + padding: 8px 0; font-size: $gl-font-size; line-height: 24px; color: $gray; display: block; text-decoration: none; - padding-left: 23px; font-weight: normal; outline: none; @@ -166,7 +118,6 @@ i { width: 16px; color: $gray-light; - margin-right: 13px; } .count { @@ -217,25 +168,13 @@ } .page-sidebar-collapsed { - padding-left: $sidebar_collapsed_width; - - @media (max-width: $screen-xs-min) { - padding-left: 0; - } + padding-left: 0; .sidebar-wrapper { - width: $sidebar_collapsed_width; - - @media (max-width: $screen-xs-min) { - width: 0; - } + width: 0; .header-logo { - width: $sidebar_collapsed_width; - - @media (max-width: $screen-xs-min) { - width: 0; - } + width: 0; a { padding-left: ($sidebar_collapsed_width - 36) / 2; @@ -246,6 +185,10 @@ } } + #logo { + display: none; + } + .nav-sidebar { width: $sidebar_collapsed_width; @@ -261,44 +204,23 @@ } .collapse-nav a { - width: $sidebar_collapsed_width; - - @media (max-width: $screen-xs-min) { - width: 0; - } + width: 0; } .sidebar-user { - padding-left: ($sidebar_collapsed_width - 36) / 2; - width: $sidebar_collapsed_width; - - @media (max-width: $screen-xs-min) { - width: 0; - padding-left: 0; - padding-right: 0; - } + width: 0; + padding-left: 0; + padding-right: 0; .username { display: none; } } } - - .layout-nav { - padding-right: $sidebar_collapsed_width; - - @media (max-width: $screen-xs-min) { - padding-right: 0;; - } - } } .page-sidebar-expanded { - padding-left: $sidebar_collapsed_width; - - @media (min-width: $screen-md-min) { - padding-left: $sidebar_width; - } + padding-left: $sidebar_width; @media (max-width: $screen-xs-min) { padding-left: 0; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f253da814b..60207ecf1d 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: 220px; +$sidebar_width: 90px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 258px; diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1e961853c7..f0ba62fa68 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,11 +1,9 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } - .header-logo - %a#logo - = brand_header_logo - = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do - .gitlab-text-container - %h3 GitLab + = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do + .header-logo + #logo + = brand_header_logo - if defined?(sidebar) && sidebar = render "layouts/nav/#{sidebar}" @@ -18,9 +16,7 @@ = render partial: 'layouts/collapse_button' - if current_user = link_to current_user, class: 'sidebar-user', title: "Profile" do - = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' - .username - = current_user.username + = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s46' - if defined?(nav) && nav .layout-nav .container-fluid diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 306ebd5fcf..1d5c243b18 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -2,54 +2,50 @@ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do = icon('bookmark fw') - %span + %div Projects = nav_link(controller: :todos) do = link_to dashboard_todos_path, title: 'Todos' do = icon('bell fw') - %span + %div Todos - %span.count.todos-pending-count= number_with_delimiter(todos_pending_count) = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = icon('dashboard fw') - %span + %div Activity = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') - %span + %div Groups = nav_link(controller: 'dashboard/milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') - %span + %div Milestones = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do = icon('exclamation-circle fw') - %span + %div Issues - %span.count= number_with_delimiter(current_user.assigned_open_issues_count) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = icon('tasks fw') - %span + %div Merge Requests - %span.count= number_with_delimiter(current_user.assigned_open_merge_request_count) = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') - %span + %div Snippets = nav_link(controller: :help) do = link_to help_path, title: 'Help' do = icon('question-circle fw') - %span + %div Help - = nav_link(html_options: {class: profile_tab_class}) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') - %span + %div Profile Settings From ffbd9cd02d701442b5304e84ad75d657eeb4e23f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 2 Jun 2016 17:31:26 -0600 Subject: [PATCH 363/507] Test impersonation using img data attribute instead of username --- app/views/layouts/_page.html.haml | 2 +- spec/features/admin/admin_users_spec.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index f0ba62fa68..261038ef94 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -15,7 +15,7 @@ .collapse-nav = render partial: 'layouts/collapse_button' - if current_user - = link_to current_user, class: 'sidebar-user', title: "Profile" do + = link_to current_user, class: 'sidebar-user', title: "Profile", data: {user: current_user.username} do = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s46' - if defined?(nav) && nav .layout-nav diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index b72ad40547..1cb709c1de 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -144,8 +144,8 @@ describe "Admin::Users", feature: true do before { click_link 'Impersonate' } it 'logs in as the user when impersonate is clicked' do - page.within '.sidebar-user .username' do - expect(page).to have_content(another_user.username) + page.within '.sidebar-wrapper' do + expect(page.find('.sidebar-user')['data-user']).to eql(another_user.username) end end @@ -158,8 +158,8 @@ describe "Admin::Users", feature: true do it 'can log out of impersonated user back to original user' do find(:css, 'li.impersonation a').click - page.within '.sidebar-user .username' do - expect(page).to have_content(@user.username) + page.within '.sidebar-wrapper' do + expect(page.find('.sidebar-user')['data-user']).to eql(@user.username) end end From ca985283b40783e498c181ac4612ddf23c421ff4 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 2 Jun 2016 17:37:37 -0600 Subject: [PATCH 364/507] Remove todos count tests in nav --- features/steps/dashboard/todos.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index bd8a270202..19fedfbfcd 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -26,7 +26,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I should see todos assigned to me' do - page.within('.nav-sidebar') { expect(page).to have_content 'Todos 4' } expect(page).to have_content 'To do 4' expect(page).to have_content 'Done 0' @@ -42,7 +41,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done' end - page.within('.nav-sidebar') { expect(page).to have_content 'Todos 3' } expect(page).to have_content 'To do 3' expect(page).to have_content 'Done 1' should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" From e8a0cafc7d9e8ee44b37a8e282f4a679bf48cac3 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 3 Jun 2016 08:05:32 -0600 Subject: [PATCH 365/507] Fix control btn position --- app/assets/stylesheets/framework/sidebar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 0acb010b23..ed12cc4f22 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -250,7 +250,7 @@ } @media (min-width: $screen-xs-min) and (max-width: $screen-md-min) { - padding-right: 62px; + padding-right: 90px; } @media (min-width: $screen-md-min) { From 2293100d95f587049b7ce7fee923c4b3fbbf6f56 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 3 Jun 2016 16:50:27 -0600 Subject: [PATCH 366/507] Update nav link font size and spacing; fix hamburger icon --- app/assets/stylesheets/framework/header.scss | 3 ++- app/assets/stylesheets/framework/sidebar.scss | 18 +++++------------ app/views/layouts/nav/_dashboard.html.haml | 20 +++++++++---------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 3be83b0fc6..c46d6b1478 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -89,7 +89,8 @@ header { display: none; position: absolute; left: -10px; - margin: 8px 0; + margin: 6px 0; + font-size: 18px; padding: 6px 10px; border: none; background-color: $background-color; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index ed12cc4f22..46d46368d2 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,9 +1,3 @@ -#logo { - z-index: 2; - position: relative; - cursor: pointer; -} - .page-with-sidebar { padding-top: $header-height; transition-duration: .3s; @@ -98,9 +92,8 @@ a { text-align: center; - padding: 8px 0; + padding: 8px; font-size: $gl-font-size; - line-height: 24px; color: $gray; display: block; text-decoration: none; @@ -120,11 +113,10 @@ color: $gray-light; } - .count { - float: right; - background: #eee; - padding: 0 8px; - @include border-radius(6px); + .nav-link-text { + margin-top: 3px; + font-size: 13px; + line-height: 18px; } &.back-link i { diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 1d5c243b18..df77d9cf83 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -2,50 +2,50 @@ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do = icon('bookmark fw') - %div + .nav-link-text Projects = nav_link(controller: :todos) do = link_to dashboard_todos_path, title: 'Todos' do = icon('bell fw') - %div + .nav-link-text Todos = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = icon('dashboard fw') - %div + .nav-link-text Activity = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') - %div + .nav-link-text Groups = nav_link(controller: 'dashboard/milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') - %div + .nav-link-text Milestones = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do = icon('exclamation-circle fw') - %div + .nav-link-text Issues = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = icon('tasks fw') - %div + .nav-link-text Merge Requests = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') - %div + .nav-link-text Snippets = nav_link(controller: :help) do = link_to help_path, title: 'Help' do = icon('question-circle fw') - %div + .nav-link-text Help = nav_link(html_options: {class: profile_tab_class}) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') - %div + .nav-link-text Profile Settings From ef37b57f1f5089420997c6092921e1d9516b03b4 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 6 Jun 2016 21:31:28 -0500 Subject: [PATCH 367/507] Update charcoal theme colors --- app/assets/stylesheets/framework/gitlab-theme.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 16cf394c42..cd2eba59f9 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -89,8 +89,11 @@ } } -$theme-blue: #2980b9; $theme-charcoal: #3d454d; +$theme-charcoal-dark: #383f45; +$theme-charcoal-text: #b9bbbe; + +$theme-blue: #2980b9; $theme-graphite: #666; $theme-gray: #373737; $theme-green: #019875; @@ -102,7 +105,7 @@ body { } &.ui_charcoal { - @include gitlab-theme(#d6d7d9, #485157, $theme-charcoal, #353b41); + @include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark); } &.ui_graphite { From e8cf89fa0693df95954339bd7593cd13cbbe2e72 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 1 Jun 2016 21:00:56 +0800 Subject: [PATCH 368/507] Add a test for User#ci_authorized_runners --- spec/models/user_spec.rb | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6ea8bf9bbe..2802c7e70b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -845,6 +845,63 @@ describe User, models: true do it { is_expected.to eq([private_project]) } end + describe '#ci_authorized_runners' do + let(:user) { create(:user) } + let(:runner) { create(:ci_runner) } + + before { project.runners << runner } + + context 'without any projects' do + let(:project) { create(:project) } + + it 'does not load' do + expect(user.ci_authorized_runners).to eq([]) + end + end + + context 'with personal projects runners' do + let(:namespace) { create(:namespace, owner: user) } + let(:project) { create(:project, namespace: namespace) } + + it 'loads' do + expect(user.ci_authorized_runners).to eq([runner]) + end + end + + shared_examples :member do + it 'loads when the user is a master' do + add_user(Gitlab::Access::MASTER) + expect(user.ci_authorized_runners).to eq([runner]) + end + + it 'does not load when the user is a developer' do + add_user(Gitlab::Access::DEVELOPER) + expect(user.ci_authorized_runners).to eq([]) + end + end + + context 'with groups projects runners' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + def add_user access + group.add_user(user, access) + end + + it_behaves_like :member + end + + context 'with other projects runners' do + let(:project) { create(:project) } + + def add_user access + Member.add_user(project.project_members, user, access) + end + + it_behaves_like :member + end + end + describe '#viewable_starred_projects' do let(:user) { create(:user) } let(:public_project) { create(:empty_project, :public) } From 5360ef2c5bbdd2706361a59310dba84ed11305bb Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 2 Jun 2016 05:03:00 +0000 Subject: [PATCH 369/507] This is easier to write: Feedback from: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4404#note_12194471 --- spec/models/user_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2802c7e70b..6f7d5a3c14 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -895,7 +895,7 @@ describe User, models: true do let(:project) { create(:project) } def add_user access - Member.add_user(project.project_members, user, access) + project.team << [user, access] end it_behaves_like :member From 0c2962eb86c4f6e938ad817372498e6600a19a1c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 2 Jun 2016 05:10:59 +0000 Subject: [PATCH 370/507] Use subject for more consistent testing style: Feedback from: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4404#note_12194489 --- spec/models/user_spec.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6f7d5a3c14..d9e65586a8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -848,6 +848,7 @@ describe User, models: true do describe '#ci_authorized_runners' do let(:user) { create(:user) } let(:runner) { create(:ci_runner) } + subject { user.ci_authorized_runners } before { project.runners << runner } @@ -855,7 +856,7 @@ describe User, models: true do let(:project) { create(:project) } it 'does not load' do - expect(user.ci_authorized_runners).to eq([]) + is_expected.to eq([]) end end @@ -864,19 +865,19 @@ describe User, models: true do let(:project) { create(:project, namespace: namespace) } it 'loads' do - expect(user.ci_authorized_runners).to eq([runner]) + is_expected.to eq([runner]) end end shared_examples :member do it 'loads when the user is a master' do add_user(Gitlab::Access::MASTER) - expect(user.ci_authorized_runners).to eq([runner]) + is_expected.to eq([runner]) end it 'does not load when the user is a developer' do add_user(Gitlab::Access::DEVELOPER) - expect(user.ci_authorized_runners).to eq([]) + is_expected.to eq([]) end end From 2dd60b9b3f400e7709000eb1b4b7b7ab21326d5f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 2 Jun 2016 05:15:45 +0000 Subject: [PATCH 371/507] Use rspec's matchers and update style: Feedback from: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4404#note_12194552 --- spec/models/user_spec.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d9e65586a8..4c213f633d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -856,7 +856,7 @@ describe User, models: true do let(:project) { create(:project) } it 'does not load' do - is_expected.to eq([]) + is_expected.to be_empty end end @@ -865,19 +865,21 @@ describe User, models: true do let(:project) { create(:project, namespace: namespace) } it 'loads' do - is_expected.to eq([runner]) + is_expected.to contain_exactly(runner) end end shared_examples :member do - it 'loads when the user is a master' do - add_user(Gitlab::Access::MASTER) - is_expected.to eq([runner]) + context 'when the user is a master' do + before { add_user(Gitlab::Access::MASTER) } + + it { is_expected.to contain_exactly(runner) } end - it 'does not load when the user is a developer' do - add_user(Gitlab::Access::DEVELOPER) - is_expected.to eq([]) + context 'when the user is a developer' do + before { add_user(Gitlab::Access::DEVELOPER) } + + it { is_expected.to be_empty } end end From db95704fb484e546ae8cbbb24aa7582a4137b90f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 2 Jun 2016 05:27:44 +0000 Subject: [PATCH 372/507] Fix method definition style --- spec/models/user_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4c213f633d..f8c2555d07 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -887,7 +887,7 @@ describe User, models: true do let(:group) { create(:group) } let(:project) { create(:project, group: group) } - def add_user access + def add_user(access) group.add_user(user, access) end @@ -897,7 +897,7 @@ describe User, models: true do context 'with other projects runners' do let(:project) { create(:project) } - def add_user access + def add_user(access) project.team << [user, access] end From 5f3e647330041cdf588c2def9cba517dd546d365 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 2 Jun 2016 23:30:27 +0800 Subject: [PATCH 373/507] Prefer do and end for before/after: Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4404#note_12217415 --- spec/models/user_spec.rb | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f8c2555d07..84c93dfeb2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -68,7 +68,9 @@ describe User, models: true do describe 'email' do context 'when no signup domains listed' do - before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([]) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([]) + end it 'accepts any email' do user = build(:user, email: "info@example.com") expect(user).to be_valid @@ -76,7 +78,9 @@ describe User, models: true do end context 'when a signup domain is listed and subdomains are allowed' do - before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) + end it 'accepts info@example.com' do user = build(:user, email: "info@example.com") expect(user).to be_valid @@ -94,7 +98,9 @@ describe User, models: true do end context 'when a signup domain is listed and subdomains are not allowed' do - before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com']) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com']) + end it 'accepts info@example.com' do user = build(:user, email: "info@example.com") @@ -202,7 +208,9 @@ describe User, models: true do end describe '#confirm' do - before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) + end let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') } it 'returns unconfirmed' do @@ -850,7 +858,9 @@ describe User, models: true do let(:runner) { create(:ci_runner) } subject { user.ci_authorized_runners } - before { project.runners << runner } + before do + project.runners << runner + end context 'without any projects' do let(:project) { create(:project) } @@ -871,13 +881,17 @@ describe User, models: true do shared_examples :member do context 'when the user is a master' do - before { add_user(Gitlab::Access::MASTER) } + before do + add_user(Gitlab::Access::MASTER) + end it { is_expected.to contain_exactly(runner) } end context 'when the user is a developer' do - before { add_user(Gitlab::Access::DEVELOPER) } + before do + add_user(Gitlab::Access::DEVELOPER) + end it { is_expected.to be_empty } end From 4fcdcc36f168bc3791536c20a6168eb7428f623d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 6 Jun 2016 14:55:18 +0800 Subject: [PATCH 374/507] Avoid using subject and is_expected.to: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4404#note_12274602 --- spec/models/user_spec.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 84c93dfeb2..f727611f7c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -856,7 +856,6 @@ describe User, models: true do describe '#ci_authorized_runners' do let(:user) { create(:user) } let(:runner) { create(:ci_runner) } - subject { user.ci_authorized_runners } before do project.runners << runner @@ -866,7 +865,7 @@ describe User, models: true do let(:project) { create(:project) } it 'does not load' do - is_expected.to be_empty + expect(user.ci_authorized_runners).to be_empty end end @@ -875,7 +874,7 @@ describe User, models: true do let(:project) { create(:project, namespace: namespace) } it 'loads' do - is_expected.to contain_exactly(runner) + expect(user.ci_authorized_runners).to contain_exactly(runner) end end @@ -885,7 +884,9 @@ describe User, models: true do add_user(Gitlab::Access::MASTER) end - it { is_expected.to contain_exactly(runner) } + it 'loads' do + expect(user.ci_authorized_runners).to contain_exactly(runner) + end end context 'when the user is a developer' do @@ -893,7 +894,9 @@ describe User, models: true do add_user(Gitlab::Access::DEVELOPER) end - it { is_expected.to be_empty } + it 'does not load' do + expect(user.ci_authorized_runners).to be_empty + end end end From 4f34cf3241fc7ef53d260dace3da20da8cd89c9e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 7 Jun 2016 20:32:48 +0800 Subject: [PATCH 375/507] Add a blank line between before and it: Feedback from: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4404#note_12301563 --- spec/models/user_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f727611f7c..73bee535fe 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -71,6 +71,7 @@ describe User, models: true do before do allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([]) end + it 'accepts any email' do user = build(:user, email: "info@example.com") expect(user).to be_valid @@ -81,6 +82,7 @@ describe User, models: true do before do allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) end + it 'accepts info@example.com' do user = build(:user, email: "info@example.com") expect(user).to be_valid @@ -211,6 +213,7 @@ describe User, models: true do before do allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) end + let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') } it 'returns unconfirmed' do From ae6591081a0a16112adcd36304fc80b037228854 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 7 Jun 2016 10:05:10 -0500 Subject: [PATCH 376/507] Fix alignment of wiki top area --- app/assets/stylesheets/framework/nav.scss | 4 ++++ app/views/projects/wikis/edit.html.haml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 1b22c88070..a811778df7 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -101,6 +101,10 @@ width: 50%; line-height: 28px; + &.wiki-page { + padding: 16px 10px 11px; + } + /* Small devices (phones, tablets, 768px and lower) */ @media (max-width: $screen-sm-min) { width: 100%; diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index aaa15dd3bb..cbd69ee1a7 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -2,7 +2,7 @@ = render 'nav' .top-area - .nav-text + .nav-text.wiki-page %strong - if @page.persisted? = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) From 8e41b21db187c6f6cfe5c0c70e986f63c11d0c25 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 6 Jun 2016 21:34:08 -0700 Subject: [PATCH 377/507] Bump nokogiri to 1.6.8 Number of important security and bug fixes. See: https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md#168--2016-06-06 --- CHANGELOG | 1 + Gemfile.lock | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ef4bd5fe29..9d631b56b5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.9.0 (unreleased) - Redesign navigation for project pages - Fix groups API to list only user's accessible projects - Redesign account and email confirmation emails + - Bump nokogiri to 1.6.8 - Use gitlab-shell v3.0.0 - Use Knapsack to evenly distribute tests across multiple nodes - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged diff --git a/Gemfile.lock b/Gemfile.lock index c85f9be778..489c7e7b44 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -390,7 +390,7 @@ GEM method_source (0.8.2) mime-types (2.99.1) mimemagic (0.3.0) - mini_portile2 (2.0.0) + mini_portile2 (2.1.0) minitest (5.7.0) mousetrap-rails (1.4.6) multi_json (1.11.2) @@ -401,8 +401,9 @@ GEM net-ldap (0.12.1) net-ssh (3.0.1) newrelic_rpm (3.14.1.311) - nokogiri (1.6.7.2) - mini_portile2 (~> 2.0.0.rc2) + nokogiri (1.6.8) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) oauth (0.4.7) oauth2 (1.0.0) faraday (>= 0.8, < 0.10) @@ -474,6 +475,7 @@ GEM parser (2.3.1.0) ast (~> 2.2) pg (0.18.4) + pkg-config (1.1.7) poltergeist (1.9.0) capybara (~> 2.1) cliver (~> 0.3.1) From d485ec9f1c7bf00a3c87e3e91b6a306f234a5232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 2 Jun 2016 16:47:39 -0400 Subject: [PATCH 378/507] Instrument `RepositoryCheck::SingleRepositoryWorker` manually This worker is called manually by `RepositoryCheck::BatchWorker` meaning it's not tracked automatically by the Sidekiq middleware. --- CHANGELOG | 1 + config/initializers/metrics.rb | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ce8f2823c2..196556328d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -41,6 +41,7 @@ v 8.9.0 (unreleased) - Put project Files and Commits tabs under Code tab - Replace Colorize with Rainbow for coloring console output in Rake tasks. - An indicator is now displayed at the top of the comment field for confidential issues. + - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented v 8.8.4 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index 0c78871471..2673093b96 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -121,6 +121,13 @@ if Gitlab::Metrics.enabled? config.instrument_instance_methods(Gitlab::GitAccessWiki) config.instrument_instance_methods(API::Helpers) + + config.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker) + # Iterate over each non-super private instance method to keep up to date if + # internals change + RepositoryCheck::SingleRepositoryWorker.private_instance_methods(false).each do |method| + config.instrument_instance_method(RepositoryCheck::SingleRepositoryWorker, method) + end end GC::Profiler.enable From c593154cb4f0215851fbbae1dde753dacbaa6713 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Tue, 7 Jun 2016 11:19:19 -0500 Subject: [PATCH 379/507] Moved `find_or_create_ldap_user` method to parent class and added logging. --- lib/gitlab/o_auth/user.rb | 18 ++++++++++++------ lib/gitlab/saml/user.rb | 24 ------------------------ spec/lib/gitlab/saml/user_spec.rb | 2 +- 3 files changed, 13 insertions(+), 31 deletions(-) diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 268ee11502..5e52979093 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -69,13 +69,19 @@ module Gitlab return unless ldap_person # If a corresponding person exists with same uid in a LDAP server, - # set up a Gitlab user with dual LDAP and Omniauth identities. - if user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider) - # Case when a LDAP user already exists in Gitlab. Add the Omniauth identity to existing account. + # check if the user already has a GitLab account. + if (user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)) + # Case when a LDAP user already exists in Gitlab. Add the OAuth identity to existing account. + log.info "LDAP account found for user #{user.username}. Building new identity." user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider) else - # No account in Gitlab yet: create it and add the LDAP identity - user = build_new_user + log.info 'No existing LDAP account was found in GitLab. Checking for OAuth account.' + user = find_by_uid_and_provider + if user.nil? + log.info 'No user found with the specified OAuth provider. Creating a new one.' + user = build_new_user + end + log.info "Correct account has been found. Adding LDAP identity to user: #{user.username}." user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn) end @@ -96,7 +102,7 @@ module Gitlab # Look for a corresponding person with same uid in any of the configured LDAP providers Gitlab::LDAP::Config.providers.each do |provider| adapter = Gitlab::LDAP::Adapter.new(provider) - @ldap_person = Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter) + @ldap_person = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) break if @ldap_person end @ldap_person diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index 6f7d4825ae..8943022612 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -62,30 +62,6 @@ module Gitlab !Gitlab::Saml::Config.external_groups.nil? end - def find_or_create_ldap_user - return unless ldap_person - - # If a corresponding person exists with same uid in a LDAP server, - # check if the user already has a GitLab account - user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider) - if user - # Case when a LDAP user already exists in Gitlab. Add the SAML identity to existing account. - user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider) - else - # No account found using LDAP in Gitlab yet: check if there is a SAML account with - # the passed uid and provider - user = find_by_uid_and_provider - if user.nil? - # No SAML account found, build a new user. - user = build_new_user - end - # Correct account is present, add the LDAP Identity to the user. - user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn) - end - - user - end - def auth_hash=(auth_hash) @auth_hash = Gitlab::Saml::AuthHash.new(auth_hash) end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index f0a17244ff..5957998e0f 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -182,7 +182,7 @@ describe Gitlab::Saml::User, lib: true do context 'user has SAML user, and wants to add their LDAP identity' do it 'adds the LDAP identity to the existing SAML user' do create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john') - local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) + local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash) local_saml_user = described_class.new(local_hash) local_saml_user.save From 5bb3e1934c2a059c8e7cdc4ecbe597781f502a3e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 7 Jun 2016 10:14:18 -0700 Subject: [PATCH 380/507] Add LGPLv2 to license whiltelist --- config/dependency_decisions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index a49d805e4f..436a2c5e17 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -175,3 +175,9 @@ :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc :versions: [] :when: 2016-05-02 05:56:50.696858000 Z +- - :whitelist + - LGPLv2+ + - :who: Stan Hu + :why: Equivalent to LGPLv2 + :versions: [] + :when: 2016-06-07 17:14:10.907682000 Z From 629b4e68e8f9f46ff81fabc2f439264e681dabcc Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 7 Jun 2016 11:23:26 -0600 Subject: [PATCH 381/507] Add License Finder information to contribution acceptance criteria. --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18270d9598..f447221477 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -405,6 +405,7 @@ description area. Copy-paste it to retain the markdown format. entire line to follow it. This prevents linting tools from generating warnings. - Don't touch neighbouring lines. As an exception, automatic mass refactoring modifications may leave style non-compliant. +1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error. ## Changes for Stable Releases @@ -531,3 +532,4 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 [`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/ +[license-finder-doc]: doc/development/licensing.md From 1558876ac0c7e521dabfc2b1efe3418d34fe531f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 7 Jun 2016 12:37:44 -0500 Subject: [PATCH 382/507] Side nav scrolls if content height taller than screen --- app/assets/stylesheets/framework/sidebar.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 46d46368d2..83d6467968 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -38,6 +38,11 @@ .header-logo { height: $header-height; padding: 8px 26px; + width: $sidebar_width; + position: fixed; + z-index: 999; + overflow: hidden; + transition-duration: .3s; &:hover { background-color: #eee; @@ -73,7 +78,8 @@ .nav-sidebar { - margin: 22px 0; + margin-top: 22 + $header-height; + margin-bottom: 116px; transition-duration: .3s; list-style: none; overflow: hidden; @@ -167,6 +173,7 @@ .header-logo { width: 0; + padding: 8px 0; a { padding-left: ($sidebar_collapsed_width - 36) / 2; From 76d618404ee8a47a34b45dfe41d268eaaf52f00d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 19:45:44 +0200 Subject: [PATCH 383/507] Fix markdown_spec to not use `before(:all)` in order to properly cleanup database after testing --- CHANGELOG | 1 + spec/features/markdown_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ef4bd5fe29..0eb82b9299 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -37,6 +37,7 @@ v 8.9.0 (unreleased) - Add Application Setting to configure Container Registry token expire delay (default 5min) - Cache assigned issue and merge request counts in sidebar nav - Cache project build count in sidebar nav + - Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing - Reduce number of queries needed to render issue labels in the sidebar - Improve error handling importing projects - Remove duplicated notification settings diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 1d892fe1a5..7663d19335 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -180,7 +180,7 @@ describe 'GitLab Markdown', feature: true do end end - before(:all) do + before do @feat = MarkdownFeature.new # `markdown` helper expects a `@project` variable @@ -188,7 +188,7 @@ describe 'GitLab Markdown', feature: true do end context 'default pipeline' do - before(:all) do + before do @html = markdown(@feat.raw_markdown) end From 88b0a9efc44a792a7fd9f2263a1c85b9ef552504 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 19:29:33 +0200 Subject: [PATCH 384/507] Load knapsack in Rakefile only when is bundled --- Gemfile | 2 +- Rakefile | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 482a6c18dd..08592607c0 100644 --- a/Gemfile +++ b/Gemfile @@ -308,6 +308,7 @@ group :development, :test do gem 'benchmark-ips', require: false gem "license_finder", require: false + gem 'knapsack' end group :test do @@ -316,7 +317,6 @@ group :test do gem 'webmock', '~> 1.21.0' gem 'test_after_commit', '~> 0.4.2' gem 'sham_rack' - gem 'knapsack' end group :production do diff --git a/Rakefile b/Rakefile index 16261bf8ae..85fff2d51e 100755 --- a/Rakefile +++ b/Rakefile @@ -3,11 +3,10 @@ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) -require 'knapsack' relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__) require relative_url_conf if File.exist?("#{relative_url_conf}.rb") Gitlab::Application.load_tasks -Knapsack.load_tasks +Knapsack.load_tasks if defined?(Knapsack) From f872f58738e27dff1d9a1b88ab6661239f6943cf Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 7 Jun 2016 14:16:34 -0500 Subject: [PATCH 385/507] Update admin sidebar --- app/views/layouts/nav/_admin.html.haml | 42 ++++++++++++-------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index f292730fe4..de2276e75e 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -2,106 +2,102 @@ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do = link_to admin_root_path, title: 'Overview' do = icon('dashboard fw') - %span + .nav-link-text Overview = nav_link(controller: [:admin, :projects]) do = link_to admin_namespaces_projects_path, title: 'Projects' do = icon('cube fw') - %span + .nav-link-text Projects = nav_link(controller: :users) do = link_to admin_users_path, title: 'Users' do = icon('user fw') - %span + .nav-link-text Users = nav_link(controller: :groups) do = link_to admin_groups_path, title: 'Groups' do = icon('group fw') - %span + .nav-link-text Groups = nav_link(controller: :deploy_keys) do = link_to admin_deploy_keys_path, title: 'Deploy Keys' do = icon('key fw') - %span + .nav-link-text Deploy Keys = nav_link path: ['runners#index', 'runners#show'] do = link_to admin_runners_path, title: 'Runners' do = icon('cog fw') - %span + .nav-link-text Runners - %span.count= number_with_delimiter(Ci::Runner.count(:all)) = nav_link path: 'builds#index' do = link_to admin_builds_path, title: 'Builds' do = icon('link fw') - %span + .nav-link-text Builds - %span.count= number_with_delimiter(Ci::Build.count(:all)) = nav_link(controller: :logs) do = link_to admin_logs_path, title: 'Logs' do = icon('file-text fw') - %span + .nav-link-text Logs = nav_link(controller: :health_check) do = link_to admin_health_check_path, title: 'Health Check' do = icon('medkit fw') - %span + .nav-link-text Health Check = nav_link(controller: :broadcast_messages) do = link_to admin_broadcast_messages_path, title: 'Messages' do = icon('bullhorn fw') - %span + .nav-link-text Messages = nav_link(controller: :hooks) do = link_to admin_hooks_path, title: 'Hooks' do = icon('external-link fw') - %span + .nav-link-text Hooks = nav_link(controller: :background_jobs) do = link_to admin_background_jobs_path, title: 'Background Jobs' do = icon('cog fw') - %span + .nav-link-text Background Jobs = nav_link(controller: :appearances) do = link_to admin_appearances_path, title: 'Appearances' do = icon('image') - %span + .nav-link-text Appearance = nav_link(controller: :applications) do = link_to admin_applications_path, title: 'Applications' do = icon('cloud fw') - %span + .nav-link-text Applications = nav_link(controller: :services) do = link_to admin_application_settings_services_path, title: 'Service Templates' do = icon('copy fw') - %span + .nav-link-text Service Templates = nav_link(controller: :labels) do = link_to admin_labels_path, title: 'Labels' do = icon('tags fw') - %span + .nav-link-text Labels = nav_link(controller: :abuse_reports) do = link_to admin_abuse_reports_path, title: "Abuse Reports" do = icon('exclamation-circle fw') - %span + .nav-link-text Abuse Reports - %span.count= number_with_delimiter(AbuseReport.count(:all)) - if askimet_enabled? = nav_link(controller: :spam_logs) do = link_to admin_spam_logs_path, title: "Spam Logs" do = icon('exclamation-triangle fw') - %span + .nav-link-text Spam Logs - %span.count= number_with_delimiter(SpamLog.count(:all)) = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do = link_to admin_application_settings_path, title: 'Settings' do = icon('cogs fw') - %span + .nav-link-text Settings From 2ffc459f34f2c259a7435121d0f18d3b1ff1a770 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 7 Jun 2016 14:17:12 -0500 Subject: [PATCH 386/507] Update explore sidebar --- app/views/layouts/nav/_explore.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index 3b40006a0c..46fcf1545f 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -2,20 +2,20 @@ = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do = link_to explore_root_path, title: 'Projects' do = icon('bookmark fw') - %span + .nav-link-text Projects = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to explore_groups_path, title: 'Groups' do = icon('group fw') - %span + .nav-link-text Groups = nav_link(controller: :snippets) do = link_to explore_snippets_path, title: 'Snippets' do = icon('clipboard fw') - %span + .nav-link-text Snippets = nav_link(controller: :help) do = link_to help_path, title: 'Help' do = icon('question-circle fw') - %span + .nav-link-text Help From 405d752b9b68d0c0c01cc4aae76d6f5acd21dc27 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 6 Jun 2016 00:31:26 -0700 Subject: [PATCH 387/507] Bump recaptcha gem to 3.0.0 to remove deprecated stoken support Closes #18210 --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index de3aeab0d4..720fb554c5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.9.0 (unreleased) - Make EmailsOnPushWorker use Sidekiq mailers queue - Fix wiki page events' webhook to point to the wiki repository - Fix issue todo not remove when leave project !4150 (Long Nguyen) + - Bump recaptcha gem to 3.0.0 to remove deprecated stoken support - Allow forking projects with restricted visibility level - Improve note validation to prevent errors when creating invalid note via API - Reduce number of fog gem dependencies diff --git a/Gemfile b/Gemfile index 08592607c0..f2ac70831f 100644 --- a/Gemfile +++ b/Gemfile @@ -38,7 +38,7 @@ gem 'rack-oauth2', '~> 1.2.1' gem 'jwt' # Spam and anti-bot protection -gem 'recaptcha', require: 'recaptcha/rails' +gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails' gem 'akismet', '~> 2.0' # Two-factor authentication diff --git a/Gemfile.lock b/Gemfile.lock index c85f9be778..fd5852c549 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -549,7 +549,7 @@ GEM debugger-ruby_core_source (~> 1.3) rdoc (3.12.2) json (~> 1.4) - recaptcha (1.0.2) + recaptcha (3.0.0) json redcarpet (3.3.3) redis (3.3.0) @@ -932,7 +932,7 @@ DEPENDENCIES raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) - recaptcha + recaptcha (~> 3.0) redcarpet (~> 3.3.3) redis (~> 3.2) redis-namespace From f4beec8a9043fbb9782e7b354b46ec1a7af7887e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 2 Jun 2016 11:44:38 -0700 Subject: [PATCH 388/507] Add Azure to supported backup list (left out accidentally) --- Gemfile | 1 + Gemfile.lock | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Gemfile b/Gemfile index 08592607c0..8c67169666 100644 --- a/Gemfile +++ b/Gemfile @@ -86,6 +86,7 @@ gem 'dropzonejs-rails', '~> 0.7.1' # for backups gem 'fog-aws', '~> 0.9' +gem 'fog-azure', '~> 0.0' gem 'fog-core', '~> 1.40' gem 'fog-local', '~> 0.3' gem 'fog-google', '~> 0.3' diff --git a/Gemfile.lock b/Gemfile.lock index c85f9be778..28d358b667 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,6 +70,21 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + azure (0.7.5) + addressable (~> 2.3) + azure-core (~> 0.1) + faraday (~> 0.9) + faraday_middleware (~> 0.10) + json (~> 1.8) + mime-types (>= 1, < 3.0) + nokogiri (~> 1.6) + systemu (~> 2.6) + thor (~> 0.19) + uuid (~> 2.0) + azure-core (0.1.2) + faraday (~> 0.9) + faraday_middleware (~> 0.10) + nokogiri (~> 1.6) babosa (1.0.2) base32 (0.3.2) bcrypt (3.1.11) @@ -213,6 +228,11 @@ GEM fog-json (~> 1.0) fog-xml (~> 0.1) ipaddress (~> 0.8) + fog-azure (0.0.2) + azure (~> 0.6) + fog-core (~> 1.27) + fog-json (~> 1.0) + fog-xml (~> 0.1) fog-core (1.40.0) builder excon (~> 0.49) @@ -854,6 +874,7 @@ DEPENDENCIES flay flog fog-aws (~> 0.9) + fog-azure (~> 0.0) fog-core (~> 1.40) fog-google (~> 0.3) fog-local (~> 0.3) From ae736d81e1f64fe9439ac0675538d359cfdb5be9 Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Tue, 17 May 2016 17:04:01 +0100 Subject: [PATCH 389/507] Added a line to find the currently saved text and replace the markdown textarea text when cancelling Updated CHANGELOG Corrected the changes to store the markdown in a hidden input when edit starts and retrieve the original from that input when edit is cancelled Replaced hidden input with form attribute --- CHANGELOG | 1 + app/assets/javascripts/notes.js.coffee | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b9189b8088..ff5a7c22e6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -57,6 +57,7 @@ v 8.8.4 (unreleased) - Fix importer for GitHub comments on diff - Disable Webhooks before proceeding with the GitHub import - Added descriptions to notification settings dropdown + - Markdown editor now correctly resets the input value on edit cancellation !4175 v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 8e33e915ba..ad216910c8 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -354,8 +354,7 @@ class @Notes Called in response to clicking the edit note link Replaces the note text with the note edit form - Adds a hidden div with the original content of the note to fill the edit note form with - if the user cancels + Adds a data attribute to the form with the original content of the note for cancellations ### showEditForm: (e, scrollTo, myLastNote) -> e.preventDefault() @@ -371,6 +370,8 @@ class @Notes done = ($noteText) -> # Neat little trick to put the cursor at the end noteTextVal = $noteText.val() + # Store the original note text in a data attribute to retrieve if a user cancels edit. + form.find('form.edit-note').data 'original-note', noteTextVal $noteText.val('').val(noteTextVal); new GLForm form @@ -393,14 +394,16 @@ class @Notes ### Called in response to clicking the edit note link - Hides edit form + Hides edit form and restores the original note text to the editor textarea. ### cancelEdit: (e) -> e.preventDefault() note = $(this).closest(".note") + form = note.find(".current-note-edit-form") note.removeClass "is-editting" - note.find(".current-note-edit-form") - .removeClass("current-note-edit-form") + form.removeClass("current-note-edit-form") + # Replace markdown textarea text with original note text. + form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')) ### Called in response to deleting a note of any kind. From d9422aef859334de737341ed4acf1933439c5693 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 7 Jun 2016 13:13:10 -0600 Subject: [PATCH 390/507] Only load Chart.js when necessary. --- app/assets/javascripts/application.js.coffee | 1 - app/assets/javascripts/graphs/application.js.coffee | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index ebf425550e..2bb11b11d5 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -35,7 +35,6 @@ #= require raphael #= require g.raphael #= require g.bar -#= require Chart #= require branch-graph #= require ace/ace #= require ace/ext-searchbox diff --git a/app/assets/javascripts/graphs/application.js.coffee b/app/assets/javascripts/graphs/application.js.coffee index e0f681acf0..91f81a5d24 100644 --- a/app/assets/javascripts/graphs/application.js.coffee +++ b/app/assets/javascripts/graphs/application.js.coffee @@ -4,4 +4,5 @@ # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # the compiled file. # +#= require Chart #= require_tree . From 200bbbcb6a0d599b033f542c50182356a5eaa933 Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Mon, 16 May 2016 23:01:48 +0100 Subject: [PATCH 391/507] Fixes issue search form hiding when current_user is nil (guest user) Fixes the bottom margin of .nav-controls > form (issues list search/filter form) so when a guest views on the field on mobile it is not squished against the issues list Updated CHANGELOG navigation tabs and navigation filter search will now stay on the same row until there is no space to Removed unneeded media queries --- CHANGELOG | 1 + app/assets/stylesheets/framework/nav.scss | 13 ++----------- app/views/projects/issues/index.html.haml | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b9189b8088..d4c5ef63e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -168,6 +168,7 @@ v 8.8.0 - Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine) - Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs) - When creating a .gitignore file a dropdown with templates will be provided + - Shows the issue/MR list search/filter form and corrects the mobile styling for guest users. #17562 v 8.7.7 - Fix import by `Any Git URL` broken if the URL contains a space diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index a811778df7..294c4e3569 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -171,6 +171,7 @@ > form { display: inline-block; margin-top: -1px; + margin-bottom: 12px; } .icon-label { @@ -207,7 +208,7 @@ @media (max-width: $screen-xs-max) { padding-bottom: 0; - + width: 100%; .btn, form, .dropdown, .dropdown-menu-toggle, .form-control { margin: 0 0 10px; display: block; @@ -238,16 +239,6 @@ margin: 0; } } - - /* Small devices (tablets, 768px and lower) */ - @media (max-width: $screen-sm-max) { - width: 100%; - text-align: left; - - input { - width: 300px; - } - } } } diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 19a6f4a91f..95b5dcf0e0 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -12,7 +12,7 @@ = icon('rss') %span.icon-label Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) + = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do = icon('plus') From 9f554aadd585e357c77a22733c82f381967cac1d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 6 Jun 2016 16:29:52 -0400 Subject: [PATCH 392/507] Fix 2FA-based login for LDAP users The OTP input form is shared by both LDAP and standard logins, but when coming from an LDAP-based form, the form parameters aren't nested in a Hash based on the `resource_name` value. Now we check for a nested `remember_me` parameter and use that if it exists, or fall back to the non-nested parameters if it doesn't. Somewhat confusingly, the OTP input form _does_ nest parameters under the `resource_name`, regardless of what type of login we're coming from, so that allows everything else to work as normal. --- app/views/devise/sessions/two_factor.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 9d04db2c45..a373f61bd3 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -6,7 +6,8 @@ - if @user.two_factor_otp_enabled? %h5 Authenticate via Two-Factor App = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - = f.hidden_field :remember_me, value: params[resource_name][:remember_me] + - resource_params = params[resource_name].presence || params + = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off' %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. .prepend-top-20 From df62cbd917f85f85d2e3371da2eccf724d5d94e0 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 8 Jun 2016 11:42:25 +0200 Subject: [PATCH 393/507] Add parentheses --- spec/requests/git_http_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 14d126480a..594a60a434 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -287,20 +287,20 @@ describe 'Git HTTP requests', lib: true do def download(project, user: nil, password: nil) args = [project, { user: user, password: password }] - clone_get *args + clone_get(*args) yield response - clone_post *args + clone_post(*args) yield response end def upload(project, user: nil, password: nil) args = [project, { user: user, password: password }] - push_get *args + push_get(*args) yield response - push_post *args + push_post(*args) yield response end From eb95019178a002e0c016e60c7b8ef3eb49f7997e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 8 Jun 2016 11:43:13 +0200 Subject: [PATCH 394/507] Enable Knapsack only in CI environment --- CHANGELOG | 1 + features/support/env.rb | 6 ++++-- spec/spec_helper.rb | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5136756079..59c7169942 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,6 +38,7 @@ v 8.9.0 (unreleased) - Make authentication service for Container Registry to be compatible with < Docker 1.11 - Add Application Setting to configure Container Registry token expire delay (default 5min) - Cache assigned issue and merge request counts in sidebar nav + - Use Knapsack only in CI environment - Cache project build count in sidebar nav - Reduce number of queries needed to render issue labels in the sidebar - Improve error handling importing projects diff --git a/features/support/env.rb b/features/support/env.rb index 4552db8ad7..edc08cf098 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -11,13 +11,15 @@ ENV['RAILS_ENV'] = 'test' require './config/environment' require 'rspec/expectations' require 'sidekiq/testing/inline' -require 'knapsack' require_relative 'capybara' require_relative 'db_cleaner' require_relative 'rerun' -Knapsack::Adapters::SpinachAdapter.bind +if ENV['CI'] + require 'knapsack' + Knapsack::Adapters::RSpecAdapter.bind +end %w(select2_helper test_env repo_helpers).each do |f| require Rails.root.join('spec', 'support', f) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a20f4c0597..b43f38ef20 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,9 +15,11 @@ require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'rspec/retry' -require 'knapsack' -Knapsack::Adapters::RSpecAdapter.bind +if ENV['CI'] + require 'knapsack' + Knapsack::Adapters::RSpecAdapter.bind +end # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. From 519c758fa9fabe9b73f784a9f5b80579b2d84325 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 17 May 2016 12:07:11 +0100 Subject: [PATCH 395/507] Removable labels from filtered issuables label bar When filtering by labels, a remove button appears next to each label. This then removes that label & refreshes the issuable filter form Closes #15474 --- app/assets/javascripts/issuable.js.coffee | 24 ++++++++++++++++++- app/views/shared/_labels_row.html.haml | 4 +++- spec/features/issues/filter_by_labels_spec.rb | 10 ++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee index 6504e48110..9801e61dab 100644 --- a/app/assets/javascripts/issuable.js.coffee +++ b/app/assets/javascripts/issuable.js.coffee @@ -6,12 +6,20 @@ issuable_created = false Issuable.initTemplates() Issuable.initSearch() Issuable.initChecks() + Issuable.initLabelFilterRemove() initTemplates: -> Issuable.labelRow = _.template( '<% _.each(labels, function(label){ %> - <%= _.escape(label.title) %> + + + <%= _.escape(label.title) %> + + + <% }); %>' ) @@ -35,6 +43,20 @@ issuable_created = false Issuable.filterResults $form , 500) + initLabelFilterRemove: -> + $(document) + .off 'click', '.js-label-filter-remove' + .on 'click', '.js-label-filter-remove', (e) -> + $button = $(@) + + # Remove the label input box + $('input[name="label_name[]"]') + .filter -> @value is $button.data('label') + .remove() + + # Submit the form to get new data + Issuable.filterResults $('.filter-form') + toggleLabelFilters: -> $filteredLabels = $('.filtered-labels') if $filteredLabels.find('.label-row').length > 0 diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml index dc89e36419..2bea183334 100644 --- a/app/views/shared/_labels_row.html.haml +++ b/app/views/shared/_labels_row.html.haml @@ -1,3 +1,5 @@ - labels.each do |label| %span.label-row - = link_to_label(label, tooltip: false) + = link_to_label(label, tooltip: true) + %button.btn.btn-sm.btn-transparent.append-right-5.js-label-filter-remove{ type: "button", data: { label: label.title } } + = icon("times") diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index 7f65468414..2015b0434f 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -54,6 +54,11 @@ feature 'Issue filtering by Labels', feature: true do expect(find('.filtered-labels')).not_to have_content "feature" expect(find('.filtered-labels')).not_to have_content "enhancement" end + + it 'should remove label "bug"' do + first('.js-label-filter-remove').click + expect(find('.filtered-labels')).to have_no_content "bug" + end end context 'filter by label feature', js: true do @@ -135,6 +140,11 @@ feature 'Issue filtering by Labels', feature: true do it 'should not show label "bug" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "bug" end + + it 'should remove label "enhancement"' do + first('.js-label-filter-remove').click + expect(find('.filtered-labels')).to have_no_content "enhancement" + end end context 'filter by label enhancement and bug in issues list', js: true do From cba0321f9640f3dd8ff0e2e1d8a3f489a4daf0ca Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 26 May 2016 18:54:07 +0100 Subject: [PATCH 396/507] Updated design --- app/assets/javascripts/issuable.js.coffee | 10 ++++------ app/assets/stylesheets/pages/labels.scss | 17 +++++++++++++++++ app/views/shared/_labels_row.html.haml | 11 ++++++++--- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee index 9801e61dab..ef0d35d0dd 100644 --- a/app/assets/javascripts/issuable.js.coffee +++ b/app/assets/javascripts/issuable.js.coffee @@ -11,13 +11,11 @@ issuable_created = false initTemplates: -> Issuable.labelRow = _.template( '<% _.each(labels, function(label){ %> - - - - <%= _.escape(label.title) %> - + + + <%= _.escape(label.title) %> - diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 2cd9d74b2d..d010b752dd 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -169,3 +169,20 @@ } } } + +.filtered-labels { + .label-row { + &:not(:last-child) { + margin-right: 5px; + } + } + + .label-remove { + border-left: 1px solid; + z-index: 3; + } + + .btn { + color: inherit; + } +} diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml index 2bea183334..87028ececd 100644 --- a/app/views/shared/_labels_row.html.haml +++ b/app/views/shared/_labels_row.html.haml @@ -1,5 +1,10 @@ - labels.each do |label| - %span.label-row - = link_to_label(label, tooltip: true) - %button.btn.btn-sm.btn-transparent.append-right-5.js-label-filter-remove{ type: "button", data: { label: label.title } } + %span.label-row.btn-group{ role: "group", aria: { label: escape_once(label.name) }, style: "color: #{text_color_for_bg(label.color)}" } + = link_to namespace_project_label_path(@project.namespace, @project, label), + class: "btn btn-transparent has-tooltip", + style: "background-color: #{label.color};", + title: escape_once(label.description), + data: { container: "body" } do + = escape_once label.name + %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } } = icon("times") From f4eb55724f9eef401283f11fb617261a390e42a5 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 1 Jun 2016 11:05:46 +0100 Subject: [PATCH 397/507] Darken the border between remove label and label name --- app/assets/stylesheets/pages/labels.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index d010b752dd..26128fcea8 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -178,7 +178,7 @@ } .label-remove { - border-left: 1px solid; + border-left: 1px solid rgba(0, 0, 0, .1); z-index: 3; } From d3ff691d768fcb40171c11cade8a659a62534160 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 2 Jun 2016 10:40:42 +0100 Subject: [PATCH 398/507] Fixed issue with dropdown toggle not updating Added tests --- app/assets/javascripts/gl_dropdown.js.coffee | 8 ++++-- app/assets/javascripts/issuable.js.coffee | 1 + spec/features/issues/filter_by_labels_spec.rb | 25 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 7c7334e9e4..b49bd4565a 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -211,6 +211,7 @@ class GitLabDropdown @dropdown.on "shown.bs.dropdown", @opened @dropdown.on "hidden.bs.dropdown", @hidden + $(@el).on "update.label", @updateLabel @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate @dropdown.on 'keyup', (e) => if e.which is 27 # Escape key @@ -453,7 +454,7 @@ class GitLabDropdown # Toggle the dropdown label if @options.toggleLabel - $(@el).find(".dropdown-toggle-text").text @options.toggleLabel + @updateLabel() else selectedObject else if el.hasClass(INDETERMINATE_CLASS) @@ -480,7 +481,7 @@ class GitLabDropdown # Toggle the dropdown label if @options.toggleLabel - $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) + @updateLabel(selectedObject, el) if value? if !field.length and fieldName @addInput(fieldName, value) @@ -579,6 +580,9 @@ class GitLabDropdown # Scroll the dropdown content up $dropdownContent.scrollTop(listItemTop - dropdownContentTop) + updateLabel: (selected = null, el = null) => + $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el) + $.fn.glDropdown = (opts) -> return @.each -> if (!$.data @, 'glDropdown') diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee index ef0d35d0dd..c244712003 100644 --- a/app/assets/javascripts/issuable.js.coffee +++ b/app/assets/javascripts/issuable.js.coffee @@ -54,6 +54,7 @@ issuable_created = false # Submit the form to get new data Issuable.filterResults $('.filter-form') + $('.js-label-select').trigger('update.label') toggleLabelFilters: -> $filteredLabels = $('.filtered-labels') diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index 2015b0434f..0ec8b6b180 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -174,4 +174,29 @@ feature 'Issue filtering by Labels', feature: true do expect(find('.filtered-labels')).not_to have_content "feature" end end + + context 'remove filtered labels', js: true do + before do + page.within '.labels-filter' do + click_button 'Label' + click_link 'bug' + find('.dropdown-menu-close').click + end + + page.within '.filtered-labels' do + expect(page).to have_content 'bug' + end + end + + it 'should allow user to remove filtered labels' do + page.within '.filtered-labels' do + first('.js-label-filter-remove').click + expect(page).not_to have_content 'bug' + end + + page.within '.labels-filter' do + expect(page).not_to have_content 'bug' + end + end + end end From 8cd17f748bb564d6aeaa4fa8339b2d5431a10697 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Jun 2016 12:24:10 +0100 Subject: [PATCH 399/507] Fixed dropdown line-height Most noticable on the commit header --- app/assets/stylesheets/framework/dropdowns.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 1ce7c57ebc..d4d579a083 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -124,6 +124,7 @@ position: relative; padding: 5px 10px; color: $dropdown-link-color; + line-height: initial; text-overflow: ellipsis; border-radius: 2px; white-space: nowrap; From 1611c57c6c7d533ae9d0aaabf72cc058fc3cec08 Mon Sep 17 00:00:00 2001 From: Bartholomew Date: Wed, 8 Jun 2016 12:43:43 +0000 Subject: [PATCH 400/507] fix empty user projects snippets list --- app/views/users/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 8268380daf..92305594a8 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -79,10 +79,10 @@ %li.js-contributed-tab = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do Contributed projects - %li.projects-tab + %li.js-projects-tab = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects - %li.snippets-tab + %li.js-snippets-tab = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do Snippets From 3b50d96b8aaa7e18efded9a80c7641d1364de5c9 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 7 Jun 2016 23:15:45 -0700 Subject: [PATCH 401/507] Fix endless redirections when accessing user OAuth applications when they are disabled Also hides the "Applications" nav button if OAuth applications are disabled by the admin. Closes #14770 --- CHANGELOG | 1 + .../oauth/applications_controller.rb | 2 +- app/views/layouts/nav/_profile.html.haml | 11 +++---- .../oauth/applications_controller_spec.rb | 29 +++++++++++++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 spec/controllers/oauth/applications_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 5136756079..f46ef823cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.9.0 (unreleased) - Bulk assign/unassign labels to issues. - Ability to prioritize labels !4009 / !3205 (Thijs Wouters) + - Fix endless redirections when accessing user OAuth applications when they are disabled - Allow enabling wiki page events from Webhook management UI - Bump rouge to 1.11.0 - Make EmailsOnPushWorker use Sidekiq mailers queue diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index c6bdd0602c..0f54dfa4ef 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -32,7 +32,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController def verify_user_oauth_applications_enabled return if current_application_settings.user_oauth_applications? - redirect_to applications_profile_url + redirect_to profile_path end def set_index_vars diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 2efc6c48a4..09d9f0184b 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -10,11 +10,12 @@ = icon('gear fw') %span Account - = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path, title: 'Applications' do - = icon('cloud fw') - %span - Applications + - if current_application_settings.user_oauth_applications? + = nav_link(controller: 'oauth/applications') do + = link_to applications_profile_path, title: 'Applications' do + = icon('cloud fw') + %span + Applications = nav_link(controller: :emails) do = link_to profile_emails_path, title: 'Emails' do = icon('envelope-o fw') diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb new file mode 100644 index 0000000000..af37830489 --- /dev/null +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Oauth::ApplicationsController do + let(:user) { create(:user) } + + context 'project members' do + before do + sign_in(user) + end + + describe 'GET #index' do + it 'shows list of applications' do + get :index + + expect(response.status).to eq(200) + end + + it 'redirects back to profile page if OAuth applications are disabled' do + settings = double(user_oauth_applications?: false) + allow_any_instance_of(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(settings) + + get :index + + expect(response.status).to eq(302) + expect(response).to redirect_to(profile_path) + end + end + end +end From c3024affb3d10b20928b0cce347a4dc6913a507c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 16:29:40 +0300 Subject: [PATCH 402/507] Use hex value instead of rgba for gray border color Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 60207ecf1d..35a75b96c5 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -121,7 +121,7 @@ $border-white-normal: #d6dae2; $border-white-dark: #c6cacf; $border-gray-light: #dcdcdc; -$border-gray-normal: rgba(0, 0, 0, 0.10); +$border-gray-normal: #d7d7d7; $border-gray-dark: #c6cacf; $border-green-light: #2faa60; From e533890bf19f6d0898ab6050f5f5ae7d061f3781 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 16:38:18 +0300 Subject: [PATCH 403/507] Improve button color for issue and mr pages * make sidebar button white * make header button gray if not primary Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/issues/show.html.haml | 8 ++++---- .../projects/merge_requests/show/_mr_title.html.haml | 6 +++--- app/views/shared/issuable/_sidebar.html.haml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index b2f14a5407..0577f4faee 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -38,13 +38,13 @@ %li = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do = icon('plus') New issue - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit' do + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-gray btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-gray btn-grouped issuable-edit' do = icon('pencil-square-o') Edit diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 36c275e8be..065fabd410 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -25,8 +25,8 @@ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' %li = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' - = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-gray btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-gray btn-grouped issuable-edit" do = icon('pencil-square-o') Edit diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 1ec2436c83..c7991d53a0 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -133,7 +133,7 @@ .title.hide-collapsed Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } + %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %span= subscribed ? 'Unsubscribe' : 'Subscribe' .subscription-status.hide-collapsed{data: {status: subscribtion_status}} .unsubscribed{class: ( 'hidden' if subscribed )} From 366ad9ff72b44df384bffe562ac66f85aff24c65 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 17:11:43 +0300 Subject: [PATCH 404/507] Reject idea of using white/gray button depends on bg color Signed-off-by: Dmitriy Zaporozhets --- doc/development/ui_guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index 23760a14b3..5893b7c219 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -49,8 +49,8 @@ information from database or file system ## Buttons * Button should contain icon or text. Exceptions should be approved by UX designer. -* Use gray button on white background or white button on gray background. * Use red button for destructive actions (not revertable). For example removing issue. * Use green or blue button for primary action. Primary button should be only one. Do not use both green and blue button in one form. +* For all other cases use default white button From 1dc001730b7d08100c0af8c210b5768c7dfbdd32 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 17:20:07 +0300 Subject: [PATCH 405/507] Cleanup and imrpove issue/mr buttons Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/buttons.scss | 15 +++++++++++++-- app/assets/stylesheets/framework/variables.scss | 3 +++ app/views/projects/issues/_new_branch.html.haml | 12 ++++++------ app/views/projects/issues/index.html.haml | 1 - app/views/projects/issues/show.html.haml | 6 ++---- app/views/projects/merge_requests/_show.html.haml | 4 +--- app/views/projects/merge_requests/index.html.haml | 1 - .../merge_requests/show/_mr_title.html.haml | 5 ++--- 8 files changed, 27 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 467f3b35d7..625200cbca 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -142,15 +142,26 @@ } &.btn-grouped { - margin-right: 7px; + margin-right: $btn-side-margin; float: left; + + &.inline { + float: none; + } + &:last-child { margin-right: 0; } + + &.btn-sm { + margin-right: $btn-sm-side-margin; + } + &.btn-xs { - margin-right: 3px; + margin-right: $btn-xs-side-margin; } } + &.disabled { pointer-events: auto !important; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 35a75b96c5..d8ea07559a 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -79,6 +79,9 @@ $provider-btn-not-active-color: #4688f1; $link-underline-blue: #4a8bee; $layout-link-gray: #7e7c7c; $todo-alert-blue: #428bca; +$btn-side-margin: 7px; +$btn-sm-side-margin: 5px; +$btn-xs-side-margin: 5px; /* * Color schema diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 469429ccf3..e93b7e0d66 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,13 +1,13 @@ - if can?(current_user, :push_code, @project) .pull-right #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} - = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do + = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), + method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do .checking - %i.fa.fa-spinner.fa-spin + = icon('spinner spin') Checking branches - .available(style="display: none") - %i.fa.fa-code-fork + .available.hide New branch - .unavailable(style="display: none") - %i.fa.fa-exclamation-triangle + .unavailable.hide + = icon('exclamation-triangle') New branch unavailable diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 19a6f4a91f..2a4027a6ec 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -15,7 +15,6 @@ = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do - = icon('plus') New Issue = render 'shared/issuable/filter', type: :issues diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 0577f4faee..9b6a97c095 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -39,13 +39,11 @@ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - if can?(current_user, :create_issue, @project) = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do - = icon('plus') New issue - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-gray btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-gray btn-grouped issuable-edit' do - = icon('pencil-square-o') + = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do Edit diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index c30459ae56..c4df8bd504 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -14,13 +14,11 @@ - if @merge_request.open? .pull-right - if @merge_request.source_branch_exists? - = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do - = icon('cloud-download fw') + = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do Check out branch %span.dropdown %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } - = icon('download') Download as %span.caret %ul.dropdown-menu diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index b517e874b0..c8653cb0c3 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -10,7 +10,6 @@ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - if merge_project = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do - = icon('plus') New Merge Request = render 'shared/issuable/filter', type: :merge_requests diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 065fabd410..5bf5210aea 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -26,7 +26,6 @@ %li = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-gray btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-gray btn-grouped issuable-edit" do - = icon('pencil-square-o') + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do Edit From 3883bc05d26d930689115c6b5d8750c594420615 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 8 Jun 2016 12:27:31 +0200 Subject: [PATCH 406/507] Remove notification settings in batches This improves performance of the duplicate notification settings migration by removing duplicates in batches instead of using one big "DELETE FROM" query. The previous query would locally run over 45 minutes without even finishing. This new setup finished in a matter of seconds. Fixes #18289 --- ...remove_duplicated_notification_settings.rb | 31 +++++++++++++++++-- db/schema.rb | 4 ++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/db/migrate/20160603180330_remove_duplicated_notification_settings.rb b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb index c2fcac4c53..fe1c863b5b 100644 --- a/db/migrate/20160603180330_remove_duplicated_notification_settings.rb +++ b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb @@ -1,7 +1,32 @@ class RemoveDuplicatedNotificationSettings < ActiveRecord::Migration def up - execute <<-SQL - DELETE FROM notification_settings WHERE id NOT IN ( SELECT min_id from (SELECT MIN(id) as min_id FROM notification_settings GROUP BY user_id, source_type, source_id) as dups ) - SQL + duplicates = exec_query(%Q{ + SELECT user_id, source_type, source_id + FROM notification_settings + GROUP BY user_id, source_type, source_id + HAVING COUNT(*) > 1 + }) + + duplicates.each do |row| + uid = row['user_id'] + stype = connection.quote(row['source_type']) + sid = row['source_id'] + + execute(%Q{ + DELETE FROM notification_settings + WHERE user_id = #{uid} + AND source_type = #{stype} + AND source_id = #{sid} + AND id != ( + SELECT id FROM ( + SELECT min(id) AS id + FROM notification_settings + WHERE user_id = #{uid} + AND source_type = #{stype} + AND source_id = #{sid} + ) min_ids + ) + }) + end end end diff --git a/db/schema.rb b/db/schema.rb index 69e37470de..00829d63b6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160530150109) do +ActiveRecord::Schema.define(version: 20160603182247) do + # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "pg_trgm" @@ -676,6 +677,7 @@ ActiveRecord::Schema.define(version: 20160530150109) do end add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree + add_index "notification_settings", ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true, using: :btree add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree create_table "oauth_access_grants", force: :cascade do |t| From ad83c3085513dd248b979d445e545e88a17c6ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Fri, 3 Jun 2016 14:47:09 -0400 Subject: [PATCH 407/507] Remove `projects` inclusion in `notes_with_associations` to skip some unnecessary queries `notes_with_associations` are used for `participant` declarations, but `Participable` only really cares about the target entity project, and not the participants projects. `notes_with_associations` are also used in `Commit::has_been_reverted?` which employs the reference extractor of the commit, so no references to the notes projects are made there (`Mentionable::all_references` cares only about the `author` and other `attr_mentionable`). A paralel situation occurs on `Issue::referenced_merge_requests`. --- app/models/commit.rb | 2 +- app/models/concerns/issuable.rb | 2 +- app/models/snippet.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index b5637bc4fb..d69d518fad 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -198,7 +198,7 @@ class Commit end def notes_with_associations - notes.includes(:author, :project) + notes.includes(:author) end def method_missing(m, *args, &block) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 92526a9914..58e7557fdc 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -239,7 +239,7 @@ module Issuable end def notes_with_associations - notes.includes(:author, :project) + notes.includes(:author) end def updated_tasks diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 407697b745..f8034cb5e6 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -102,7 +102,7 @@ class Snippet < ActiveRecord::Base end def notes_with_associations - notes.includes(:author, :project) + notes.includes(:author) end class << self From fade1a4cdebb4124048e9764486812627333ff95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Mon, 6 Jun 2016 16:19:39 -0400 Subject: [PATCH 408/507] Fix pseudo n+1 queries with Note and Note Authors in issuables APIs This was not a clear cut n+1 query, given that if you're directly subscribed to all issues that the API is returning you never really need to check for the notes. However, if you're subscribed to _all_ of them, then for each issuable you need to go once to `notes`, and once to `users` (for the authors). By preemtively loading notes and authors, at worst you have 1 extra query, and at best you saved 2n extra queries. We also took advantage of this preloading of notes when counting user notes. --- app/models/concerns/issuable.rb | 24 +++++++++++++++++++++--- lib/api/issues.rb | 4 ++-- lib/api/merge_requests.rb | 2 +- spec/models/concerns/issuable_spec.rb | 26 ++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 58e7557fdc..0ccd3474b8 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -17,7 +17,12 @@ module Issuable belongs_to :assignee, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :milestone - has_many :notes, as: :noteable, dependent: :destroy + has_many :notes, as: :noteable, dependent: :destroy do + def authors_loaded? + # We check first if we're loaded to not load unnecesarily. + loaded? && to_a.all? { |note| note.association(:author).loaded? } + end + end has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links has_many :todos, as: :target, dependent: :destroy @@ -44,6 +49,7 @@ module Issuable scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } + scope :inc_notes_with_associations, -> { includes(notes: :author) } scope :references_project, -> { references(:project) } scope :non_archived, -> { join_project.where(projects: { archived: false }) } @@ -179,7 +185,13 @@ module Issuable end def user_notes_count - notes.user.count + if notes.loaded? + # Use the in-memory association to select and count to avoid hitting the db + notes.to_a.count { |note| !note.system? } + else + # do the count query + notes.user.count + end end def subscribed_without_subscriptions?(user) @@ -239,7 +251,13 @@ module Issuable end def notes_with_associations - notes.includes(:author) + # If A has_many Bs, and B has_many Cs, and you do + # `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord + # will do the inclusion again. So, we check if all notes in the relation + # already have their authors loaded (possibly because the scope + # `inc_notes_with_associations` was used) and skip the inclusion if that's + # the case. + notes.authors_loaded? ? notes : notes.includes(:author) end def updated_tasks diff --git a/lib/api/issues.rb b/lib/api/issues.rb index f59a4d6c01..4c43257c48 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -51,7 +51,7 @@ module API # GET /issues?labels=foo,bar # GET /issues?labels=foo,bar&state=opened get do - issues = current_user.issues + issues = current_user.issues.inc_notes_with_associations issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues.reorder(issuable_order_by => issuable_sort) @@ -82,7 +82,7 @@ module API # GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /issues?iid=42 get ":id/issues" do - issues = user_project.issues.visible_to_user(current_user) + issues = user_project.issues.inc_notes_with_associations.visible_to_user(current_user) issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 2e7836dc8f..43221d5622 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -41,7 +41,7 @@ module API # get ":id/merge_requests" do authorize! :read_merge_request, user_project - merge_requests = user_project.merge_requests + merge_requests = user_project.merge_requests.inc_notes_with_associations unless params[:iid].nil? merge_requests = filter_by_iid(merge_requests, params[:iid]) diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index dd03d64f75..efbcbf72f7 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -10,6 +10,16 @@ describe Issue, "Issuable" do it { is_expected.to belong_to(:assignee) } it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } + + context 'Notes' do + let!(:note) { create(:note, noteable: issue, project: issue.project) } + let(:scoped_issue) { Issue.includes(notes: :author).find(issue.id) } + + it 'indicates if the notes have their authors loaded' do + expect(issue.notes).not_to be_authors_loaded + expect(scoped_issue.notes).to be_authors_loaded + end + end end describe 'Included modules' do @@ -245,6 +255,22 @@ describe Issue, "Issuable" do end end + describe '#user_notes_count' do + let(:project) { create(:project) } + let(:issue1) { create(:issue, project: project) } + let(:issue2) { create(:issue, project: project) } + + before do + create_list(:note, 3, noteable: issue1, project: project) + create_list(:note, 6, noteable: issue2, project: project) + end + + it 'counts the user notes' do + expect(issue1.user_notes_count).to be(3) + expect(issue2.user_notes_count).to be(6) + end + end + describe "votes" do let(:project) { issue.project } From e7bf943127c9798d55bf59552c55e8400e47b56e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 8 Jun 2016 16:29:09 +0200 Subject: [PATCH 409/507] Update Knapsack report only on master --- .gitlab-ci.yml | 2 ++ db/schema.rb | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5ef3081395..3dc48a8946 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,6 +61,8 @@ update-knapsack: - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json - rm -f knapsack/*_node_*.json + only: + - master # Execute all testing suites diff --git a/db/schema.rb b/db/schema.rb index 69e37470de..00829d63b6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160530150109) do +ActiveRecord::Schema.define(version: 20160603182247) do + # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "pg_trgm" @@ -676,6 +677,7 @@ ActiveRecord::Schema.define(version: 20160530150109) do end add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree + add_index "notification_settings", ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true, using: :btree add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree create_table "oauth_access_grants", force: :cascade do |t| From bf9e482695481f5a3ec32f7c36d7ec051ec96fa5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 17:32:49 +0300 Subject: [PATCH 410/507] Remove unnecessary icons from buttons Signed-off-by: Dmitriy Zaporozhets --- app/views/dashboard/_groups_head.html.haml | 1 - app/views/dashboard/_projects_head.html.haml | 1 - app/views/groups/group_members/_group_member.html.haml | 4 ++-- app/views/groups/show.html.haml | 1 - app/views/projects/branches/_branch.html.haml | 2 -- app/views/projects/labels/index.html.haml | 1 - app/views/projects/project_members/_group_members.html.haml | 1 - app/views/projects/project_members/_project_member.html.haml | 4 ++-- app/views/shared/_new_project_item_select.html.haml | 3 +-- 9 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 3d17f74b70..23c145ebbb 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -9,5 +9,4 @@ - if current_user.can_create_group? .nav-controls = link_to new_group_path, class: "btn btn-new" do - = icon('plus') New Group diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 9da3fcbd98..d35f332e1e 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -18,5 +18,4 @@ = render 'shared/projects/dropdown' - if current_user.can_create_project? = link_to new_project_path, class: 'btn btn-new' do - = icon('plus') New Project diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 60234be8f8..271700e6db 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -36,7 +36,7 @@ - if can?(current_user, :update_group_member, member) = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do - %i.fa.fa-pencil-square-o + = icon('pencil') - if can?(current_user, :destroy_group_member, member)   @@ -46,7 +46,7 @@ Leave - else = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-minus.fa-inverse + = icon('trash') .edit-member.hide.js-toggle-content %br diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 77c297255b..54e89a0191 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -35,7 +35,6 @@ = render 'shared/projects/dropdown' - if can? current_user, :create_projects, @group = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do - = icon('plus') New Project .tab-content diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 57e507e68c..87c732626a 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -21,12 +21,10 @@ .controls.hidden-xs - if create_mr_button?(@repository.root_ref, branch.name) = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do - = icon('plus') Merge Request - if branch.name != @repository.root_ref = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do - = icon("exchange") Compare - if can_remove_branch?(@project, branch.name) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index c72eddba37..93583c9260 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -7,7 +7,6 @@ .nav-controls - if can?(current_user, :admin_label, @project) = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do - = icon('plus') New label .labels diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index c53033e367..6671ee2c6d 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -7,7 +7,6 @@ - if can?(current_user, :admin_group_member, @group) .controls = link_to group_group_members_path(@group), class: 'btn' do - = icon('pencil-square-o') Manage group members %ul.content-list - members.limit(20).each do |member| diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 05bf3a7ef6..1e53b8e37d 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -34,7 +34,7 @@ - if can?(current_user, :update_project_member, member) = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do - %i.fa.fa-pencil-square-o + = icon('pencil') - if can?(current_user, :destroy_project_member, member)   @@ -44,7 +44,7 @@ Leave - else = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do - %i.fa.fa-minus.fa-inverse + = icon('trash') .edit-member.hide.js-toggle-content %br diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index 1c58345278..51622931e2 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,8 +1,7 @@ - if @projects.any? - .prepend-left-10.project-item-select-holder + .project-item-select-holder = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' } %a.btn.btn-new.new-project-item-select-button - = icon('plus') = local_assigns[:label] %b.caret From 13d941e185c3ca1e9ad54c97f878e9cfdd32e5aa Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 8 Jun 2016 16:49:01 +0200 Subject: [PATCH 411/507] Make Omniauth providers specs to not modify global configuration --- CHANGELOG | 1 + spec/controllers/import/import_spec_helper.rb | 2 +- spec/lib/gitlab/bitbucket_import/client_spec.rb | 4 +++- spec/lib/gitlab/bitbucket_import/importer_spec.rb | 4 +++- spec/lib/gitlab/gitlab_import/client_spec.rb | 4 +++- spec/services/projects/import_service_spec.rb | 2 +- 6 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0506854599..ca312a111f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,6 +36,7 @@ v 8.9.0 (unreleased) - Use downcased path to container repository as this is expected path by Docker - Projects pending deletion will render a 404 page - Measure queue duration between gitlab-workhorse and Rails + - Make Omniauth providers specs to not modify global configuration - Make authentication service for Container Registry to be compatible with < Docker 1.11 - Add Application Setting to configure Container Registry token expire delay (default 5min) - Cache assigned issue and merge request counts in sidebar nav diff --git a/spec/controllers/import/import_spec_helper.rb b/spec/controllers/import/import_spec_helper.rb index 9d7648e25a..6710962f08 100644 --- a/spec/controllers/import/import_spec_helper.rb +++ b/spec/controllers/import/import_spec_helper.rb @@ -28,6 +28,6 @@ module ImportSpecHelper app_id: 'asd123', app_secret: 'asd123' ) - Gitlab.config.omniauth.providers << provider + allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) end end diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb index 7718689e6d..760d66a148 100644 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' describe Gitlab::BitbucketImport::Client, lib: true do + include ImportSpecHelper + let(:token) { '123456' } let(:secret) { 'secret' } let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) } before do - Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") + stub_omniauth_provider('bitbucket') end it 'all OAuth client options are symbols' do diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 1a833f255a..aa00f32bec 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' describe Gitlab::BitbucketImport::Importer, lib: true do + include ImportSpecHelper + before do - Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") + stub_omniauth_provider('bitbucket') end let(:statuses) do diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb index e6831e7c38..cd8e805466 100644 --- a/spec/lib/gitlab/gitlab_import/client_spec.rb +++ b/spec/lib/gitlab/gitlab_import/client_spec.rb @@ -1,11 +1,13 @@ require 'spec_helper' describe Gitlab::GitlabImport::Client, lib: true do + include ImportSpecHelper + let(:token) { '123456' } let(:client) { Gitlab::GitlabImport::Client.new(token) } before do - Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "gitlab") + stub_omniauth_provider('gitlab') end it 'all OAuth2 client options are symbols' do diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 9d90bfceb7..068c9a1219 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -124,7 +124,7 @@ describe Projects::ImportService, services: true do } ) - Gitlab.config.omniauth.providers << provider + allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) end end end From cd36f293991e05323fe90cfdfb31f8315d0d7445 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 18:06:28 +0300 Subject: [PATCH 412/507] Improve button margin for btn-grouped css class Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/buttons.scss | 42 +++++++++---------- .../group_members/_group_member.html.haml | 2 +- .../project_members/_project_member.html.haml | 2 +- .../shared/milestones/_milestone.html.haml | 8 ++-- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 625200cbca..1e3083cce5 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -79,6 +79,23 @@ @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active); } +@mixin btn-with-margin { + margin-left: $btn-side-margin; + float: left; + + &.inline { + float: none; + } + + &.btn-sm { + margin-left: $btn-sm-side-margin; + } + + &.btn-xs { + margin-left: $btn-xs-side-margin; + } +} + .btn { @include btn-default; @include btn-white; @@ -142,24 +159,7 @@ } &.btn-grouped { - margin-right: $btn-side-margin; - float: left; - - &.inline { - float: none; - } - - &:last-child { - margin-right: 0; - } - - &.btn-sm { - margin-right: $btn-sm-side-margin; - } - - &.btn-xs { - margin-right: $btn-xs-side-margin; - } + @include btn-with-margin; } &.disabled { @@ -203,11 +203,7 @@ .btn-group { &.btn-grouped { - margin-right: 7px; - float: left; - &:last-child { - margin-right: 0; - } + @include btn-with-margin; } } diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 271700e6db..6bb542e658 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -34,7 +34,7 @@ %strong.member-access-level= member.human_access - if show_controls - if can?(current_user, :update_group_member, member) - = button_tag class: "btn-xs btn js-toggle-button", + = button_tag class: "btn-xs btn btn-grouped inline js-toggle-button", title: 'Edit access level', type: 'button' do = icon('pencil') diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 1e53b8e37d..268f140d7d 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -32,7 +32,7 @@ .pull-right %strong= member.human_access - if can?(current_user, :update_project_member, member) - = button_tag class: "btn-xs btn js-toggle-button", + = button_tag class: "btn-xs btn-grouped inline btn js-toggle-button", title: 'Edit access level', type: 'button' do = icon('pencil') diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 6b25745c55..acc3ccf4dc 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -35,11 +35,9 @@ .col-sm-6= render('shared/milestone_expired', milestone: milestone) .col-sm-6 - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do - = icon('pencil-square-o') + = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do Edit \ - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" - = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do - = icon('trash-o') + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do Delete From c5e6292e5f9cdb71b785b00a0140e82528e095c7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 18:14:20 +0300 Subject: [PATCH 413/507] Use default button size in the content list controls Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/lists.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 96e7aa4fb1..dec327669e 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -138,7 +138,9 @@ ul.content-list { float: right; .btn { - padding: 10px 14px; + padding: $gl-vert-padding $gl-btn-padding; + margin-top: 4px; + margin-bottom: 4px; } } From 480d74685dff61fdbcf3034224fa201d60181445 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 8 Jun 2016 18:18:54 +0300 Subject: [PATCH 414/507] Use title attribute instead of data-original-title. --- app/assets/javascripts/milestone_select.js.coffee | 4 ++-- app/views/shared/issuable/_sidebar.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index a5c5a349f1..df6ba3167d 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -25,7 +25,7 @@ class @MilestoneSelect if issueUpdateURL milestoneLinkTemplate = _.template( ' - + <%= _.escape(title) %> ' @@ -34,7 +34,7 @@ class @MilestoneSelect milestoneLinkNoneTemplate = '
                                        None
                                        ' collapsedSidebarLabelTemplate = _.template( - ' + ' <%= _.escape(title) %> ' ) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index dafd11b90d..8cfda13083 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -56,7 +56,7 @@ = icon('clock-o') %span - if issuable.milestone - %span.has-tooltip{"data-container" => "body", "data-placement" => "left", "data-original-title" => milestone_remaining_days(issuable.milestone, false)} + %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1, placement: 'left'}} = issuable.milestone.title - else None @@ -68,7 +68,7 @@ .value.bold.hide-collapsed - if issuable.milestone = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do - %span.has-tooltip{"data-container" => "body", "data-original-title" => milestone_remaining_days(issuable.milestone, false)} + %span.has-tooltip{ "title" => milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1 } } = issuable.milestone.title - else .light None From b868b814167564176bc6a08d01170e6a43c97fc4 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 8 Jun 2016 18:19:04 +0300 Subject: [PATCH 415/507] Revert milestone_remaining_days helper. --- app/helpers/milestones_helper.rb | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index f6a8ae3fd6..87fc2db690 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -54,18 +54,13 @@ module MilestonesHelper end end - def milestone_remaining_days(milestone, withContentTag = true) + def milestone_remaining_days(milestone) if milestone.expired? - withContentTag ? content_tag(:strong, 'expired') : 'expired' + content_tag(:strong, 'expired') elsif milestone.due_date - days = milestone.remaining_days - - if withContentTag - content = content_tag(:strong, days) - content << " #{'day'.pluralize(days)} remaining" - else - "#{days} #{'day'.pluralize(days)} remaining" - end + days = milestone.remaining_days + content = content_tag(:strong, days) + content << " #{'day'.pluralize(days)} remaining" end end end From 9560639e817147064e3929243668e49336a4f3f5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 8 Jun 2016 17:21:08 +0200 Subject: [PATCH 416/507] Move ImportSpecHelper to spec/support/ --- spec/{controllers/import => support}/import_spec_helper.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/{controllers/import => support}/import_spec_helper.rb (100%) diff --git a/spec/controllers/import/import_spec_helper.rb b/spec/support/import_spec_helper.rb similarity index 100% rename from spec/controllers/import/import_spec_helper.rb rename to spec/support/import_spec_helper.rb From 7d3dae2309025c796bdbdcec9ab2b0f5965ff166 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 18:23:45 +0300 Subject: [PATCH 417/507] Use responsive top-area on project branches and tags pages Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/branches/index.html.haml | 47 ++++++++++----------- app/views/projects/tags/index.html.haml | 14 +++--- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 0d59c3884c..e0367c4027 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -3,31 +3,30 @@ = render "projects/commits/head" %div{ class: (container_class) } - .row-content-block.second-block.content-component-block - .pull-right - - if can? current_user, :push_code, @project - = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do - = icon('plus') - New branch -   - .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = @sort.humanize - - else - Name - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to namespace_project_branches_path(sort: nil) do - Name - = link_to namespace_project_branches_path(sort: 'recently_updated') do - = sort_title_recently_updated - = link_to namespace_project_branches_path(sort: 'last_updated') do - = sort_title_oldest_updated - .oneline + .top-area + .nav-text Protected branches can be managed in project settings + + - if can? current_user, :push_code, @project + .nav-controls + = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do + New branch + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + - if @sort.present? + = @sort.humanize + - else + Name + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to namespace_project_branches_path(sort: nil) do + Name + = link_to namespace_project_branches_path(sort: 'recently_updated') do + = sort_title_recently_updated + = link_to namespace_project_branches_path(sort: 'last_updated') do + = sort_title_oldest_updated - unless @branches.empty? %ul.content-list.all-branches - @branches.each do |branch| diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 9ff805a898..2779084fe3 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -3,15 +3,15 @@ = render "projects/commits/head" %div{ class: (container_class) } - .row-content-block.second-block.content-component-block - - if can? current_user, :push_code, @project - .pull-right - = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do - = icon('plus') - New tag - .oneline + .top-area + .nav-text Tags give the ability to mark specific points in history as being important + - if can? current_user, :push_code, @project + .nav-controls + = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do + New tag + .tags - unless @tags.empty? %ul.content-list From 4f5ee68f09672d08a4e3997a771e062f56665dab Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 8 Jun 2016 18:41:03 +0300 Subject: [PATCH 418/507] Copy changes for milestone due dates. --- app/assets/javascripts/lib/common_utils.js.coffee | 2 +- app/helpers/milestones_helper.rb | 2 +- app/views/projects/milestones/show.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/lib/common_utils.js.coffee b/app/assets/javascripts/lib/common_utils.js.coffee index a7cc07608a..0000e99a65 100644 --- a/app/assets/javascripts/lib/common_utils.js.coffee +++ b/app/assets/javascripts/lib/common_utils.js.coffee @@ -5,7 +5,7 @@ return '' unless time suffix or= 'remaining' - expiredLabel or= 'expired' + expiredLabel or= 'Past due' jQuery.timeago.settings.allowFuture = yes diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 87fc2db690..e28ca13ec2 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -56,7 +56,7 @@ module MilestonesHelper def milestone_remaining_days(milestone) if milestone.expired? - content_tag(:strong, 'expired') + content_tag(:strong, 'Past Due') elsif milestone.due_date days = milestone.remaining_days content = content_tag(:strong, days) diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 19944e3e02..58b1f8f664 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -6,7 +6,7 @@ - if @milestone.closed? Closed - elsif @milestone.expired? - Expired + Past Due - else Open %span.identifier From 83e1274145792fbfb09557a2a202843129edf483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Wed, 8 Jun 2016 12:05:23 -0400 Subject: [PATCH 419/507] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 0506854599..657c6f4833 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -49,6 +49,7 @@ v 8.9.0 (unreleased) - Replace Colorize with Rainbow for coloring console output in Rake tasks. - An indicator is now displayed at the top of the comment field for confidential issues. - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented + - Improve issuables APIs performance when accessing notes !4471 v 8.8.4 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds From a0adafddd0ce40c4ce9f052d2ee5e8ea38a2fb58 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 8 Jun 2016 18:18:49 +0200 Subject: [PATCH 420/507] Remove require_relative 'import_spec_helper' --- spec/controllers/import/bitbucket_controller_spec.rb | 1 - spec/controllers/import/fogbugz_controller_spec.rb | 1 - spec/controllers/import/github_controller_spec.rb | 1 - spec/controllers/import/gitlab_controller_spec.rb | 1 - spec/controllers/import/gitorious_controller_spec.rb | 1 - spec/controllers/import/google_code_controller_spec.rb | 1 - 6 files changed, 6 deletions(-) diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 81c03c9059..07bf8d2d1c 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::BitbucketController do include ImportSpecHelper diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb index 27b11267d2..5f0f6dea82 100644 --- a/spec/controllers/import/fogbugz_controller_spec.rb +++ b/spec/controllers/import/fogbugz_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::FogbugzController do include ImportSpecHelper diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index bcc713dce2..c55a3c2820 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::GithubController do include ImportSpecHelper diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 198d006af7..e8cf6aa776 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::GitlabController do include ImportSpecHelper diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb index 7cb1b85a46..4ae2b78e11 100644 --- a/spec/controllers/import/gitorious_controller_spec.rb +++ b/spec/controllers/import/gitorious_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::GitoriousController do include ImportSpecHelper diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb index 66088139a6..4241db6e77 100644 --- a/spec/controllers/import/google_code_controller_spec.rb +++ b/spec/controllers/import/google_code_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::GoogleCodeController do include ImportSpecHelper From 477c113c47717535365f7c22027234a20833a760 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 20:44:39 +0300 Subject: [PATCH 421/507] Improve buttons size and paddings in the lists Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/lists.scss | 10 ++++++++-- app/assets/stylesheets/framework/variables.scss | 4 ++-- app/views/projects/tags/_download.html.haml | 7 ++----- app/views/projects/tags/_tag.html.haml | 4 ++-- app/views/shared/groups/_group.html.haml | 6 +++--- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index dec327669e..b34ec16cdb 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -137,10 +137,16 @@ ul.content-list { padding-top: 1px; float: right; - .btn { - padding: $gl-vert-padding $gl-btn-padding; + > .btn, + > .btn-group { + margin-right: $gl-padding-top; + display: inline-block; margin-top: 4px; margin-bottom: 4px; + + &:last-child { + margin-right: 0; + } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d8ea07559a..9a85dedc09 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -79,8 +79,8 @@ $provider-btn-not-active-color: #4688f1; $link-underline-blue: #4a8bee; $layout-link-gray: #7e7c7c; $todo-alert-blue: #428bca; -$btn-side-margin: 7px; -$btn-sm-side-margin: 5px; +$btn-side-margin: 10px; +$btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; /* diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 093d1d1bb0..8a11dbfa9f 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -1,7 +1,6 @@ -%span.btn-group.btn-grouped +%span.btn-group = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do - %i.fa.fa-download - %span source code + %span Source code %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only @@ -9,9 +8,7 @@ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download %span Download zip %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download %span Download tar.gz diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index dbc35c16fe..844e105581 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -15,11 +15,11 @@ = render 'projects/tags/download', ref: tag.name, project: @project - if can?(current_user, :push_code, @project) - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has-tooltip', title: "Edit release notes" do + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes" do = icon("pencil") - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") - if commit diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 40c6eb9be4..a25365a94f 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -6,10 +6,10 @@ - if group_member .controls.hidden-xs - if can?(current_user, :admin_group, group) - = link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do - %i.fa.fa-cogs + = link_to edit_group_path(group), class: "btn" do + = icon('cogs') - = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do + = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn", title: 'Leave this group' do = icon('sign-out') .stats From f3637ed782dace291473f56967c26bde42dd9bf9 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 5 May 2016 12:00:40 -0500 Subject: [PATCH 422/507] Start adding SVG icons --- app/assets/stylesheets/framework/sidebar.scss | 8 ++++ app/helpers/appearances_helper.rb | 4 ++ app/views/layouts/nav/_dashboard.html.haml | 20 ++++++++ app/views/shared/icons/_activity.svg | 18 +++++++ .../shared/icons/_contributionanalytics.svg | 27 +++++++++++ app/views/shared/icons/_group.svg | 42 ++++++++++++++++ app/views/shared/icons/_issues.svg | 38 +++++++++++++++ app/views/shared/icons/_members.svg | 17 +++++++ app/views/shared/icons/_milestones.svg | 48 +++++++++++++++++++ app/views/shared/icons/_mr.svg | 47 ++++++++++++++++++ 10 files changed, 269 insertions(+) create mode 100644 app/views/shared/icons/_activity.svg create mode 100644 app/views/shared/icons/_contributionanalytics.svg create mode 100644 app/views/shared/icons/_group.svg create mode 100644 app/views/shared/icons/_issues.svg create mode 100644 app/views/shared/icons/_members.svg create mode 100644 app/views/shared/icons/_milestones.svg create mode 100644 app/views/shared/icons/_mr.svg diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 46d46368d2..fa9cddef0a 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -113,6 +113,14 @@ color: $gray-light; } + path { + fill: $gray-light; + } + + svg { + margin-right: 13px; + } + .nav-link-text { margin-top: 3px; font-size: 13px; diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index e0abc3a286..f240584ccb 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -30,4 +30,8 @@ module AppearancesHelper render 'shared/logo.svg' end end + + def navbar_icon(icon_name) + render "shared/icons/#{icon_name}.svg" + end end diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index df77d9cf83..3072981068 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -11,6 +11,7 @@ Todos = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do +<<<<<<< e6daf1f899b412ded9a16674865b09f31fc7c75a = icon('dashboard fw') .nav-link-text Activity @@ -28,6 +29,25 @@ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do = icon('exclamation-circle fw') .nav-link-text +======= + = navbar_icon('activity') + %span + Activity + = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do + = link_to dashboard_groups_path, title: 'Groups' do + = navbar_icon('group') + %span + Groups + = nav_link(controller: 'dashboard/milestones') do + = link_to dashboard_milestones_path, title: 'Milestones' do + = navbar_icon('milestones') + %span + Milestones + = nav_link(path: 'dashboard#issues') do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do + = navbar_icon('issues') + %span +>>>>>>> Start adding SVG icons Issues = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do diff --git a/app/views/shared/icons/_activity.svg b/app/views/shared/icons/_activity.svg new file mode 100644 index 0000000000..69e5afd3a4 --- /dev/null +++ b/app/views/shared/icons/_activity.svg @@ -0,0 +1,18 @@ + + + + Group 5 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_contributionanalytics.svg b/app/views/shared/icons/_contributionanalytics.svg new file mode 100644 index 0000000000..33edf0f932 --- /dev/null +++ b/app/views/shared/icons/_contributionanalytics.svg @@ -0,0 +1,27 @@ + + + + Pasted Image 234 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_group.svg b/app/views/shared/icons/_group.svg new file mode 100644 index 0000000000..6f2c530c9b --- /dev/null +++ b/app/views/shared/icons/_group.svg @@ -0,0 +1,42 @@ + + + + Pasted Image 232 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_issues.svg b/app/views/shared/icons/_issues.svg new file mode 100644 index 0000000000..92f48b897b --- /dev/null +++ b/app/views/shared/icons/_issues.svg @@ -0,0 +1,38 @@ + + + + Pasted Image 227 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_members.svg b/app/views/shared/icons/_members.svg new file mode 100644 index 0000000000..98635108d0 --- /dev/null +++ b/app/views/shared/icons/_members.svg @@ -0,0 +1,17 @@ + + + + Pasted Image 233 + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_milestones.svg b/app/views/shared/icons/_milestones.svg new file mode 100644 index 0000000000..f4f5b0c867 --- /dev/null +++ b/app/views/shared/icons/_milestones.svg @@ -0,0 +1,48 @@ + + + + Pasted Image 226 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_mr.svg b/app/views/shared/icons/_mr.svg new file mode 100644 index 0000000000..c14a5c2816 --- /dev/null +++ b/app/views/shared/icons/_mr.svg @@ -0,0 +1,47 @@ + + + + Pasted Image 228 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 5e9e9a1e8284a8d87780725f765a6c04ab100117 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 23 May 2016 18:35:18 -0500 Subject: [PATCH 423/507] Add SVG icons to side bar --- .../stylesheets/framework/gitlab-theme.scss | 27 ++++++++--- app/assets/stylesheets/framework/sidebar.scss | 4 -- .../shared/icons/_contributionanalytics.svg | 26 ++++------ app/views/shared/icons/_group.svg | 44 ++++------------- app/views/shared/icons/_issues.svg | 35 ++------------ app/views/shared/icons/_members.svg | 12 ++--- app/views/shared/icons/_milestones.svg | 47 +++---------------- app/views/shared/icons/_mr.svg | 44 ++--------------- 8 files changed, 60 insertions(+), 179 deletions(-) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index cd2eba59f9..2540ff497f 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -22,17 +22,17 @@ &:hover { background-color: $color-dark; a { - color: #fff; + color: $white-light; h3 { - color: #fff; + color: $white-light; } } } } .collapse-nav a { - color: #fff; + color: $white-light; background: $color; } @@ -45,7 +45,7 @@ &:hover { background-color: $color-dark; - color: #fff; + color: $white-light; text-decoration: none; } } @@ -63,10 +63,20 @@ color: $color-light; } + path, + polygon { + fill: $color-light; + } + .count { color: $color-light; background: $color-dark; } + + svg { + position: relative; + top: 3px; + } } &.separate-item { @@ -74,7 +84,7 @@ } &.active a { - color: #fff; + color: $white-light; background: $color-dark; &.no-highlight { @@ -82,7 +92,12 @@ } i { - color: #fff + color: $white-light + } + + path, + polygon { + fill: $white-light; } } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index fa9cddef0a..12a342bf0b 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -113,10 +113,6 @@ color: $gray-light; } - path { - fill: $gray-light; - } - svg { margin-right: 13px; } diff --git a/app/views/shared/icons/_contributionanalytics.svg b/app/views/shared/icons/_contributionanalytics.svg index 33edf0f932..adf09a1496 100644 --- a/app/views/shared/icons/_contributionanalytics.svg +++ b/app/views/shared/icons/_contributionanalytics.svg @@ -1,27 +1,17 @@ - Pasted Image 234 + Group Created with Sketch. - - - + - + - - - - - - - - - - - - - + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_group.svg b/app/views/shared/icons/_group.svg index 6f2c530c9b..75cae0d16c 100644 --- a/app/views/shared/icons/_group.svg +++ b/app/views/shared/icons/_group.svg @@ -1,42 +1,18 @@ - Pasted Image 232 + Group Created with Sketch. - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_issues.svg b/app/views/shared/icons/_issues.svg index 92f48b897b..2682c27ade 100644 --- a/app/views/shared/icons/_issues.svg +++ b/app/views/shared/icons/_issues.svg @@ -1,38 +1,13 @@ - Pasted Image 227 + Group Created with Sketch. - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - + + + \ No newline at end of file diff --git a/app/views/shared/icons/_members.svg b/app/views/shared/icons/_members.svg index 98635108d0..f8043b31fe 100644 --- a/app/views/shared/icons/_members.svg +++ b/app/views/shared/icons/_members.svg @@ -1,17 +1,13 @@ - Pasted Image 233 + Group Created with Sketch. - - - - - - - + + + \ No newline at end of file diff --git a/app/views/shared/icons/_milestones.svg b/app/views/shared/icons/_milestones.svg index f4f5b0c867..3d62ecc063 100644 --- a/app/views/shared/icons/_milestones.svg +++ b/app/views/shared/icons/_milestones.svg @@ -1,48 +1,15 @@ - Pasted Image 226 + Group Created with Sketch. - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_mr.svg b/app/views/shared/icons/_mr.svg index c14a5c2816..dd3dbcc447 100644 --- a/app/views/shared/icons/_mr.svg +++ b/app/views/shared/icons/_mr.svg @@ -1,47 +1,13 @@ - Pasted Image 228 + Group Created with Sketch. - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + \ No newline at end of file From 52080f74d42a62298654b8550e6dfbcd06de65a7 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 1 Jun 2016 16:54:04 -0600 Subject: [PATCH 424/507] Add new icons to group page layout nav --- app/assets/stylesheets/framework/nav.scss | 21 +++++++++++++++++++++ app/views/layouts/nav/_dashboard.html.haml | 12 +++--------- app/views/layouts/nav/_group.html.haml | 12 ++++++------ app/views/layouts/nav/_project.html.haml | 18 ++++++++++-------- app/views/shared/icons/_commits.svg | 10 ++++++++++ app/views/shared/icons/_files.svg | 17 +++++++++++++++++ app/views/shared/icons/_pipelines.svg | 10 ++++++++++ app/views/shared/icons/_project.svg | 10 ++++++++++ app/views/shared/icons/_wiki.svg | 10 ++++++++++ 9 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 app/views/shared/icons/_commits.svg create mode 100644 app/views/shared/icons/_files.svg create mode 100644 app/views/shared/icons/_pipelines.svg create mode 100644 app/views/shared/icons/_project.svg create mode 100644 app/views/shared/icons/_wiki.svg diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index a811778df7..0918f67360 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -304,6 +304,19 @@ border-bottom: none; height: 51px; + svg { + position: relative; + top: 2px; + margin-right: 2px; + height: 15px; + width: auto; + + path, + polygon { + fill: $layout-link-gray; + } + } + .fade-right { @include fade(left, rgba(250, 250, 250, 0.4), $background-color); right: 0; @@ -325,9 +338,17 @@ } &.active { + a, i { color: $black; } + + svg { + path, + polygon { + fill: $black; + } + } } .badge { diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 3072981068..2f956dbbd7 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,7 +1,7 @@ %ul.nav.nav-sidebar = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - = icon('bookmark fw') + = navbar_icon('project') .nav-link-text Projects = nav_link(controller: :todos) do @@ -11,7 +11,6 @@ Todos = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do -<<<<<<< e6daf1f899b412ded9a16674865b09f31fc7c75a = icon('dashboard fw') .nav-link-text Activity @@ -27,11 +26,8 @@ Milestones = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do - = icon('exclamation-circle fw') - .nav-link-text -======= = navbar_icon('activity') - %span + .nav-link-text Activity = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do @@ -46,12 +42,10 @@ = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do = navbar_icon('issues') - %span ->>>>>>> Start adding SVG icons Issues = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do - = icon('tasks fw') + = navbar_icon('mr') .nav-link-text Merge Requests = nav_link(controller: :snippets) do diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index de15add361..9cbee0aa36 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -5,36 +5,36 @@ .fade-left = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do - = icon('group fw') + = navbar_icon('group') %span Group = nav_link(path: 'groups#activity') do = link_to activity_group_path(@group), title: 'Activity' do - = icon('dashboard fw') + = navbar_icon('activity') %span Activity = nav_link(controller: [:group, :milestones]) do = link_to group_milestones_path(@group), title: 'Milestones' do - = icon('clock-o fw') + = navbar_icon('milestones') %span Milestones = nav_link(path: 'groups#issues') do = link_to issues_group_path(@group), title: 'Issues' do - = icon('exclamation-circle fw') + = navbar_icon('issues') %span Issues - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute %span.badge.count= number_with_delimiter(issues.count) = nav_link(path: 'groups#merge_requests') do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - = icon('tasks fw') + = navbar_icon('mr') %span Merge Requests - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute %span.badge.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do - = icon('users fw') + = navbar_icon('members') %span Members .fade-right diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 03c9fa0a94..2a58ef224b 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -24,17 +24,19 @@ .fade-left = nav_link(path: 'projects#show', html_options: {class: 'home'}) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - = icon('bookmark fw') + = navbar_icon('project') %span Project + = nav_link(path: 'projects#activity') do = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - = icon('dashboard fw') + = navbar_icon('activity') %span Activity + - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do - = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do + = link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do = icon('code fw') %span Code @@ -42,7 +44,7 @@ - if project_nav_tab? :pipelines = nav_link(controller: :pipelines) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - = icon('ship fw') + = navbar_icon('pipelines') %span Pipelines @@ -63,14 +65,14 @@ - if project_nav_tab? :milestones = nav_link(controller: :milestones) do = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - = icon('clock-o fw') + = navbar_icon('milestones') %span Milestones - if project_nav_tab? :issues = nav_link(controller: :issues) do = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do - = icon('exclamation-circle fw') + = navbar_icon('issues') %span Issues - if @project.default_issues_tracker? @@ -79,7 +81,7 @@ - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - = icon('tasks fw') + = navbar_icon('mr') %span Merge Requests %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) @@ -94,7 +96,7 @@ - if project_nav_tab? :wiki = nav_link(controller: :wikis) do = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - = icon('book fw') + = navbar_icon('wiki') %span Wiki diff --git a/app/views/shared/icons/_commits.svg b/app/views/shared/icons/_commits.svg new file mode 100644 index 0000000000..ba9bb89935 --- /dev/null +++ b/app/views/shared/icons/_commits.svg @@ -0,0 +1,10 @@ + + + + Pasted Image 240 + Created with Sketch. + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_files.svg b/app/views/shared/icons/_files.svg new file mode 100644 index 0000000000..fc378d81e4 --- /dev/null +++ b/app/views/shared/icons/_files.svg @@ -0,0 +1,17 @@ + + + + Pasted Image 237 + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_pipelines.svg b/app/views/shared/icons/_pipelines.svg new file mode 100644 index 0000000000..794e8a2702 --- /dev/null +++ b/app/views/shared/icons/_pipelines.svg @@ -0,0 +1,10 @@ + + + + Pasted Image 246 + Created with Sketch. + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_project.svg b/app/views/shared/icons/_project.svg new file mode 100644 index 0000000000..1e8b43f8c6 --- /dev/null +++ b/app/views/shared/icons/_project.svg @@ -0,0 +1,10 @@ + + + + Page 1 + Created with Sketch. + + + + + \ No newline at end of file diff --git a/app/views/shared/icons/_wiki.svg b/app/views/shared/icons/_wiki.svg new file mode 100644 index 0000000000..182d91e23a --- /dev/null +++ b/app/views/shared/icons/_wiki.svg @@ -0,0 +1,10 @@ + + + + Pasted Image 241 + Created with Sketch. + + + + + \ No newline at end of file From cdcca06b143c85e48196993a29ee73f746463dc9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 8 Jun 2016 21:01:53 +0300 Subject: [PATCH 425/507] Remove icons from button on wiki pages. Also consistent padding between buttons there Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/builds/index.html.haml | 1 - app/views/projects/milestones/index.html.haml | 1 - app/views/projects/milestones/show.html.haml | 2 -- app/views/projects/pipelines/index.html.haml | 2 -- app/views/projects/wikis/_main_links.html.haml | 6 ++---- app/views/projects/wikis/_nav.html.haml | 1 - 6 files changed, 2 insertions(+), 11 deletions(-) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 55d2ac89eb..181547316a 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -34,7 +34,6 @@ = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' = link_to ci_lint_path, class: 'btn btn-default' do - = icon('wrench') %span CI Lint %ul.content-list diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index e6133b22f9..60a5b83434 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -6,7 +6,6 @@ .nav-controls - if can?(current_user, :admin_milestone, @project) = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do - = icon('plus') New Milestone .milestones diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 19944e3e02..0b0e2bd686 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -23,11 +23,9 @@ = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do - = icon('pencil-square-o') Edit = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do - = icon('trash-o') Delete .detail-page-description.milestone-detail diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index a78450e09d..b70693eeb6 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -28,14 +28,12 @@ .nav-controls - if can? current_user, :create_pipeline, @project = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do - = icon('plus') New pipeline - unless @repository.gitlab_ci_yml = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' = link_to ci_lint_path, class: 'btn btn-default' do - = icon('wrench') %span CI Lint %ul.content-list.pipelines diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 2b91b7e8f6..4faa547769 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,11 +1,9 @@ - if (@page && @page.persisted?) - = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do + = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do Page History - if can?(current_user, :create_wiki, @project) - = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do - %i.fa.fa-pencil-square-o + = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do Edit - if can?(current_user, :admin_wiki, @project) = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do - = icon('trash') Delete diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index a722fbc535..988fe024e2 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -13,7 +13,6 @@ .nav-controls - if can?(current_user, :create_wiki, @project) = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - = icon('plus') New Page = render 'projects/wikis/new' From 7be19db42fd51da23b1ef658263897213b624500 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 8 Jun 2016 02:02:55 -0500 Subject: [PATCH 426/507] Set target="_blank" for external links --- lib/banzai/filter/external_link_filter.rb | 3 +++ spec/features/markdown_spec.rb | 10 ++++++++++ spec/fixtures/markdown.md.erb | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 38c4219518..27ca38e6d4 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -15,6 +15,9 @@ module Banzai next if link.start_with?(internal_url) node.set_attribute('rel', 'nofollow noreferrer') + + # Open external links on a new tab + node.set_attribute('target', '_blank') end doc diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 7663d19335..cabccdacf8 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -173,10 +173,20 @@ describe 'GitLab Markdown', feature: true do expect(link.attr('rel')).to include('noreferrer') end + it 'adds _blank to target attribute for external links' do + link = doc.at_css('a:contains("Google")') + expect(link.attr('target')).to match('_blank') + end + it 'ignores internal link' do link = doc.at_css('a:contains("GitLab Root")') expect(link.attr('rel')).not_to match 'nofollow' end + + it 'does not set _blank to target attribute for internal links' do + link = doc.at_css('a:contains("GitLab Root")') + expect(link.attr('target')).not_to match '_blank' + end end end diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 34ce7c4f03..c75d28d980 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -136,7 +136,7 @@ But it shouldn't autolink text inside certain tags: ### ExternalLinkFilter -External links get a `rel="nofollow"` attribute: +External links get a `rel="nofollow noreferrer"` and `target="_blank"` attributes: - [Google](https://google.com/) - [GitLab Root](<%= Gitlab.config.gitlab.url %>) From 064cff13fd7d02672695b4ac1722bbd6df35f1d4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 8 Jun 2016 12:14:56 -0500 Subject: [PATCH 427/507] Remove obvious comment and extra line --- lib/banzai/filter/external_link_filter.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 27ca38e6d4..f73ecfc941 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -15,8 +15,6 @@ module Banzai next if link.start_with?(internal_url) node.set_attribute('rel', 'nofollow noreferrer') - - # Open external links on a new tab node.set_attribute('target', '_blank') end From 27ada5aa4690affd04ada606d15a44a598184c57 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 8 Jun 2016 12:20:24 -0500 Subject: [PATCH 428/507] Combine tests for internal links --- spec/features/markdown_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index cabccdacf8..1193cae5a2 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -165,26 +165,26 @@ describe 'GitLab Markdown', feature: true do describe 'ExternalLinkFilter' do it 'adds nofollow to external link' do link = doc.at_css('a:contains("Google")') + expect(link.attr('rel')).to include('nofollow') end it 'adds noreferrer to external link' do link = doc.at_css('a:contains("Google")') + expect(link.attr('rel')).to include('noreferrer') end it 'adds _blank to target attribute for external links' do link = doc.at_css('a:contains("Google")') + expect(link.attr('target')).to match('_blank') end it 'ignores internal link' do link = doc.at_css('a:contains("GitLab Root")') - expect(link.attr('rel')).not_to match 'nofollow' - end - it 'does not set _blank to target attribute for internal links' do - link = doc.at_css('a:contains("GitLab Root")') + expect(link.attr('rel')).not_to match 'nofollow' expect(link.attr('target')).not_to match '_blank' end end From 26d4e633413d359c278262c9d9b2d3b94b0792e2 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 8 Jun 2016 13:02:32 -0500 Subject: [PATCH 429/507] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 657c6f4833..3ae881da36 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -50,6 +50,7 @@ v 8.9.0 (unreleased) - An indicator is now displayed at the top of the comment field for confidential issues. - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented - Improve issuables APIs performance when accessing notes !4471 + - External links now open in a new tab v 8.8.4 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds From 40b6d9064a2ab6c89cb54b62536fe2952c6cbca6 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 9 May 2016 16:12:53 +0100 Subject: [PATCH 430/507] Allow custom text on 'almost there' page Add a new application setting, after_sign_up_text. This is text to be rendered as Markdown and shown on the 'almost there' page after a user signs up, but before they've confirmed their account. Tweak the styles for that page so that centered lists look reasonable. --- CHANGELOG | 1 + app/assets/stylesheets/pages/confirmation.scss | 10 +++++++++- .../admin/application_settings_controller.rb | 1 + app/helpers/application_settings_helper.rb | 4 ++++ app/models/application_setting.rb | 5 ++++- app/views/admin/application_settings/_form.html.haml | 5 +++++ app/views/devise/confirmations/almost_there.haml | 3 +++ ...2_add_after_sign_up_text_to_application_settings.rb | 5 +++++ db/schema.rb | 3 ++- lib/api/entities.rb | 1 + lib/gitlab/current_settings.rb | 5 ++++- 11 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb diff --git a/CHANGELOG b/CHANGELOG index 657c6f4833..0593ce2308 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 8.9.0 (unreleased) - Make EmailsOnPushWorker use Sidekiq mailers queue - Fix wiki page events' webhook to point to the wiki repository - Fix issue todo not remove when leave project !4150 (Long Nguyen) + - Allow customisable text on the 'nearly there' page after a user signs up - Bump recaptcha gem to 3.0.0 to remove deprecated stoken support - Allow forking projects with restricted visibility level - Improve note validation to prevent errors when creating invalid note via API diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss index 125f495d6d..292225c526 100644 --- a/app/assets/stylesheets/pages/confirmation.scss +++ b/app/assets/stylesheets/pages/confirmation.scss @@ -2,13 +2,21 @@ margin-bottom: 20px; border-bottom: 1px solid #eee; - > h1 { + > h1, h2, h3, h4, h5, h6 { font-weight: 400; } .lead { margin-bottom: 20px; } + + ul, ol { + padding-left: 0; + } + + li { + list-style-type: none; + } } .confirmation-content { diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 0a34a12e2a..f4eda864aa 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -74,6 +74,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :two_factor_grace_period, :gravatar_enabled, :sign_in_text, + :after_sign_up_text, :help_page_text, :home_page_url, :after_sign_out_path, diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 03080d2593..55313fd835 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -15,6 +15,10 @@ module ApplicationSettingsHelper current_application_settings.sign_in_text end + def after_sign_up_text + current_application_settings.after_sign_up_text + end + def shared_runners_text current_application_settings.shared_runners_text end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 42f908aa34..a744f93791 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -113,7 +113,10 @@ class ApplicationSetting < ActiveRecord::Base signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], - sign_in_text: Settings.extra['sign_in_text'], + sign_in_text: nil, + after_sign_up_text: nil, + help_page_text: nil, + shared_runners_text: nil, restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], max_attachment_size: Settings.gitlab['max_attachment_size'], session_expire_delay: Settings.gitlab['session_expire_delay'], diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index f149f9eb43..c883e8f97d 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -154,6 +154,11 @@ .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 .help-block Markdown enabled + .form-group + = f.label :after_sign_up_text, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_area :after_sign_up_text, class: 'form-control', rows: 4 + .help-block Markdown enabled .form-group = f.label :help_page_text, class: 'control-label col-sm-2' .col-sm-10 diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index 3c3830a3f1..73c3a3dd2e 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -3,6 +3,9 @@ Almost there... %p.lead Please check your email to confirm your account +- if after_sign_up_text.present? + .well-confirmation.text-center + = markdown(after_sign_up_text) %p.confirmation-content.text-center No confirmation email received? Please check your spam folder or .append-bottom-20.prepend-top-20.text-center diff --git a/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb b/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb new file mode 100644 index 0000000000..89826fb96c --- /dev/null +++ b/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddAfterSignUpTextToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :after_sign_up_text, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 00829d63b6..b7adf48fdb 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: 20160603182247) do +ActiveRecord::Schema.define(version: 20160608155312) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -84,6 +84,7 @@ ActiveRecord::Schema.define(version: 20160603182247) do t.string "health_check_access_token" t.boolean "send_user_confirmation_email", default: false t.integer "container_registry_token_expire_delay", default: 5 + t.text "after_sign_up_text" end create_table "audit_events", force: :cascade do |t| diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 66c138eb90..50d69274b2 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -351,6 +351,7 @@ module API expose :signin_enabled expose :gravatar_enabled expose :sign_in_text + expose :after_sign_up_text expose :created_at expose :updated_at expose :home_page_url diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 92c7e8b9d8..5e7532f57a 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -26,7 +26,10 @@ module Gitlab signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], - sign_in_text: Settings.extra['sign_in_text'], + sign_in_text: nil, + after_sign_up_text: nil, + help_page_text: nil, + shared_runners_text: nil, restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], max_attachment_size: Settings.gitlab['max_attachment_size'], session_expire_delay: Settings.gitlab['session_expire_delay'], From 0cfa368bf8cae4f9a54186241b06ef588688cfb2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 8 Jun 2016 14:17:03 -0400 Subject: [PATCH 431/507] Fix version references in 8.8 to 8.9 update guide [ci skip] --- doc/update/8.8-to-8.9.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md index 67a986ead5..f14046bb4b 100644 --- a/doc/update/8.8-to-8.9.md +++ b/doc/update/8.8-to-8.9.md @@ -120,7 +120,7 @@ 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 +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-9-stable/lib/support/init.d/gitlab.default.example#L37 #### Init script @@ -145,7 +145,7 @@ To make sure you didn't miss anything run a more thorough check: If all items are green, then congratulations, the upgrade is complete! -## Things went south? Revert to previous version (8.7) +## Things went south? Revert to previous version (8.8) ### 1. Revert the code to the previous version From 59e47e67ea426df14dc0eb5c25073883a7d6d99c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 8 Jun 2016 13:50:44 -0500 Subject: [PATCH 432/507] Fix merge conflicts from removing links from sidebar --- app/assets/stylesheets/framework/sidebar.scss | 6 +----- app/views/layouts/nav/_dashboard.html.haml | 20 +++---------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 12a342bf0b..f438acfa4b 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -109,12 +109,8 @@ } i { - width: 16px; color: $gray-light; - } - - svg { - margin-right: 13px; + font-size: 16px; } .nav-link-text { diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 2f956dbbd7..b73fde7797 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -11,37 +11,23 @@ Todos = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - = icon('dashboard fw') - .nav-link-text - Activity - = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do - = link_to dashboard_groups_path, title: 'Groups' do - = icon('group fw') - .nav-link-text - Groups - = nav_link(controller: 'dashboard/milestones') do - = link_to dashboard_milestones_path, title: 'Milestones' do - = icon('clock-o fw') - .nav-link-text - Milestones - = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do = navbar_icon('activity') .nav-link-text Activity = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do = navbar_icon('group') - %span + .nav-link-text Groups = nav_link(controller: 'dashboard/milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do = navbar_icon('milestones') - %span + .nav-link-text Milestones = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do = navbar_icon('issues') + .nav-link-text Issues = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do From f367582ffe275dde4a0bd5ef281fd9e93e7586e5 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 8 Jun 2016 14:03:01 -0500 Subject: [PATCH 433/507] Use container helper instead of hard coded container --- app/views/groups/show.html.haml | 2 +- app/views/projects/_home_panel.html.haml | 2 +- app/views/projects/show.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 77c297255b..dbc3d1b07f 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -5,7 +5,7 @@ = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") .cover-block.groups-cover-block - .container-fluid.container-limited + %div{ class: (container_class) } = link_to group_icon(@group), target: '_blank' do = image_tag group_icon(@group), class: "avatar group-avatar s70" .group-info diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index f0e04a0235..f5bc1b4e40 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,6 +1,6 @@ - empty_repo = @project.empty_repo? .project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)} - .container-fluid.container-limited + %div{ class: (container_class) } .row .project-image-container = project_icon(@project, alt: '', class: 'project-avatar avatar s70') diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index a19c7c406a..4afa902b4e 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -13,7 +13,7 @@ = render "home_panel" .project-stats.row-content-block.second-block - .container-fluid.container-limited + %div{ class: (container_class) } %ul.nav %li = link_to project_files_path(@project) do From cba32b71bd27b747f5a667ec6ba11ff5e21362ef Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 8 Jun 2016 11:34:24 +0100 Subject: [PATCH 434/507] Allow clones from /namespace/project Redirect /namespace/project/info/refs to /namespace/project.git/info/refs, so `git clone` works on either namespace/project.git or namespace/project. --- CHANGELOG | 1 + config/routes.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 5136756079..419ea3d1b5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.9.0 (unreleased) - Redesign navigation for project pages - Fix groups API to list only user's accessible projects - Redesign account and email confirmation emails + - `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix - Bump nokogiri to 1.6.8 - Use gitlab-shell v3.0.0 - Use Knapsack to evenly distribute tests across multiple nodes diff --git a/config/routes.rb b/config/routes.rb index 240dcc74b0..f1a43cbbd2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -441,6 +441,23 @@ Rails.application.routes.draw do resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(? Date: Wed, 8 Jun 2016 18:36:43 +0200 Subject: [PATCH 435/507] Skip authenticity token checks for Git HTTP --- app/controllers/projects/git_http_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 380139a9c3..348d6cf4d9 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,6 +1,8 @@ class Projects::GitHttpController < Projects::ApplicationController attr_reader :user + # Git clients will not know what authenticity token to send along + skip_before_action :verify_authenticity_token skip_before_action :repository before_action :authenticate_user before_action :ensure_project_found! From 58456e26f5149eb60a401f7f400effe994c59360 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 8 Jun 2016 19:40:32 +0300 Subject: [PATCH 436/507] Fixes for MR comments. --- app/assets/javascripts/milestone_select.js.coffee | 4 ++-- app/helpers/milestones_helper.rb | 2 +- app/views/projects/milestones/show.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index df6ba3167d..648e1f3bde 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -25,7 +25,7 @@ class @MilestoneSelect if issueUpdateURL milestoneLinkTemplate = _.template( ' - + <%= _.escape(title) %> ' @@ -34,7 +34,7 @@ class @MilestoneSelect milestoneLinkNoneTemplate = '
                                        None
                                        ' collapsedSidebarLabelTemplate = _.template( - ' + ' <%= _.escape(title) %> ' ) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index e28ca13ec2..b3e6e468ec 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -56,7 +56,7 @@ module MilestonesHelper def milestone_remaining_days(milestone) if milestone.expired? - content_tag(:strong, 'Past Due') + content_tag(:strong, 'Past due') elsif milestone.due_date days = milestone.remaining_days content = content_tag(:strong, days) diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 58b1f8f664..fe3dde8b25 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -6,7 +6,7 @@ - if @milestone.closed? Closed - elsif @milestone.expired? - Past Due + Past due - else Open %span.identifier diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 8993261d22..d2b0d956d0 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -53,7 +53,7 @@ .value.bold.hide-collapsed - if issuable.milestone = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do - %span.has-tooltip{ "title" => milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1 } } + %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1}} = issuable.milestone.title - else .light None From a280116a058bb74f3fa29f40caf2ac962b662f49 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 8 Jun 2016 16:15:30 -0500 Subject: [PATCH 437/507] Fix activity SVG --- app/assets/stylesheets/framework/sidebar.scss | 1 - app/views/shared/icons/_activity.svg | 17 +++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index f438acfa4b..a1202374a0 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -109,7 +109,6 @@ } i { - color: $gray-light; font-size: 16px; } diff --git a/app/views/shared/icons/_activity.svg b/app/views/shared/icons/_activity.svg index 69e5afd3a4..c87794b906 100644 --- a/app/views/shared/icons/_activity.svg +++ b/app/views/shared/icons/_activity.svg @@ -1,18 +1,15 @@ - - Group 5 + + path-1 Created with Sketch. - + - - - - - - - + + + + \ No newline at end of file From 9282810fb7b6102657a0ddb2a02f71b6da22067f Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Wed, 8 Jun 2016 18:09:43 -0500 Subject: [PATCH 438/507] Syntax fixes and better logging around the `ldap_person` method. --- lib/gitlab/o_auth/user.rb | 9 +++++---- spec/lib/gitlab/saml/user_spec.rb | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 5e52979093..78f3ecb4cb 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -70,15 +70,16 @@ module Gitlab # If a corresponding person exists with same uid in a LDAP server, # check if the user already has a GitLab account. - if (user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)) + user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider) + if user # Case when a LDAP user already exists in Gitlab. Add the OAuth identity to existing account. - log.info "LDAP account found for user #{user.username}. Building new identity." + log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity." user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider) else - log.info 'No existing LDAP account was found in GitLab. Checking for OAuth account.' + log.info "No existing LDAP account was found in GitLab. Checking for #{auth_hash.provider} account." user = find_by_uid_and_provider if user.nil? - log.info 'No user found with the specified OAuth provider. Creating a new one.' + log.info "No user found using #{auth_hash.provider} provider. Creating a new one." user = build_new_user end log.info "Correct account has been found. Adding LDAP identity to user: #{user.username}." diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index 5957998e0f..84c21ceefd 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -184,9 +184,9 @@ describe Gitlab::Saml::User, lib: true do create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john') local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash) local_saml_user = described_class.new(local_hash) - local_saml_user.save local_gl_user = local_saml_user.gl_user + expect(local_gl_user).to be_valid expect(local_gl_user.identities.length).to eql 2 identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } @@ -194,7 +194,6 @@ describe Gitlab::Saml::User, lib: true do { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' } ]) end - end end end From bdc995bf00794f96af46d92f61a940896c1404d0 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 8 Jun 2016 17:09:48 -0600 Subject: [PATCH 439/507] Shows award emoji for comments to all users who are logged in. --- app/views/projects/notes/_note.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 5ddd0ecc4c..bcdbff0801 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -20,10 +20,11 @@ - access = note.project.team.human_max_access(note.author.id) - if access %span.note-role.hidden-xs= access - - if note_editable + - if current_user = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do = icon('spinner spin') = icon('smile-o') + - if note_editable = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do From 091e0300708e72049d4ee27b995d0f17c360d167 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 9 Jun 2016 02:56:07 +0300 Subject: [PATCH 440/507] Instantiate awardsHandler in application main script. --- app/assets/javascripts/application.js.coffee | 1 + app/assets/javascripts/dispatcher.js.coffee | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index ebf425550e..c300370f40 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -268,5 +268,6 @@ $ -> .on "resize", (e) -> fitSidebarForSize() + gl.awardsHandler = new AwardsHandler() checkInitialSidebarSize() new Aside() diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 5d6ac6e757..29ac0f70b3 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -23,7 +23,6 @@ class Dispatcher new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() - gl.awardsHandler = new AwardsHandler() when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() when 'dashboard:todos:index' @@ -54,7 +53,6 @@ class Dispatcher new Diff() shortcut_handler = new ShortcutsIssuable(true) new ZenMode() - gl.awardsHandler = new AwardsHandler() when "projects:merge_requests:diffs" new Diff() new ZenMode() From 52525dc4c28750a61697375900830589bb79eaa9 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 9 Jun 2016 03:02:49 +0300 Subject: [PATCH 441/507] Render frequent emoji block once. --- app/assets/javascripts/awards_handler.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index efa8f6cd01..0e5dcdc464 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -65,7 +65,7 @@ class @AwardsHandler $addBtn.removeClass 'is-loading' $menu = $('.emoji-menu') @positionMenu($menu, $addBtn) - @renderFrequentlyUsedBlock() + @renderFrequentlyUsedBlock() unless @frequentEmojiBlockRendered setTimeout => $menu.addClass 'is-visible' @@ -343,6 +343,8 @@ class @AwardsHandler $('input.emoji-search').after(ul).after($('
                                        ').text('Frequently used')) + @frequentEmojiBlockRendered = yes + setupSearch: -> From 92af60c2449cae5cbe2a8ef8ea65e973017053c2 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 9 Jun 2016 03:27:52 +0300 Subject: [PATCH 442/507] Move award_menu_url variable into gon object. --- app/assets/javascripts/awards_handler.coffee | 2 +- app/views/award_emoji/_awards_block.html.haml | 3 --- lib/gitlab/gon_helper.rb | 1 + spec/javascripts/awards_handler_spec.js.coffee | 13 ++++++------- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 0e5dcdc464..58fd8f0590 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -282,7 +282,7 @@ class @AwardsHandler @createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji - getAwardMenuUrl: -> return gl.awardMenuUrl + getAwardMenuUrl: -> return gon.award_menu_url resolveNameToCssClass: (emoji) -> diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 84fd146a26..02efcecc88 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -7,9 +7,6 @@ = awards.count - if current_user - :javascript - gl.awardMenuUrl = "#{emojis_path}" - .award-menu-holder.js-award-holder %button.btn.award-control.js-add-award{ type: "button" } = icon('smile-o', class: "award-control-icon award-control-icon-normal") diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index ab900b641c..f751a3a12f 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -8,6 +8,7 @@ module Gitlab gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.shortcuts_path = help_shortcuts_path gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class + gon.award_menu_url = emojis_path if current_user gon.current_user_id = current_user.id diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee index 0bd6d69638..ba191199dc 100644 --- a/spec/javascripts/awards_handler_spec.js.coffee +++ b/spec/javascripts/awards_handler_spec.js.coffee @@ -3,10 +3,11 @@ #= require jquery.cookie #= require ./fixtures/emoji_menu -awardsHandler = null -window.gl or= {} -gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } -gl.awardMenuUrl = '/emojis' +awardsHandler = null +window.gl or= {} +window.gon or= {} +gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } +gon.award_menu_url = '/emojis' lazyAssert = (done, assertFn) -> @@ -25,9 +26,7 @@ describe 'AwardsHandler', -> fixture.load 'awards_handler.html' awardsHandler = new AwardsHandler spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb() - spyOn(jQuery, 'get').and.callFake (req, cb) -> - expect(req).toBe '/emojis' - cb window.emojiMenu + spyOn(jQuery, 'get').and.callFake (req, cb) -> cb window.emojiMenu describe '::showEmojiMenu', -> From 8e71c19a6940b8d82c70ee9b2550b62b5169eb54 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 8 Jun 2016 10:29:20 +0530 Subject: [PATCH 443/507] Implement the correct linking behaviour in `WikiLinkFilter`. Original Comments ================= - Linking behaves as per rules documented here: https://gitlab.com/gitlab-org/gitlab-ce/blob/16568-document-wiki-linking-behavior/doc/markdown/wiki.md - All links (to other wiki pages) are rewritten to be at the level of the app root. We can't use links relative to the current page ('./foo', 'foo', '../foo'), because they won't work in the markdown preview, where the current page is suffixed with `/edit` - Move existing `WikiLinkFilter` specs to `WikiPipeline` spec. It makes sense to run these tests on the combined output of the pipeline, rather than a single filter, since we can catch issues with conflicting filters. - Add more tests to cover the new linking @rymai's Review =============== - Classes nested under `WikiLinkFilter` should declare `WikiLinkFilter`'s inherit, so nothing changes if the nested class is loaded first. - Add a blank line after a guard clause - Use keyword arguments for the `Rewriter` constructor - Invert a condition - use `if` instead of `unless` - Inline a `let` in `WikiPipeline` spec - it was only used in a single place - Change out of date spec names - Add a comment for every rewrite rule in `Rewriter` --- lib/banzai/filter/wiki_link_filter.rb | 32 +----- .../filter/wiki_link_filter/rewriter.rb | 40 +++++++ spec/factories/wiki_pages.rb | 2 +- .../banzai/filter/wiki_link_filter_spec.rb | 85 -------------- .../lib/banzai/pipeline/wiki_pipeline_spec.rb | 108 ++++++++++++++++++ 5 files changed, 155 insertions(+), 112 deletions(-) create mode 100644 lib/banzai/filter/wiki_link_filter/rewriter.rb delete 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 7dc771afd7..37a2779d45 100644 --- a/lib/banzai/filter/wiki_link_filter.rb +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -2,7 +2,8 @@ require 'uri' module Banzai module Filter - # HTML filter that "fixes" relative links to files in a repository. + # HTML filter that "fixes" links to pages/files in a wiki. + # Rewrite rules are documented in the `WikiPipeline` spec. # # Context options: # :project_wiki @@ -25,36 +26,15 @@ module Banzai end def process_link_attr(html_attr) - return if html_attr.blank? || file_reference?(html_attr) || hierarchical_link?(html_attr) + return if html_attr.blank? - uri = URI(html_attr.value) - if uri.relative? && uri.path.present? - html_attr.value = rebuild_wiki_uri(uri).to_s - end + html_attr.value = apply_rewrite_rules(html_attr.value) rescue URI::Error # noop end - def rebuild_wiki_uri(uri) - uri.path = ::File.join(project_wiki_base_path, uri.path) - uri - end - - def project_wiki - context[:project_wiki] - end - - def file_reference?(html_attr) - !File.extname(html_attr.value).blank? - end - - # Of the form `./link`, `../link`, or similar - def hierarchical_link?(html_attr) - html_attr.value[0] == '.' - end - - def project_wiki_base_path - project_wiki && project_wiki.wiki_base_path + def apply_rewrite_rules(link_string) + Rewriter.new(link_string, wiki: context[:project_wiki], slug: context[:page_slug]).apply_rules end end end diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb new file mode 100644 index 0000000000..2e2c8da311 --- /dev/null +++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb @@ -0,0 +1,40 @@ +module Banzai + module Filter + class WikiLinkFilter < HTML::Pipeline::Filter + class Rewriter + def initialize(link_string, wiki:, slug:) + @uri = Addressable::URI.parse(link_string) + @wiki_base_path = wiki && wiki.wiki_base_path + @slug = slug + end + + def apply_rules + apply_file_link_rules! + apply_hierarchical_link_rules! + apply_relative_link_rules! + @uri.to_s + end + + private + + # Of the form 'file.md' + def apply_file_link_rules! + @uri = Addressable::URI.join(@slug, @uri) if @uri.extname.present? + end + + # Of the form `./link`, `../link`, or similar + def apply_hierarchical_link_rules! + @uri = Addressable::URI.join(@slug, @uri) if @uri.to_s[0] == '.' + end + + # Any link _not_ of the form `http://example.com/` + def apply_relative_link_rules! + if @uri.relative? && @uri.path.present? + link = ::File.join(@wiki_base_path, @uri.path) + @uri = Addressable::URI.parse(link) + end + end + end + end + end +end diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index 938ccf2306..efa6cbe5bb 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -2,7 +2,7 @@ require 'ostruct' FactoryGirl.define do factory :wiki_page do - page = OpenStruct.new(url_path: 'some-name') + page { OpenStruct.new(url_path: 'some-name') } association :wiki, factory: :project_wiki, strategy: :build initialize_with { new(wiki, page, true) } end diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb deleted file mode 100644 index 185abbb210..0000000000 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::WikiLinkFilter, lib: true do - include FilterSpecHelper - - 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) } - - 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 diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 7aa1b4a3bf..ea4ab2c852 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -50,4 +50,112 @@ describe Banzai::Pipeline::WikiPipeline do end end end + + describe "Links" do + let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") } + let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } + let(:project_wiki) { ProjectWiki.new(project, double(:user)) } + let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } + + { "when GitLab is hosted at a root URL" => '/', + "when GitLab is hosted at a relative URL" => '/nested/relative/gitlab' }.each do |test_name, relative_url_root| + + context test_name do + before do + allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return(relative_url_root) + end + + describe "linking to pages within the wiki" do + context "when creating hierarchical links to the current directory" do + it "rewrites non-file links to be at the scope of the current directory" do + markdown = "[Page](./page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page\"") + end + + it "rewrites file links to be at the scope of the current directory" do + markdown = "[Link to Page](./page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"") + end + end + + context "when creating hierarchical links to the parent directory" do + it "rewrites non-file links to be at the scope of the parent directory" do + markdown = "[Link to Page](../page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page\"") + end + + it "rewrites file links to be at the scope of the parent directory" do + markdown = "[Link to Page](../page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page.md\"") + end + end + + context "when creating hierarchical links to a sub-directory" do + it "rewrites non-file links to be at the scope of the sub-directory" do + markdown = "[Link to Page](./subdirectory/page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page\"") + end + + it "rewrites file links to be at the scope of the sub-directory" do + markdown = "[Link to Page](./subdirectory/page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page.md\"") + end + end + + describe "when creating non-hierarchical links" do + it 'rewrites non-file links to be at the scope of the wiki root' do + markdown = "[Link to Page](page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"") + end + + it "rewrites file links to be at the scope of the current directory" do + markdown = "[Link to Page](page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"") + end + end + + describe "when creating root links" do + it 'rewrites non-file links to be at the scope of the wiki root' do + markdown = "[Link to Page](/page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"") + end + + it 'rewrites file links to be at the scope of the wiki root' do + markdown = "[Link to Page](/page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page.md\"") + end + end + end + + describe "linking to pages outside the wiki (absolute)" do + it "doesn't rewrite links" do + markdown = "[Link to Page](http://example.com/page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include('href="http://example.com/page"') + end + end + end + end + end end From e6b1d1669b362ad4cea27ac44e89e73f4d6e92fd Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 8 Jun 2016 10:32:50 +0530 Subject: [PATCH 444/507] Hook up the updated `WikiLinkFilter` to the wiki controllers. - Need to pass in a `page_slug` to the filter, so it can rewrite based on the current page (all links are rewritten to the level of the app root). - The earlier `markdown_preview` endpoint was at the level of the wiki. We need to know the current page (for rewriting, as above), so this commit moves the endpoint to the level of a wiki page. - Fix all tests --- app/controllers/projects/wikis_controller.rb | 2 +- app/helpers/gitlab_markdown_helper.rb | 2 +- app/views/layouts/project.html.haml | 4 ++-- config/routes.rb | 2 +- features/steps/project/wiki.rb | 4 ++-- spec/features/markdown_spec.rb | 3 ++- spec/helpers/gitlab_markdown_helper_spec.rb | 3 ++- spec/support/markdown_feature.rb | 4 ++++ 8 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 4b404eb03f..2aa6bed072 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -95,7 +95,7 @@ class Projects::WikisController < Projects::ApplicationController ext.analyze(text, author: current_user) render json: { - body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki), + body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]), references: { users: ext.users.map(&:username) } diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 0a1b48af21..067a00660a 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -108,7 +108,7 @@ module GitlabMarkdownHelper def render_wiki_content(wiki_page) case wiki_page.format when :markdown - markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki) + markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug) when :asciidoc asciidoc(wiki_page.content) else diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 20d6cdf724..2049b20495 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -5,8 +5,8 @@ - content_for :scripts_body_top do - project = @target_project || @project - - if @project_wiki - - markdown_preview_path = namespace_project_wikis_markdown_preview_path(project.namespace, project) + - if @project_wiki && @page + - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, params[:id]) - else - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project) - if current_user diff --git a/config/routes.rb b/config/routes.rb index 428302d0fd..34126bbe91 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -591,7 +591,6 @@ Rails.application.routes.draw do # Order matters to give priority to these matches get '/wikis/git_access', to: 'wikis#git_access' get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' - post '/wikis/markdown_preview', to:'wikis#markdown_preview' post '/wikis', to: 'wikis#create' get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID @@ -600,6 +599,7 @@ Rails.application.routes.draw do get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID + post '/wikis/*id/markdown_preview', to:'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview' end resource :repository, only: [:show, :create] do diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 9f6aed1c5b..3cbf832c72 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -97,7 +97,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps file = Gollum::File.new(wiki.wiki) Gollum::Wiki.any_instance.stub(:file).with("image.jpg", "master", true).and_return(file) Gollum::File.any_instance.stub(:mime_type).and_return("image/jpeg") - expect(page).to have_link('image', href: "image.jpg") + expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg") click_on "image" end @@ -113,7 +113,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I click on image link' do - expect(page).to have_link('image', href: "image.jpg") + expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg") click_on "image" end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 1d892fe1a5..07275bfb3d 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -231,13 +231,14 @@ describe 'GitLab Markdown', feature: true do context 'wiki pipeline' do before do @project_wiki = @feat.project_wiki + @project_wiki_page = @feat.project_wiki_page file = Gollum::File.new(@project_wiki.wiki) expect(file).to receive(:path).and_return('images/example.jpg') expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file) allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' } - @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki }) + @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki, page_slug: @project_wiki_page.slug }) end it_behaves_like 'all pipelines' diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 13de88e2f2..ade5c3b02d 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -121,13 +121,14 @@ describe GitlabMarkdownHelper do before do @wiki = double('WikiPage') allow(@wiki).to receive(:content).and_return('wiki content') + allow(@wiki).to receive(:slug).and_return('nested/page') helper.instance_variable_set(:@project_wiki, @wiki) end it "should use Wiki pipeline for markdown files" do allow(@wiki).to receive(:format).and_return(:markdown) - expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki) + expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page") helper.render_wiki_content(@wiki) end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index 7fc6d6fcc5..a79386b5db 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -32,6 +32,10 @@ class MarkdownFeature @project_wiki ||= ProjectWiki.new(project, user) end + def project_wiki_page + @project_wiki_page ||= build(:wiki_page, wiki: project_wiki) + end + def issue @issue ||= create(:issue, project: project) end From 19b91e749a6320d12fb299d33f1f6440777e0e26 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 9 Jun 2016 10:16:46 +0530 Subject: [PATCH 445/507] Add #18019 to the CHANGELOG. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d1cde40c1c..1e51bbff4e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 8.9.0 (unreleased) - Add DB index on users.state - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Changed the Slack build message to use the singular duration if necessary (Aran Koning) + - Links from a wiki page to other wiki pages should be rewritten as expected - Fix issues filter when ordering by milestone - Todos will display target state if issuable target is 'Closed' or 'Merged' - Fix bug when sorting issues by milestone due date and filtering by two or more labels From df5fb28a3a7f3bae496805716211eb47936ecc81 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 9 Jun 2016 11:53:11 +0100 Subject: [PATCH 446/507] Ensure only IDs ending in .git perform git actions It doesn't seem possible to set constraints based on format for project IDs ending in .git, so set the constraint on the ID and ensure the format is nil to avoid the case where the project ID is something like project.git.foo. --- config/routes.rb | 34 ++++++++++----------- spec/requests/git_http_spec.rb | 55 +++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 417289829d..4d12254963 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -442,22 +442,6 @@ Rails.application.routes.draw do resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(? Date: Thu, 9 Jun 2016 14:26:52 +0100 Subject: [PATCH 447/507] Add test for getting info/refs from repo --- spec/requests/git_http_spec.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index df6a6d5da7..c44a4a7a1f 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -321,6 +321,34 @@ describe 'Git HTTP requests', lib: true do end end + context "retrieving an info/refs file" do + before { project.update_attribute(:visibility_level, Project::PUBLIC) } + + context "when the file exists" do + before do + # Provide a dummy file in its place + allow_any_instance_of(Repository).to receive(:blob_at).and_call_original + allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do + Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore') + end + + get "/#{project.path_with_namespace}/blob/master/info/refs" + end + + it "returns the file" do + expect(response.status).to eq(200) + end + end + + context "when the file exists" do + before { get "/#{project.path_with_namespace}/blob/master/info/refs" } + + it "returns not found" do + expect(response.status).to eq(404) + end + end + end + def clone_get(project, options={}) get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password)) end From 98bb435f4266719b1e0fca57472a0f4e50d30371 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 9 Jun 2016 13:39:16 +0100 Subject: [PATCH 448/507] Enable RuboCop for migrations Migrations shouldn't fail RuboCop checks - especially lint checks, such as the nested method check. To avoid changing code in existing migrations, add the magic comment to the top of each of them to skip that file. --- .rubocop.yml | 3 ++- db/migrate/20121220064453_init_schema.rb | 1 + .../20130102143055_rename_owner_to_creator_for_project.rb | 1 + db/migrate/20130110172407_add_public_to_project.rb | 1 + db/migrate/20130123114545_add_issues_tracker_to_project.rb | 1 + db/migrate/20130125090214_add_user_permissions.rb | 1 + db/migrate/20130131070232_remove_private_flag_from_project.rb | 1 + db/migrate/20130206084024_add_description_to_namsespace.rb | 1 + db/migrate/20130207104426_add_description_to_teams.rb | 1 + db/migrate/20130211085435_add_issues_tracker_id_to_project.rb | 1 + ...20130214154045_rename_state_to_merge_status_in_milestone.rb | 1 + db/migrate/20130218140952_add_state_to_issue.rb | 1 + db/migrate/20130218141038_add_state_to_merge_request.rb | 1 + db/migrate/20130218141117_add_state_to_milestone.rb | 1 + db/migrate/20130218141258_convert_closed_to_state_in_issue.rb | 1 + .../20130218141327_convert_closed_to_state_in_merge_request.rb | 1 + .../20130218141344_convert_closed_to_state_in_milestone.rb | 1 + db/migrate/20130218141444_remove_merged_from_merge_request.rb | 1 + db/migrate/20130218141507_remove_closed_from_issue.rb | 1 + db/migrate/20130218141536_remove_closed_from_merge_request.rb | 1 + db/migrate/20130218141554_remove_closed_from_milestone.rb | 1 + .../20130220124204_add_new_merge_status_to_merge_request.rb | 1 + .../20130220125544_convert_merge_status_in_merge_request.rb | 1 + .../20130220125545_remove_merge_status_from_merge_request.rb | 1 + ...245_rename_new_merge_status_to_merge_status_in_milestone.rb | 1 + db/migrate/20130304104623_add_state_to_user.rb | 1 + db/migrate/20130304104740_convert_blocked_to_state.rb | 1 + db/migrate/20130304105317_remove_blocked_from_user.rb | 1 + db/migrate/20130315124931_user_color_scheme.rb | 1 + db/migrate/20130318212250_add_snippets_to_features.rb | 1 + db/migrate/20130319214458_create_forked_project_links.rb | 1 + db/migrate/20130323174317_add_private_to_snippets.rb | 1 + db/migrate/20130324151736_add_type_to_snippets.rb | 1 + .../20130324172327_change_project_id_to_null_in_snipepts.rb | 1 + db/migrate/20130324203535_add_type_value_for_snippets.rb | 1 + db/migrate/20130325173941_add_notification_level_to_user.rb | 1 + .../20130326142630_add_index_to_users_authentication_token.rb | 1 + .../20130403003950_add_last_activity_column_into_project.rb | 1 + .../20130404164628_add_notification_level_to_user_project.rb | 1 + db/migrate/20130410175022_remove_wiki_table.rb | 1 + db/migrate/20130419190306_allow_merges_for_forks.rb | 1 + db/migrate/20130506085413_add_type_to_key.rb | 1 + db/migrate/20130506090604_create_deploy_keys_projects.rb | 1 + db/migrate/20130506095501_remove_project_id_from_key.rb | 1 + db/migrate/20130522141856_add_more_fields_to_service.rb | 1 + db/migrate/20130528184641_add_system_to_notes.rb | 1 + db/migrate/20130611210815_increase_snippet_text_column_size.rb | 1 + db/migrate/20130613165816_add_password_expires_at_to_users.rb | 1 + db/migrate/20130613173246_add_created_by_id_to_user.rb | 1 + db/migrate/20130614132337_add_improted_to_project.rb | 1 + db/migrate/20130617095603_create_users_groups.rb | 1 + .../20130621195223_add_notification_level_to_user_group.rb | 1 + db/migrate/20130622115340_add_more_db_index.rb | 1 + db/migrate/20130624162710_add_fingerprint_to_key.rb | 1 + db/migrate/20130711063759_create_project_group_links.rb | 1 + db/migrate/20130804151314_add_st_diff_to_note.rb | 1 + db/migrate/20130809124851_add_permission_check_to_user.rb | 1 + db/migrate/20130812143708_add_import_url_to_project.rb | 1 + db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb | 1 + db/migrate/20130820102832_add_access_to_project_group_link.rb | 1 + db/migrate/20130821090530_remove_deprecated_tables.rb | 1 + db/migrate/20130821090531_add_internal_ids_to_milestones.rb | 1 + db/migrate/20130909132950_add_description_to_merge_request.rb | 1 + db/migrate/20130926081215_change_owner_id_for_group.rb | 1 + db/migrate/20131005191208_add_avatar_to_users.rb | 1 + db/migrate/20131009115346_add_confirmable_to_users.rb | 1 + db/migrate/20131106151520_remove_default_branch.rb | 1 + db/migrate/20131112114325_create_broadcast_messages.rb | 1 + db/migrate/20131112220935_add_visibility_level_to_projects.rb | 1 + db/migrate/20131129154016_add_archived_to_projects.rb | 1 + .../20131130165425_add_color_and_font_to_broadcast_messages.rb | 1 + db/migrate/20131202192556_add_event_fields_for_web_hook.rb | 1 + db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb | 1 + db/migrate/20131217102743_add_recipients_to_service.rb | 1 + db/migrate/20140116231608_add_website_url_to_users.rb | 1 + db/migrate/20140122112253_create_merge_request_diffs.rb | 1 + db/migrate/20140122114406_migrate_mr_diffs.rb | 1 + db/migrate/20140122122549_remove_m_rdiff_fields.rb | 1 + db/migrate/20140125162722_add_avatar_to_projects.rb | 1 + db/migrate/20140127170938_add_group_avatars.rb | 1 + db/migrate/20140209025651_create_emails.rb | 1 + db/migrate/20140214102325_add_api_key_to_services.rb | 1 + ...005354_add_index_merge_request_diffs_on_merge_request_id.rb | 1 + .../20140305193308_add_tag_push_hooks_to_project_hook.rb | 1 + db/migrate/20140312145357_add_import_status_to_project.rb | 1 + db/migrate/20140313092127_migrate_already_imported_projects.rb | 1 + db/migrate/20140407135544_fix_namespaces.rb | 1 + ...14131055_change_state_to_allow_empty_merge_request_diffs.rb | 1 + db/migrate/20140415124820_limits_to_mysql.rb | 1 + db/migrate/20140416074002_add_index_on_iid.rb | 1 + db/migrate/20140416185734_index_on_current_sign_in_at.rb | 1 + db/migrate/20140428105831_add_notes_index_updated_at.rb | 1 + db/migrate/20140502115131_add_repo_size_to_db.rb | 1 + db/migrate/20140502125220_migrate_repo_size.rb | 1 + db/migrate/20140611135229_add_position_to_merge_request.rb | 1 + db/migrate/20140625115202_create_users_star_projects.rb | 1 + db/migrate/20140729134820_create_labels.rb | 1 + db/migrate/20140729140420_create_label_links.rb | 1 + db/migrate/20140729145339_migrate_project_tags.rb | 1 + db/migrate/20140729152420_migrate_taggable_labels.rb | 1 + db/migrate/20140730111702_add_index_to_labels.rb | 1 + db/migrate/20140903115954_migrate_to_new_shell.rb | 1 + db/migrate/20140907220153_serialize_service_properties.rb | 1 + db/migrate/20140914113604_add_members_table.rb | 1 + db/migrate/20140914145549_migrate_to_new_members_model.rb | 1 + db/migrate/20140914173417_remove_old_member_tables.rb | 1 + db/migrate/20141006143943_move_slack_service_to_webhook.rb | 1 + db/migrate/20141007100818_add_visibility_level_to_snippet.rb | 1 + db/migrate/20141118150935_add_audit_event.rb | 1 + db/migrate/20141121133009_add_timestamps_to_members.rb | 1 + db/migrate/20141121161704_add_identity_table.rb | 1 + db/migrate/20141205134006_add_locked_at_to_merge_request.rb | 1 + db/migrate/20141216155758_create_doorkeeper_tables.rb | 1 + db/migrate/20141217125223_add_owner_to_application.rb | 1 + db/migrate/20141223135007_add_import_data_to_project_table.rb | 1 + ...1226080412_add_developers_can_push_to_protected_branches.rb | 1 + db/migrate/20150108073740_create_application_settings.rb | 1 + ...0150116234544_add_home_page_url_for_application_settings.rb | 1 + db/migrate/20150116234545_add_gitlab_access_token_to_user.rb | 1 + .../20150125163100_add_default_branch_protection_setting.rb | 1 + db/migrate/20150205211843_add_timestamps_to_identities.rb | 1 + db/migrate/20150206181414_add_index_to_created_at.rb | 1 + db/migrate/20150206222854_add_notification_email_to_user.rb | 1 + db/migrate/20150209222013_add_missing_index.rb | 1 + db/migrate/20150211172122_add_template_to_service.rb | 1 + db/migrate/20150211174341_allow_null_in_services_project_id.rb | 1 + ...4043_add_twitter_sharing_enabled_to_application_settings.rb | 1 + db/migrate/20150213114800_add_hide_no_password_to_user.rb | 1 + .../20150213121042_add_password_automatically_set_to_user.rb | 1 + ...0217123345_add_bitbucket_access_token_and_secret_to_user.rb | 1 + db/migrate/20150219004514_add_events_to_services.rb | 1 + db/migrate/20150223022001_set_missing_last_activity_at.rb | 1 + db/migrate/20150225065047_add_note_events_to_services.rb | 1 + ...add_restricted_visibility_levels_to_application_settings.rb | 1 + db/migrate/20150306023106_fix_namespace_duplication.rb | 1 + db/migrate/20150306023112_add_unique_index_to_namespace.rb | 1 + ...20150310194358_add_version_check_to_application_settings.rb | 1 + db/migrate/20150313012111_create_subscriptions_table.rb | 1 + db/migrate/20150320234437_add_location_to_user.rb | 1 + db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb | 1 + db/migrate/20150327122227_add_public_to_key.rb | 1 + db/migrate/20150327150017_add_import_data_to_project.rb | 1 + db/migrate/20150327223628_add_devise_two_factor_to_users.rb | 1 + ...28132231_add_max_attachment_size_to_application_settings.rb | 1 + ...20150331183602_add_devise_two_factor_backupable_to_users.rb | 1 + db/migrate/20150406133311_add_invite_data_to_member.rb | 1 + db/migrate/20150411000035_fix_identities.rb | 1 + db/migrate/20150411180045_rename_buildbox_service.rb | 1 + db/migrate/20150413192223_add_public_email_to_users.rb | 1 + db/migrate/20150417121913_create_project_import_data.rb | 1 + db/migrate/20150417122318_remove_import_data_from_project.rb | 1 + .../20150421120000_remove_periods_at_ends_of_usernames.rb | 1 + ..._add_default_project_visibililty_to_application_settings.rb | 1 + ...hange_collation_for_tag_names.acts_as_taggable_on_engine.rb | 1 + db/migrate/20150425164647_remove_duplicate_tags.rb | 1 + ...48_add_missing_unique_indices.acts_as_taggable_on_engine.rb | 1 + ...aggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb | 1 + ...50_add_missing_taggable_index.acts_as_taggable_on_engine.rb | 1 + ...hange_collation_for_tag_names.acts_as_taggable_on_engine.rb | 1 + ...425173433_add_default_snippet_visibility_to_app_settings.rb | 1 + .../20150429002313_remove_abandoned_group_members_records.rb | 1 + ...22_add_restricted_signup_domains_to_application_settings.rb | 1 + db/migrate/20150509180749_convert_legacy_reference_notes.rb | 1 + db/migrate/20150516060434_add_note_events_to_web_hooks.rb | 1 + ...1607_add_user_oauth_applications_to_application_settings.rb | 1 + ...9150354_add_after_sign_out_path_for_application_settings.rb | 1 + ...141121_add_session_expire_delay_for_application_settings.rb | 1 + db/migrate/20150610065936_add_dashboard_to_users.rb | 1 + .../20150620233230_add_default_otp_required_for_login_value.rb | 1 + db/migrate/20150713160110_add_project_view_to_users.rb | 1 + db/migrate/20150717130904_add_commits_count_to_project.rb | 1 + .../20150730122406_add_updated_by_to_issuables_and_notes.rb | 1 + db/migrate/20150806104937_create_abuse_reports.rb | 1 + db/migrate/20150812080800_add_settings_import_sources.rb | 1 + db/migrate/20150814065925_remove_oauth_tokens_from_users.rb | 1 + db/migrate/20150817163600_deduplicate_user_identities.rb | 1 + db/migrate/20150818213832_add_sent_notifications.rb | 1 + db/migrate/20150824002011_add_enable_ssl_verification.rb | 1 + db/migrate/20150826001931_add_ci_tables.rb | 1 + db/migrate/20150902001023_add_template_to_label.rb | 1 + db/migrate/20150914215247_add_ci_tags.rb | 1 + .../20150915001905_enable_ssl_verification_by_default.rb | 1 + .../20150916000405_enable_ssl_verification_for_web_hooks.rb | 1 + ...0150916114643_add_help_page_text_to_application_settings.rb | 1 + db/migrate/20150916145038_add_index_for_committed_at_and_id.rb | 1 + .../20150918084513_add_ci_enabled_to_application_settings.rb | 1 + ...0918161719_remove_invalid_milestones_from_merge_requests.rb | 1 + db/migrate/20150920010715_add_consumed_timestep_to_users.rb | 1 + .../20150920161119_add_line_code_to_sent_notification.rb | 1 + db/migrate/20150924125150_add_project_id_to_ci_commit.rb | 1 + db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb | 1 + db/migrate/20150930001110_merge_request_error_field.rb | 1 + db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb | 1 + db/migrate/20150930110012_add_group_share_lock.rb | 1 + db/migrate/20151002112914_add_stage_idx_to_builds.rb | 1 + db/migrate/20151002121400_add_index_for_builds.rb | 1 + db/migrate/20151002122929_add_ref_and_tag_to_builds.rb | 1 + db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb | 1 + db/migrate/20151005075649_add_user_id_to_build.rb | 1 + db/migrate/20151005150751_add_layout_option_for_users.rb | 1 + ...151005162154_remove_ci_enabled_from_application_settings.rb | 1 + .../20151007120511_namespaces_projects_path_lower_indexes.rb | 1 + .../20151008110232_add_users_lower_username_email_indexes.rb | 1 + .../20151008123042_add_type_and_description_to_builds.rb | 1 + .../20151008130321_migrate_name_to_description_for_builds.rb | 1 + .../20151008143519_add_admin_notification_email_setting.rb | 1 + db/migrate/20151012173029_set_jira_service_api_url.rb | 1 + db/migrate/20151013092124_add_artifacts_file_to_builds.rb | 1 + .../20151016131433_add_ci_projects_gl_project_id_index.rb | 1 + .../20151016195451_add_ci_builds_and_projects_indexes.rb | 1 + db/migrate/20151016195706_add_notes_line_code_index.rb | 1 + db/migrate/20151019111551_fix_build_tags.rb | 1 + db/migrate/20151019111703_fail_build_without_names.rb | 1 + db/migrate/20151020145526_add_services_template_index.rb | 1 + db/migrate/20151020173516_ci_limits_to_mysql.rb | 1 + db/migrate/20151020173906_add_ci_builds_index_for_status.rb | 1 + db/migrate/20151023112551_fail_build_with_empty_name.rb | 1 + db/migrate/20151023144219_remove_satellites.rb | 1 + db/migrate/20151026182941_add_project_path_index.rb | 1 + ...028152939_add_merge_when_build_succeeds_to_merge_request.rb | 1 + db/migrate/20151103001141_add_public_to_group.rb | 1 + db/migrate/20151103133339_add_shared_runners_setting.rb | 1 + db/migrate/20151103134857_create_lfs_objects.rb | 1 + db/migrate/20151103134958_create_lfs_objects_projects.rb | 1 + db/migrate/20151104105513_add_file_to_lfs_objects.rb | 1 + db/migrate/20151105094515_create_releases.rb | 1 + db/migrate/20151106000015_add_is_award_to_notes.rb | 1 + ...109100728_add_max_artifacts_size_to_application_settings.rb | 1 + db/migrate/20151109134526_add_issues_state_index.rb | 1 + .../20151109134916_add_projects_visibility_level_index.rb | 1 + db/migrate/20151110125604_add_import_error_to_project.rb | 1 + db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb | 1 + db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb | 1 + db/migrate/20151118162244_add_projects_public_index.rb | 1 + db/migrate/20151201203948_raise_hook_url_limit.rb | 1 + db/migrate/20151203162133_add_hide_project_limit_to_users.rb | 1 + db/migrate/20151203162134_add_build_events_to_services.rb | 1 + db/migrate/20151209144329_migrate_ci_web_hooks.rb | 1 + db/migrate/20151209145909_migrate_ci_emails.rb | 1 + db/migrate/20151210030143_add_unlock_token_to_user.rb | 1 + ...3_add_runners_registration_token_to_application_settings.rb | 1 + db/migrate/20151210125232_migrate_ci_slack_service.rb | 1 + db/migrate/20151210125927_migrate_ci_hip_chat_service.rb | 1 + db/migrate/20151210125928_add_ci_to_project.rb | 1 + db/migrate/20151210125929_add_project_id_to_ci.rb | 1 + db/migrate/20151210125930_migrate_ci_to_project.rb | 1 + db/migrate/20151210125931_add_index_to_ci_tables.rb | 1 + db/migrate/20151210125932_drop_null_for_ci_tables.rb | 1 + db/migrate/20151218154042_add_tfa_to_application_settings.rb | 1 + db/migrate/20151221234414_add_tfa_additional_fields.rb | 1 + db/migrate/20151224123230_rename_emojis.rb | 1 + db/migrate/20151228111122_remove_public_from_namespace.rb | 1 + db/migrate/20151228150906_influxdb_settings.rb | 1 + .../20151228175719_add_recaptcha_to_application_settings.rb | 1 + db/migrate/20151229102248_influxdb_udp_port_setting.rb | 1 + db/migrate/20151229112614_influxdb_remote_database_setting.rb | 1 + .../20151230132518_add_artifacts_metadata_to_ci_build.rb | 1 + .../20151231152326_add_akismet_to_application_settings.rb | 1 + ...20151231202530_remove_alert_type_from_broadcast_messages.rb | 1 + db/migrate/20160106162223_add_index_milestones_title.rb | 1 + db/migrate/20160106164438_remove_influxdb_credentials.rb | 1 + db/migrate/20160109054846_create_spam_logs.rb | 1 + db/migrate/20160113111034_add_metrics_sample_interval.rb | 1 + .../20160118155830_add_sentry_to_application_settings.rb | 1 + ...8232755_add_ip_blocking_settings_to_application_settings.rb | 1 + db/migrate/20160119111158_add_services_category.rb | 1 + db/migrate/20160119112418_add_services_default.rb | 1 + db/migrate/20160119145451_add_ldap_email_to_users.rb | 1 + ...0160120172143_add_base_commit_sha_to_merge_request_diffs.rb | 1 + ...1030729_add_email_author_in_body_to_application_settings.rb | 1 + db/migrate/20160122185421_add_pending_delete_to_project.rb | 1 + ...47_remove_ip_blocking_settings_from_application_settings.rb | 1 + db/migrate/20160128233227_change_lfs_objects_size_column.rb | 1 + .../20160129135155_remove_dot_atom_path_ending_of_projects.rb | 1 + .../20160129155512_add_merge_commit_sha_to_merge_requests.rb | 1 + db/migrate/20160202091601_add_erasable_to_ci_build.rb | 1 + .../20160202164642_add_allow_guest_to_access_builds_project.rb | 1 + .../20160204144558_add_real_size_to_merge_request_diffs.rb | 1 + db/migrate/20160209130428_add_index_to_snippet.rb | 1 + db/migrate/20160212123307_create_tasks.rb | 1 + db/migrate/20160217100506_add_description_to_label.rb | 1 + db/migrate/20160217174422_add_note_to_tasks.rb | 1 + db/migrate/20160220123949_rename_tasks_to_todos.rb | 1 + db/migrate/20160222153918_create_appearances_ce.rb | 1 + db/migrate/20160223192159_add_confidential_to_issues.rb | 1 + db/migrate/20160225090018_add_delete_at_to_issues.rb | 1 + db/migrate/20160225101956_add_delete_at_to_merge_requests.rb | 1 + db/migrate/20160226114608_add_trigram_indexes_for_searching.rb | 1 + db/migrate/20160227120001_add_event_field_for_web_hook.rb | 1 + db/migrate/20160227120047_add_event_to_services.rb | 1 + db/migrate/20160229193553_add_main_language_to_repository.rb | 1 + db/migrate/20160301124843_add_visibility_level_to_groups.rb | 1 + ...0302151724_add_import_credentials_to_project_import_data.rb | 1 + .../20160302152808_remove_wrong_import_url_from_projects.rb | 1 + db/migrate/20160305220806_remove_expires_at_from_snippets.rb | 1 + db/migrate/20160307221555_disallow_blank_line_code_on_note.rb | 1 + ...903_add_default_group_visibility_to_application_settings.rb | 1 + db/migrate/20160309140734_fix_todos.rb | 1 + db/migrate/20160310124959_add_due_date_to_issues.rb | 1 + db/migrate/20160310185910_add_external_flag_to_users.rb | 1 + db/migrate/20160314094147_add_priority_to_label.rb | 1 + db/migrate/20160314143402_projects_add_pushes_since_gc.rb | 1 + db/migrate/20160315135439_project_add_repository_check.rb | 1 + db/migrate/20160316123110_ci_runners_token_index.rb | 1 + db/migrate/20160316192622_change_target_id_to_null_on_todos.rb | 1 + db/migrate/20160316204731_add_commit_id_to_todos.rb | 1 + db/migrate/20160317092222_add_moved_to_to_issue.rb | 1 + .../20160320204112_index_namespaces_on_visibility_level.rb | 1 + db/migrate/20160324020319_remove_todos_for_deleted_issues.rb | 1 + db/migrate/20160328112808_create_notification_settings.rb | 1 + db/migrate/20160328115649_migrate_new_notification_setting.rb | 1 + db/migrate/20160328121138_add_notification_setting_index.rb | 1 + .../20160329144452_add_index_on_pending_delete_projects.rb | 1 + .../20160331133914_remove_todos_for_deleted_merge_requests.rb | 1 + ...remove_twitter_sharing_enabled_from_application_settings.rb | 1 + db/migrate/20160407120251_add_images_enabled_for_project.rb | 1 + .../20160412140240_add_repository_checks_enabled_setting.rb | 1 + db/migrate/20160412173416_add_fields_to_ci_commit.rb | 1 + db/migrate/20160412173417_update_ci_commit.rb | 1 + db/migrate/20160412173418_add_ci_commit_indexes.rb | 1 + db/migrate/20160413115152_add_token_to_web_hooks.rb | 1 + ...15133440_add_shared_runners_text_to_application_settings.rb | 1 + db/migrate/20160416180807_add_award_emoji.rb | 1 + db/migrate/20160416182152_convert_award_note_to_emoji_award.rb | 1 + db/migrate/20160416190505_remove_note_is_award.rb | 1 + db/migrate/20160419120017_add_metrics_packet_size.rb | 1 + db/migrate/20160421130527_disable_repository_checks.rb | 1 + db/migrate/20160425045124_create_u2f_registrations.rb | 1 + ...d_disabled_oauth_sign_in_sources_to_application_settings.rb | 1 + db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb | 1 + db/migrate/20160508194200_remove_wall_enabled_from_projects.rb | 1 + db/migrate/20160508215820_add_type_to_notes.rb | 1 + db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb | 1 + ...28_add_health_check_access_token_to_application_settings.rb | 1 + ...add_send_user_confirmation_email_to_application_settings.rb | 1 + .../20160525205328_remove_main_language_from_projects.rb | 1 + ...020117_remove_notification_settings_for_deleted_projects.rb | 1 + db/migrate/20160528043124_add_users_state_index.rb | 1 + ...iner_registry_token_expire_delay_to_application_settings.rb | 1 + .../20160603180330_remove_duplicated_notification_settings.rb | 1 + .../20160603182247_add_index_to_notification_settings.rb | 1 + ...608155312_add_after_sign_up_text_to_application_settings.rb | 1 + db/migrate/limits_to_mysql.rb | 1 + 343 files changed, 344 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 678f7db025..c637f5e12f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -13,7 +13,8 @@ AllCops: # Exclude some GitLab files Exclude: - 'vendor/**/*' - - 'db/**/*' + - 'db/*' + - 'db/fixtures/**/*' - 'tmp/**/*' - 'bin/**/*' - 'lib/backup/**/*' diff --git a/db/migrate/20121220064453_init_schema.rb b/db/migrate/20121220064453_init_schema.rb index d7644b6847..f93dc92b70 100644 --- a/db/migrate/20121220064453_init_schema.rb +++ b/db/migrate/20121220064453_init_schema.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class InitSchema < ActiveRecord::Migration def up diff --git a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb b/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb index d0fca26987..84fd206077 100644 --- a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb +++ b/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameOwnerToCreatorForProject < ActiveRecord::Migration def change rename_column :projects, :owner_id, :creator_id diff --git a/db/migrate/20130110172407_add_public_to_project.rb b/db/migrate/20130110172407_add_public_to_project.rb index 45edba4815..4362aadcc1 100644 --- a/db/migrate/20130110172407_add_public_to_project.rb +++ b/db/migrate/20130110172407_add_public_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPublicToProject < ActiveRecord::Migration def change add_column :projects, :public, :boolean, default: false, null: false diff --git a/db/migrate/20130123114545_add_issues_tracker_to_project.rb b/db/migrate/20130123114545_add_issues_tracker_to_project.rb index 288d0f07c9..ba8c50b53e 100644 --- a/db/migrate/20130123114545_add_issues_tracker_to_project.rb +++ b/db/migrate/20130123114545_add_issues_tracker_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIssuesTrackerToProject < ActiveRecord::Migration def change add_column :projects, :issues_tracker, :string, default: :gitlab, null: false diff --git a/db/migrate/20130125090214_add_user_permissions.rb b/db/migrate/20130125090214_add_user_permissions.rb index 38b5f439a2..1350eadb60 100644 --- a/db/migrate/20130125090214_add_user_permissions.rb +++ b/db/migrate/20130125090214_add_user_permissions.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUserPermissions < ActiveRecord::Migration def up add_column :users, :can_create_group, :boolean, default: true, null: false diff --git a/db/migrate/20130131070232_remove_private_flag_from_project.rb b/db/migrate/20130131070232_remove_private_flag_from_project.rb index 5754db1155..f0273ba448 100644 --- a/db/migrate/20130131070232_remove_private_flag_from_project.rb +++ b/db/migrate/20130131070232_remove_private_flag_from_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemovePrivateFlagFromProject < ActiveRecord::Migration def up remove_column :projects, :private_flag diff --git a/db/migrate/20130206084024_add_description_to_namsespace.rb b/db/migrate/20130206084024_add_description_to_namsespace.rb index ef02e489d0..62676ce891 100644 --- a/db/migrate/20130206084024_add_description_to_namsespace.rb +++ b/db/migrate/20130206084024_add_description_to_namsespace.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDescriptionToNamsespace < ActiveRecord::Migration def change add_column :namespaces, :description, :string, default: '', null: false diff --git a/db/migrate/20130207104426_add_description_to_teams.rb b/db/migrate/20130207104426_add_description_to_teams.rb index 6d03777901..bd9a4767b6 100644 --- a/db/migrate/20130207104426_add_description_to_teams.rb +++ b/db/migrate/20130207104426_add_description_to_teams.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDescriptionToTeams < ActiveRecord::Migration def change add_column :user_teams, :description, :string, default: '', null: false diff --git a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb index 71763d18ae..56b01cbf89 100644 --- a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb +++ b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIssuesTrackerIdToProject < ActiveRecord::Migration def change add_column :projects, :issues_tracker_id, :string diff --git a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb index 23797fe189..4722cc13d4 100644 --- a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb +++ b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameStateToMergeStatusInMilestone < ActiveRecord::Migration def change rename_column :merge_requests, :state, :merge_status diff --git a/db/migrate/20130218140952_add_state_to_issue.rb b/db/migrate/20130218140952_add_state_to_issue.rb index 062103d0e3..3a5e978a18 100644 --- a/db/migrate/20130218140952_add_state_to_issue.rb +++ b/db/migrate/20130218140952_add_state_to_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStateToIssue < ActiveRecord::Migration def change add_column :issues, :state, :string diff --git a/db/migrate/20130218141038_add_state_to_merge_request.rb b/db/migrate/20130218141038_add_state_to_merge_request.rb index ac4108ee31..e0180c755e 100644 --- a/db/migrate/20130218141038_add_state_to_merge_request.rb +++ b/db/migrate/20130218141038_add_state_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStateToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :state, :string diff --git a/db/migrate/20130218141117_add_state_to_milestone.rb b/db/migrate/20130218141117_add_state_to_milestone.rb index c84039106b..5f71608692 100644 --- a/db/migrate/20130218141117_add_state_to_milestone.rb +++ b/db/migrate/20130218141117_add_state_to_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStateToMilestone < ActiveRecord::Migration def change add_column :milestones, :state, :string diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb index 99289166e8..94c0a6845d 100644 --- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb +++ b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertClosedToStateInIssue < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb index bd1e016d67..64a9c76135 100644 --- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb +++ b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb index d1174bc3d9..41508c2dc9 100644 --- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb +++ b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertClosedToStateInMilestone < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20130218141444_remove_merged_from_merge_request.rb b/db/migrate/20130218141444_remove_merged_from_merge_request.rb index a7bd82f500..afa5137061 100644 --- a/db/migrate/20130218141444_remove_merged_from_merge_request.rb +++ b/db/migrate/20130218141444_remove_merged_from_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveMergedFromMergeRequest < ActiveRecord::Migration def up remove_column :merge_requests, :merged diff --git a/db/migrate/20130218141507_remove_closed_from_issue.rb b/db/migrate/20130218141507_remove_closed_from_issue.rb index 95cc064252..f250288bc3 100644 --- a/db/migrate/20130218141507_remove_closed_from_issue.rb +++ b/db/migrate/20130218141507_remove_closed_from_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveClosedFromIssue < ActiveRecord::Migration def up remove_column :issues, :closed diff --git a/db/migrate/20130218141536_remove_closed_from_merge_request.rb b/db/migrate/20130218141536_remove_closed_from_merge_request.rb index 371835938b..efa12e3263 100644 --- a/db/migrate/20130218141536_remove_closed_from_merge_request.rb +++ b/db/migrate/20130218141536_remove_closed_from_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveClosedFromMergeRequest < ActiveRecord::Migration def up remove_column :merge_requests, :closed diff --git a/db/migrate/20130218141554_remove_closed_from_milestone.rb b/db/migrate/20130218141554_remove_closed_from_milestone.rb index e8dae4a19b..75ac14e43b 100644 --- a/db/migrate/20130218141554_remove_closed_from_milestone.rb +++ b/db/migrate/20130218141554_remove_closed_from_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveClosedFromMilestone < ActiveRecord::Migration def up remove_column :milestones, :closed diff --git a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb index d78bd0ae92..97615e47c8 100644 --- a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb +++ b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNewMergeStatusToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :new_merge_status, :string diff --git a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb index 1c758c56ff..3b8c3686c5 100644 --- a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb +++ b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration def up execute "UPDATE #{table_name} SET new_merge_status = 'unchecked' WHERE merge_status = 1" diff --git a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb index 9083183beb..bd25ffbfc9 100644 --- a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb +++ b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveMergeStatusFromMergeRequest < ActiveRecord::Migration def up remove_column :merge_requests, :merge_status diff --git a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb index 3f8f38dc97..f0595720a3 100644 --- a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb +++ b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameNewMergeStatusToMergeStatusInMilestone < ActiveRecord::Migration def change rename_column :merge_requests, :new_merge_status, :merge_status diff --git a/db/migrate/20130304104623_add_state_to_user.rb b/db/migrate/20130304104623_add_state_to_user.rb index 8154c21065..4456d022e3 100644 --- a/db/migrate/20130304104623_add_state_to_user.rb +++ b/db/migrate/20130304104623_add_state_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStateToUser < ActiveRecord::Migration def change add_column :users, :state, :string diff --git a/db/migrate/20130304104740_convert_blocked_to_state.rb b/db/migrate/20130304104740_convert_blocked_to_state.rb index e8d5257ac9..9afd109364 100644 --- a/db/migrate/20130304104740_convert_blocked_to_state.rb +++ b/db/migrate/20130304104740_convert_blocked_to_state.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertBlockedToState < ActiveRecord::Migration def up User.transaction do diff --git a/db/migrate/20130304105317_remove_blocked_from_user.rb b/db/migrate/20130304105317_remove_blocked_from_user.rb index e010474538..8f5b2c59b4 100644 --- a/db/migrate/20130304105317_remove_blocked_from_user.rb +++ b/db/migrate/20130304105317_remove_blocked_from_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveBlockedFromUser < ActiveRecord::Migration def up remove_column :users, :blocked diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb index 56c9a31ee3..06e28a49d9 100644 --- a/db/migrate/20130315124931_user_color_scheme.rb +++ b/db/migrate/20130315124931_user_color_scheme.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class UserColorScheme < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20130318212250_add_snippets_to_features.rb b/db/migrate/20130318212250_add_snippets_to_features.rb index ad0b4434c4..9860b85f50 100644 --- a/db/migrate/20130318212250_add_snippets_to_features.rb +++ b/db/migrate/20130318212250_add_snippets_to_features.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSnippetsToFeatures < ActiveRecord::Migration def change add_column :projects, :snippets_enabled, :boolean, null: false, default: true diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb index f91afc26e7..66eb11a4b2 100644 --- a/db/migrate/20130319214458_create_forked_project_links.rb +++ b/db/migrate/20130319214458_create_forked_project_links.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateForkedProjectLinks < ActiveRecord::Migration def change create_table :forked_project_links do |t| diff --git a/db/migrate/20130323174317_add_private_to_snippets.rb b/db/migrate/20130323174317_add_private_to_snippets.rb index 92f3a5c701..376f4618d4 100644 --- a/db/migrate/20130323174317_add_private_to_snippets.rb +++ b/db/migrate/20130323174317_add_private_to_snippets.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPrivateToSnippets < ActiveRecord::Migration def change add_column :snippets, :private, :boolean, null: false, default: true diff --git a/db/migrate/20130324151736_add_type_to_snippets.rb b/db/migrate/20130324151736_add_type_to_snippets.rb index 276aab2ca1..097cb9bc7c 100644 --- a/db/migrate/20130324151736_add_type_to_snippets.rb +++ b/db/migrate/20130324151736_add_type_to_snippets.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTypeToSnippets < ActiveRecord::Migration def change add_column :snippets, :type, :string diff --git a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb index 4c992bac4d..9256e62086 100644 --- a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb +++ b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeProjectIdToNullInSnipepts < ActiveRecord::Migration def up change_column :snippets, :project_id, :integer, :null => true diff --git a/db/migrate/20130324203535_add_type_value_for_snippets.rb b/db/migrate/20130324203535_add_type_value_for_snippets.rb index 8c05dd2cc7..6e910fd74c 100644 --- a/db/migrate/20130324203535_add_type_value_for_snippets.rb +++ b/db/migrate/20130324203535_add_type_value_for_snippets.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTypeValueForSnippets < ActiveRecord::Migration def up Snippet.where("project_id IS NOT NULL").update_all(type: 'ProjectSnippet') diff --git a/db/migrate/20130325173941_add_notification_level_to_user.rb b/db/migrate/20130325173941_add_notification_level_to_user.rb index 9f466e38c1..1dc58d4bcc 100644 --- a/db/migrate/20130325173941_add_notification_level_to_user.rb +++ b/db/migrate/20130325173941_add_notification_level_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationLevelToUser < ActiveRecord::Migration def change add_column :users, :notification_level, :integer, null: false, default: 1 diff --git a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb index d42ef11373..0592181927 100644 --- a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb +++ b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToUsersAuthenticationToken < ActiveRecord::Migration def change add_index :users, :authentication_token, unique: true diff --git a/db/migrate/20130403003950_add_last_activity_column_into_project.rb b/db/migrate/20130403003950_add_last_activity_column_into_project.rb index 85e31608d7..04a01612c6 100644 --- a/db/migrate/20130403003950_add_last_activity_column_into_project.rb +++ b/db/migrate/20130403003950_add_last_activity_column_into_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLastActivityColumnIntoProject < ActiveRecord::Migration def up add_column :projects, :last_activity_at, :datetime diff --git a/db/migrate/20130404164628_add_notification_level_to_user_project.rb b/db/migrate/20130404164628_add_notification_level_to_user_project.rb index 27de5d6bf5..1e072d9c6e 100644 --- a/db/migrate/20130404164628_add_notification_level_to_user_project.rb +++ b/db/migrate/20130404164628_add_notification_level_to_user_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationLevelToUserProject < ActiveRecord::Migration def change add_column :users_projects, :notification_level, :integer, null: false, default: 3 diff --git a/db/migrate/20130410175022_remove_wiki_table.rb b/db/migrate/20130410175022_remove_wiki_table.rb index 9077aa2473..5885b1cc37 100644 --- a/db/migrate/20130410175022_remove_wiki_table.rb +++ b/db/migrate/20130410175022_remove_wiki_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveWikiTable < ActiveRecord::Migration def up drop_table :wikis diff --git a/db/migrate/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb index 56ea97e856..ec953986c6 100644 --- a/db/migrate/20130419190306_allow_merges_for_forks.rb +++ b/db/migrate/20130419190306_allow_merges_for_forks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AllowMergesForForks < ActiveRecord::Migration def self.up add_column :merge_requests, :target_project_id, :integer, :null => true diff --git a/db/migrate/20130506085413_add_type_to_key.rb b/db/migrate/20130506085413_add_type_to_key.rb index 315e7ca77b..c9f1ee4e38 100644 --- a/db/migrate/20130506085413_add_type_to_key.rb +++ b/db/migrate/20130506085413_add_type_to_key.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTypeToKey < ActiveRecord::Migration def change add_column :keys, :type, :string diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb index 0dc8cdeb07..7d6662d358 100644 --- a/db/migrate/20130506090604_create_deploy_keys_projects.rb +++ b/db/migrate/20130506090604_create_deploy_keys_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateDeployKeysProjects < ActiveRecord::Migration def change create_table :deploy_keys_projects do |t| diff --git a/db/migrate/20130506095501_remove_project_id_from_key.rb b/db/migrate/20130506095501_remove_project_id_from_key.rb index 6b794cfb5c..53abc4e7b5 100644 --- a/db/migrate/20130506095501_remove_project_id_from_key.rb +++ b/db/migrate/20130506095501_remove_project_id_from_key.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveProjectIdFromKey < ActiveRecord::Migration def up puts 'Migrate deploy keys: ' diff --git a/db/migrate/20130522141856_add_more_fields_to_service.rb b/db/migrate/20130522141856_add_more_fields_to_service.rb index 298e902df2..9f764a1d05 100644 --- a/db/migrate/20130522141856_add_more_fields_to_service.rb +++ b/db/migrate/20130522141856_add_more_fields_to_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMoreFieldsToService < ActiveRecord::Migration def change add_column :services, :subdomain, :string diff --git a/db/migrate/20130528184641_add_system_to_notes.rb b/db/migrate/20130528184641_add_system_to_notes.rb index 1b22a4934f..27fbf8983a 100644 --- a/db/migrate/20130528184641_add_system_to_notes.rb +++ b/db/migrate/20130528184641_add_system_to_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSystemToNotes < ActiveRecord::Migration class Note < ActiveRecord::Base end diff --git a/db/migrate/20130611210815_increase_snippet_text_column_size.rb b/db/migrate/20130611210815_increase_snippet_text_column_size.rb index f7b4447e43..f710c79a9a 100644 --- a/db/migrate/20130611210815_increase_snippet_text_column_size.rb +++ b/db/migrate/20130611210815_increase_snippet_text_column_size.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class IncreaseSnippetTextColumnSize < ActiveRecord::Migration def up # MYSQL LARGETEXT for snippet diff --git a/db/migrate/20130613165816_add_password_expires_at_to_users.rb b/db/migrate/20130613165816_add_password_expires_at_to_users.rb index 3479c8e64d..47306a370a 100644 --- a/db/migrate/20130613165816_add_password_expires_at_to_users.rb +++ b/db/migrate/20130613165816_add_password_expires_at_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPasswordExpiresAtToUsers < ActiveRecord::Migration def change add_column :users, :password_expires_at, :datetime diff --git a/db/migrate/20130613173246_add_created_by_id_to_user.rb b/db/migrate/20130613173246_add_created_by_id_to_user.rb index 615e96eb15..3138c0f40a 100644 --- a/db/migrate/20130613173246_add_created_by_id_to_user.rb +++ b/db/migrate/20130613173246_add_created_by_id_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCreatedByIdToUser < ActiveRecord::Migration def change add_column :users, :created_by_id, :integer diff --git a/db/migrate/20130614132337_add_improted_to_project.rb b/db/migrate/20130614132337_add_improted_to_project.rb index cc882c3f10..26dc16e3b4 100644 --- a/db/migrate/20130614132337_add_improted_to_project.rb +++ b/db/migrate/20130614132337_add_improted_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImprotedToProject < ActiveRecord::Migration def change add_column :projects, :imported, :boolean, default: false, null: false diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb index 2efc04f115..45cff93fe4 100644 --- a/db/migrate/20130617095603_create_users_groups.rb +++ b/db/migrate/20130617095603_create_users_groups.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateUsersGroups < ActiveRecord::Migration def change create_table :users_groups do |t| diff --git a/db/migrate/20130621195223_add_notification_level_to_user_group.rb b/db/migrate/20130621195223_add_notification_level_to_user_group.rb index 8c2e3dfcac..6fd4941f61 100644 --- a/db/migrate/20130621195223_add_notification_level_to_user_group.rb +++ b/db/migrate/20130621195223_add_notification_level_to_user_group.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationLevelToUserGroup < ActiveRecord::Migration def change add_column :users_groups, :notification_level, :integer, null: false, default: 3 diff --git a/db/migrate/20130622115340_add_more_db_index.rb b/db/migrate/20130622115340_add_more_db_index.rb index 9570a7a3f1..4113217de5 100644 --- a/db/migrate/20130622115340_add_more_db_index.rb +++ b/db/migrate/20130622115340_add_more_db_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMoreDbIndex < ActiveRecord::Migration def change add_index :deploy_keys_projects, :project_id diff --git a/db/migrate/20130624162710_add_fingerprint_to_key.rb b/db/migrate/20130624162710_add_fingerprint_to_key.rb index 544a836672..3e574ea81b 100644 --- a/db/migrate/20130624162710_add_fingerprint_to_key.rb +++ b/db/migrate/20130624162710_add_fingerprint_to_key.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddFingerprintToKey < ActiveRecord::Migration def change add_column :keys, :fingerprint, :string diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb index 395083f2a0..bd9d40a50d 100644 --- a/db/migrate/20130711063759_create_project_group_links.rb +++ b/db/migrate/20130711063759_create_project_group_links.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateProjectGroupLinks < ActiveRecord::Migration def change create_table :project_group_links do |t| diff --git a/db/migrate/20130804151314_add_st_diff_to_note.rb b/db/migrate/20130804151314_add_st_diff_to_note.rb index 3f9abb975c..9e2da73b69 100644 --- a/db/migrate/20130804151314_add_st_diff_to_note.rb +++ b/db/migrate/20130804151314_add_st_diff_to_note.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStDiffToNote < ActiveRecord::Migration def change add_column :notes, :st_diff, :text, :null => true diff --git a/db/migrate/20130809124851_add_permission_check_to_user.rb b/db/migrate/20130809124851_add_permission_check_to_user.rb index c26157904c..9f9dea3610 100644 --- a/db/migrate/20130809124851_add_permission_check_to_user.rb +++ b/db/migrate/20130809124851_add_permission_check_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPermissionCheckToUser < ActiveRecord::Migration def change add_column :users, :last_credential_check_at, :datetime diff --git a/db/migrate/20130812143708_add_import_url_to_project.rb b/db/migrate/20130812143708_add_import_url_to_project.rb index 023a48741b..d2bdfe1894 100644 --- a/db/migrate/20130812143708_add_import_url_to_project.rb +++ b/db/migrate/20130812143708_add_import_url_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportUrlToProject < ActiveRecord::Migration def change add_column :projects, :import_url, :string diff --git a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb index e55ae38f14..0e0e78b0f0 100644 --- a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb +++ b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddInternalIdsToIssuesAndMr < ActiveRecord::Migration def change add_column :issues, :iid, :integer diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb index 00e3947a6b..98f3fa8752 100644 --- a/db/migrate/20130820102832_add_access_to_project_group_link.rb +++ b/db/migrate/20130820102832_add_access_to_project_group_link.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAccessToProjectGroupLink < ActiveRecord::Migration def change add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access diff --git a/db/migrate/20130821090530_remove_deprecated_tables.rb b/db/migrate/20130821090530_remove_deprecated_tables.rb index 539c0617ee..d22e713a7a 100644 --- a/db/migrate/20130821090530_remove_deprecated_tables.rb +++ b/db/migrate/20130821090530_remove_deprecated_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveDeprecatedTables < ActiveRecord::Migration def up drop_table :user_teams diff --git a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb index 33e5bae580..e25b8f9166 100644 --- a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb +++ b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddInternalIdsToMilestones < ActiveRecord::Migration def change add_column :milestones, :iid, :integer diff --git a/db/migrate/20130909132950_add_description_to_merge_request.rb b/db/migrate/20130909132950_add_description_to_merge_request.rb index 9bcd0c7ee0..fbac50c821 100644 --- a/db/migrate/20130909132950_add_description_to_merge_request.rb +++ b/db/migrate/20130909132950_add_description_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDescriptionToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :description, :text, null: true diff --git a/db/migrate/20130926081215_change_owner_id_for_group.rb b/db/migrate/20130926081215_change_owner_id_for_group.rb index 8f1992c37a..2bdd22d5a0 100644 --- a/db/migrate/20130926081215_change_owner_id_for_group.rb +++ b/db/migrate/20130926081215_change_owner_id_for_group.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeOwnerIdForGroup < ActiveRecord::Migration def up change_column :namespaces, :owner_id, :integer, null: true diff --git a/db/migrate/20131005191208_add_avatar_to_users.rb b/db/migrate/20131005191208_add_avatar_to_users.rb index 7b4de37ad7..df9057b81d 100644 --- a/db/migrate/20131005191208_add_avatar_to_users.rb +++ b/db/migrate/20131005191208_add_avatar_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAvatarToUsers < ActiveRecord::Migration def change add_column :users, :avatar, :string diff --git a/db/migrate/20131009115346_add_confirmable_to_users.rb b/db/migrate/20131009115346_add_confirmable_to_users.rb index 249cbe704e..d714dd98e8 100644 --- a/db/migrate/20131009115346_add_confirmable_to_users.rb +++ b/db/migrate/20131009115346_add_confirmable_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddConfirmableToUsers < ActiveRecord::Migration def self.up add_column :users, :confirmation_token, :string diff --git a/db/migrate/20131106151520_remove_default_branch.rb b/db/migrate/20131106151520_remove_default_branch.rb index 88a890eb3e..fd3d1ed7ab 100644 --- a/db/migrate/20131106151520_remove_default_branch.rb +++ b/db/migrate/20131106151520_remove_default_branch.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveDefaultBranch < ActiveRecord::Migration def up remove_column :projects, :default_branch diff --git a/db/migrate/20131112114325_create_broadcast_messages.rb b/db/migrate/20131112114325_create_broadcast_messages.rb index 147178e9dc..ce37a8e270 100644 --- a/db/migrate/20131112114325_create_broadcast_messages.rb +++ b/db/migrate/20131112114325_create_broadcast_messages.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateBroadcastMessages < ActiveRecord::Migration def change create_table :broadcast_messages do |t| diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb index 89421cbeda..5efc17b228 100644 --- a/db/migrate/20131112220935_add_visibility_level_to_projects.rb +++ b/db/migrate/20131112220935_add_visibility_level_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddVisibilityLevelToProjects < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20131129154016_add_archived_to_projects.rb b/db/migrate/20131129154016_add_archived_to_projects.rb index 917e690ba4..e8e6908d13 100644 --- a/db/migrate/20131129154016_add_archived_to_projects.rb +++ b/db/migrate/20131129154016_add_archived_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddArchivedToProjects < ActiveRecord::Migration def change add_column :projects, :archived, :boolean, default: false, null: false diff --git a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb b/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb index 473f355ece..348a284a53 100644 --- a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb +++ b/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddColorAndFontToBroadcastMessages < ActiveRecord::Migration def change add_column :broadcast_messages, :color, :string diff --git a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb b/db/migrate/20131202192556_add_event_fields_for_web_hook.rb index d29e996852..99d7661152 100644 --- a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb +++ b/db/migrate/20131202192556_add_event_fields_for_web_hook.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEventFieldsForWebHook < ActiveRecord::Migration def change add_column :web_hooks, :push_events, :boolean, default: true, null: false diff --git a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb b/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb index 7cec79e7ee..4333dc5932 100644 --- a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb +++ b/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHideNoSshKeyToUsers < ActiveRecord::Migration def change add_column :users, :hide_no_ssh_key, :boolean, :default => false diff --git a/db/migrate/20131217102743_add_recipients_to_service.rb b/db/migrate/20131217102743_add_recipients_to_service.rb index 9695c25135..3c76be0f68 100644 --- a/db/migrate/20131217102743_add_recipients_to_service.rb +++ b/db/migrate/20131217102743_add_recipients_to_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRecipientsToService < ActiveRecord::Migration def change add_column :services, :recipients, :text diff --git a/db/migrate/20140116231608_add_website_url_to_users.rb b/db/migrate/20140116231608_add_website_url_to_users.rb index 0996fdcad7..1c39423562 100644 --- a/db/migrate/20140116231608_add_website_url_to_users.rb +++ b/db/migrate/20140116231608_add_website_url_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddWebsiteUrlToUsers < ActiveRecord::Migration def change add_column :users, :website_url, :string, {:null => false, :default => ''} diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb index f34e30925d..395c3edfc7 100644 --- a/db/migrate/20140122112253_create_merge_request_diffs.rb +++ b/db/migrate/20140122112253_create_merge_request_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateMergeRequestDiffs < ActiveRecord::Migration def up create_table :merge_request_diffs do |t| diff --git a/db/migrate/20140122114406_migrate_mr_diffs.rb b/db/migrate/20140122114406_migrate_mr_diffs.rb index 1595e2b647..429aeb2293 100644 --- a/db/migrate/20140122114406_migrate_mr_diffs.rb +++ b/db/migrate/20140122114406_migrate_mr_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateMrDiffs < ActiveRecord::Migration def self.up execute "INSERT INTO merge_request_diffs ( merge_request_id, st_commits, st_diffs ) SELECT id, st_commits, st_diffs FROM merge_requests" diff --git a/db/migrate/20140122122549_remove_m_rdiff_fields.rb b/db/migrate/20140122122549_remove_m_rdiff_fields.rb index 8f863d85a6..bbf35811b6 100644 --- a/db/migrate/20140122122549_remove_m_rdiff_fields.rb +++ b/db/migrate/20140122122549_remove_m_rdiff_fields.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveMRdiffFields < ActiveRecord::Migration def up remove_column :merge_requests, :st_commits diff --git a/db/migrate/20140125162722_add_avatar_to_projects.rb b/db/migrate/20140125162722_add_avatar_to_projects.rb index 9523ac722f..888341b753 100644 --- a/db/migrate/20140125162722_add_avatar_to_projects.rb +++ b/db/migrate/20140125162722_add_avatar_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAvatarToProjects < ActiveRecord::Migration def change add_column :projects, :avatar, :string diff --git a/db/migrate/20140127170938_add_group_avatars.rb b/db/migrate/20140127170938_add_group_avatars.rb index 2911096dd5..95d1c1c6b2 100644 --- a/db/migrate/20140127170938_add_group_avatars.rb +++ b/db/migrate/20140127170938_add_group_avatars.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddGroupAvatars < ActiveRecord::Migration def change add_column :namespaces, :avatar, :string diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb index cb78c4af11..571beb19cd 100644 --- a/db/migrate/20140209025651_create_emails.rb +++ b/db/migrate/20140209025651_create_emails.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateEmails < ActiveRecord::Migration def change create_table :emails do |t| diff --git a/db/migrate/20140214102325_add_api_key_to_services.rb b/db/migrate/20140214102325_add_api_key_to_services.rb index 30eeca2c1f..b58c36c0a3 100644 --- a/db/migrate/20140214102325_add_api_key_to_services.rb +++ b/db/migrate/20140214102325_add_api_key_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddApiKeyToServices < ActiveRecord::Migration def change add_column :services, :api_key, :string diff --git a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb b/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb index 65d28e8cb0..aab8a41c2c 100644 --- a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb +++ b/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexMergeRequestDiffsOnMergeRequestId < ActiveRecord::Migration def change add_index :merge_request_diffs, :merge_request_id, unique: true diff --git a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb b/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb index 7017148702..ec163bb843 100644 --- a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb +++ b/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTagPushHooksToProjectHook < ActiveRecord::Migration def change add_column :web_hooks, :tag_push_events, :boolean, default: false diff --git a/db/migrate/20140312145357_add_import_status_to_project.rb b/db/migrate/20140312145357_add_import_status_to_project.rb index ef972e8342..9947cd8c6f 100644 --- a/db/migrate/20140312145357_add_import_status_to_project.rb +++ b/db/migrate/20140312145357_add_import_status_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportStatusToProject < ActiveRecord::Migration def change add_column :projects, :import_status, :string diff --git a/db/migrate/20140313092127_migrate_already_imported_projects.rb b/db/migrate/20140313092127_migrate_already_imported_projects.rb index 0a9f73a575..f2e91fe1b4 100644 --- a/db/migrate/20140313092127_migrate_already_imported_projects.rb +++ b/db/migrate/20140313092127_migrate_already_imported_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateAlreadyImportedProjects < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20140407135544_fix_namespaces.rb b/db/migrate/20140407135544_fix_namespaces.rb index 59665d538f..9137496669 100644 --- a/db/migrate/20140407135544_fix_namespaces.rb +++ b/db/migrate/20140407135544_fix_namespaces.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixNamespaces < ActiveRecord::Migration def up Namespace.where('name <> path and type is null').each do |namespace| diff --git a/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb b/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb index 1f6d85d5f6..fb9c7a6636 100644 --- a/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb +++ b/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeStateToAllowEmptyMergeRequestDiffs < ActiveRecord::Migration def up change_column :merge_request_diffs, :state, :string, null: true, diff --git a/db/migrate/20140415124820_limits_to_mysql.rb b/db/migrate/20140415124820_limits_to_mysql.rb index 3f6e62617c..c712423bcd 100644 --- a/db/migrate/20140415124820_limits_to_mysql.rb +++ b/db/migrate/20140415124820_limits_to_mysql.rb @@ -1 +1,2 @@ +# rubocop:disable all require_relative 'limits_to_mysql' diff --git a/db/migrate/20140416074002_add_index_on_iid.rb b/db/migrate/20140416074002_add_index_on_iid.rb index 85269e2a03..6cdaa5a3c0 100644 --- a/db/migrate/20140416074002_add_index_on_iid.rb +++ b/db/migrate/20140416074002_add_index_on_iid.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexOnIid < ActiveRecord::Migration def change RemoveDuplicateIid.clean(Issue) diff --git a/db/migrate/20140416185734_index_on_current_sign_in_at.rb b/db/migrate/20140416185734_index_on_current_sign_in_at.rb index 0bf80ce154..8c620b545b 100644 --- a/db/migrate/20140416185734_index_on_current_sign_in_at.rb +++ b/db/migrate/20140416185734_index_on_current_sign_in_at.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class IndexOnCurrentSignInAt < ActiveRecord::Migration def change add_index :users, :current_sign_in_at diff --git a/db/migrate/20140428105831_add_notes_index_updated_at.rb b/db/migrate/20140428105831_add_notes_index_updated_at.rb index 6c25570f12..0589101af9 100644 --- a/db/migrate/20140428105831_add_notes_index_updated_at.rb +++ b/db/migrate/20140428105831_add_notes_index_updated_at.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotesIndexUpdatedAt < ActiveRecord::Migration def change add_index :notes, :updated_at diff --git a/db/migrate/20140502115131_add_repo_size_to_db.rb b/db/migrate/20140502115131_add_repo_size_to_db.rb index 7361d1a944..090b30a4f2 100644 --- a/db/migrate/20140502115131_add_repo_size_to_db.rb +++ b/db/migrate/20140502115131_add_repo_size_to_db.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRepoSizeToDb < ActiveRecord::Migration def change add_column :projects, :repository_size, :float, default: 0 diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb index efdf53112f..84463727b3 100644 --- a/db/migrate/20140502125220_migrate_repo_size.rb +++ b/db/migrate/20140502125220_migrate_repo_size.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateRepoSize < ActiveRecord::Migration def up project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id') diff --git a/db/migrate/20140611135229_add_position_to_merge_request.rb b/db/migrate/20140611135229_add_position_to_merge_request.rb index d5fdecd0c3..3a7d2f7c35 100644 --- a/db/migrate/20140611135229_add_position_to_merge_request.rb +++ b/db/migrate/20140611135229_add_position_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPositionToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :position, :integer, default: 0 diff --git a/db/migrate/20140625115202_create_users_star_projects.rb b/db/migrate/20140625115202_create_users_star_projects.rb index 412f0f6f34..32dd99e83b 100644 --- a/db/migrate/20140625115202_create_users_star_projects.rb +++ b/db/migrate/20140625115202_create_users_star_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateUsersStarProjects < ActiveRecord::Migration def change create_table :users_star_projects do |t| diff --git a/db/migrate/20140729134820_create_labels.rb b/db/migrate/20140729134820_create_labels.rb index 3a4b6a152d..df0f8cb9f0 100644 --- a/db/migrate/20140729134820_create_labels.rb +++ b/db/migrate/20140729134820_create_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateLabels < ActiveRecord::Migration def change create_table :labels do |t| diff --git a/db/migrate/20140729140420_create_label_links.rb b/db/migrate/20140729140420_create_label_links.rb index 2bfc4ae209..fa5992605f 100644 --- a/db/migrate/20140729140420_create_label_links.rb +++ b/db/migrate/20140729140420_create_label_links.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateLabelLinks < ActiveRecord::Migration def change create_table :label_links do |t| diff --git a/db/migrate/20140729145339_migrate_project_tags.rb b/db/migrate/20140729145339_migrate_project_tags.rb index 5760e4bfea..ac46847f3e 100644 --- a/db/migrate/20140729145339_migrate_project_tags.rb +++ b/db/migrate/20140729145339_migrate_project_tags.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateProjectTags < ActiveRecord::Migration def up ActsAsTaggableOn::Tagging.where(taggable_type: 'Project', context: 'labels').update_all(context: 'tags') diff --git a/db/migrate/20140729152420_migrate_taggable_labels.rb b/db/migrate/20140729152420_migrate_taggable_labels.rb index dc28d727d9..04cdc6bead 100644 --- a/db/migrate/20140729152420_migrate_taggable_labels.rb +++ b/db/migrate/20140729152420_migrate_taggable_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateTaggableLabels < ActiveRecord::Migration def up taggings = ActsAsTaggableOn::Tagging.where(taggable_type: ['Issue', 'MergeRequest'], context: 'labels') diff --git a/db/migrate/20140730111702_add_index_to_labels.rb b/db/migrate/20140730111702_add_index_to_labels.rb index 494241c873..cc7ac1fc44 100644 --- a/db/migrate/20140730111702_add_index_to_labels.rb +++ b/db/migrate/20140730111702_add_index_to_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToLabels < ActiveRecord::Migration def change add_index "labels", :project_id diff --git a/db/migrate/20140903115954_migrate_to_new_shell.rb b/db/migrate/20140903115954_migrate_to_new_shell.rb index 54cbe48960..04acf24284 100644 --- a/db/migrate/20140903115954_migrate_to_new_shell.rb +++ b/db/migrate/20140903115954_migrate_to_new_shell.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateToNewShell < ActiveRecord::Migration def change return if Rails.env.test? diff --git a/db/migrate/20140907220153_serialize_service_properties.rb b/db/migrate/20140907220153_serialize_service_properties.rb index d45a10465b..c2d67fad0a 100644 --- a/db/migrate/20140907220153_serialize_service_properties.rb +++ b/db/migrate/20140907220153_serialize_service_properties.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class SerializeServiceProperties < ActiveRecord::Migration def change unless column_exists?(:services, :properties) diff --git a/db/migrate/20140914113604_add_members_table.rb b/db/migrate/20140914113604_add_members_table.rb index d311f3033e..bc3c1bb61e 100644 --- a/db/migrate/20140914113604_add_members_table.rb +++ b/db/migrate/20140914113604_add_members_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMembersTable < ActiveRecord::Migration def change create_table :members do |t| diff --git a/db/migrate/20140914145549_migrate_to_new_members_model.rb b/db/migrate/20140914145549_migrate_to_new_members_model.rb index 2a5a49c724..b4c98f016d 100644 --- a/db/migrate/20140914145549_migrate_to_new_members_model.rb +++ b/db/migrate/20140914145549_migrate_to_new_members_model.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateToNewMembersModel < ActiveRecord::Migration def up execute "INSERT INTO members ( user_id, source_id, source_type, access_level, notification_level, type ) SELECT user_id, group_id, 'Namespace', group_access, notification_level, 'GroupMember' FROM users_groups" diff --git a/db/migrate/20140914173417_remove_old_member_tables.rb b/db/migrate/20140914173417_remove_old_member_tables.rb index 408b9551db..aff8e94e5b 100644 --- a/db/migrate/20140914173417_remove_old_member_tables.rb +++ b/db/migrate/20140914173417_remove_old_member_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveOldMemberTables < ActiveRecord::Migration def up drop_table :users_groups diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index 5836cd6b8d..8cb120f700 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MoveSlackServiceToWebhook < ActiveRecord::Migration def change SlackService.all.each do |slack_service| diff --git a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb index 93826185e8..688d857847 100644 --- a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb +++ b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddVisibilityLevelToSnippet < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20141118150935_add_audit_event.rb b/db/migrate/20141118150935_add_audit_event.rb index 07383c6bbc..3884228456 100644 --- a/db/migrate/20141118150935_add_audit_event.rb +++ b/db/migrate/20141118150935_add_audit_event.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAuditEvent < ActiveRecord::Migration def change create_table :audit_events do |t| diff --git a/db/migrate/20141121133009_add_timestamps_to_members.rb b/db/migrate/20141121133009_add_timestamps_to_members.rb index ef6d4dedf3..68f164cd35 100644 --- a/db/migrate/20141121133009_add_timestamps_to_members.rb +++ b/db/migrate/20141121133009_add_timestamps_to_members.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # In 20140914145549_migrate_to_new_members_model.rb we forgot to set the # created_at and updated_at times for new records in the 'members' table. This # became a problem after commit c8e78d972a5a628870eefca0f2ccea0199c55bda which diff --git a/db/migrate/20141121161704_add_identity_table.rb b/db/migrate/20141121161704_add_identity_table.rb index a85b0426ce..5a399f0d32 100644 --- a/db/migrate/20141121161704_add_identity_table.rb +++ b/db/migrate/20141121161704_add_identity_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIdentityTable < ActiveRecord::Migration def up create_table :identities do |t| diff --git a/db/migrate/20141205134006_add_locked_at_to_merge_request.rb b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb index 49651c44a8..5aa91c7587 100644 --- a/db/migrate/20141205134006_add_locked_at_to_merge_request.rb +++ b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLockedAtToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :locked_at, :datetime diff --git a/db/migrate/20141216155758_create_doorkeeper_tables.rb b/db/migrate/20141216155758_create_doorkeeper_tables.rb index af5aa7d8b7..b323ffe96f 100644 --- a/db/migrate/20141216155758_create_doorkeeper_tables.rb +++ b/db/migrate/20141216155758_create_doorkeeper_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateDoorkeeperTables < ActiveRecord::Migration def change create_table :oauth_applications do |t| diff --git a/db/migrate/20141217125223_add_owner_to_application.rb b/db/migrate/20141217125223_add_owner_to_application.rb index 7d5e6d07d0..e5a669ab4d 100644 --- a/db/migrate/20141217125223_add_owner_to_application.rb +++ b/db/migrate/20141217125223_add_owner_to_application.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddOwnerToApplication < ActiveRecord::Migration def change add_column :oauth_applications, :owner_id, :integer, null: true diff --git a/db/migrate/20141223135007_add_import_data_to_project_table.rb b/db/migrate/20141223135007_add_import_data_to_project_table.rb index 5db78f94cc..9c8a483e4d 100644 --- a/db/migrate/20141223135007_add_import_data_to_project_table.rb +++ b/db/migrate/20141223135007_add_import_data_to_project_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportDataToProjectTable < ActiveRecord::Migration def change add_column :projects, :import_type, :string diff --git a/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb b/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb index 70e7272f7f..a18b2f4974 100644 --- a/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb +++ b/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDevelopersCanPushToProtectedBranches < ActiveRecord::Migration def change add_column :protected_branches, :developers_can_push, :boolean, default: false, null: false diff --git a/db/migrate/20150108073740_create_application_settings.rb b/db/migrate/20150108073740_create_application_settings.rb index 651e35fdf7..dfa2f76535 100644 --- a/db/migrate/20150108073740_create_application_settings.rb +++ b/db/migrate/20150108073740_create_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateApplicationSettings < ActiveRecord::Migration def change create_table :application_settings do |t| diff --git a/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb b/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb index aa179ce3a4..10e6549c72 100644 --- a/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb +++ b/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHomePageUrlForApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :home_page_url, :string diff --git a/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb index c28ba3197a..e083973615 100644 --- a/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb +++ b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddGitlabAccessTokenToUser < ActiveRecord::Migration def change add_column :users, :gitlab_access_token, :string diff --git a/db/migrate/20150125163100_add_default_branch_protection_setting.rb b/db/migrate/20150125163100_add_default_branch_protection_setting.rb index 5020daf55f..7ca3116d35 100644 --- a/db/migrate/20150125163100_add_default_branch_protection_setting.rb +++ b/db/migrate/20150125163100_add_default_branch_protection_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDefaultBranchProtectionSetting < ActiveRecord::Migration def change add_column :application_settings, :default_branch_protection, :integer, :default => 2 diff --git a/db/migrate/20150205211843_add_timestamps_to_identities.rb b/db/migrate/20150205211843_add_timestamps_to_identities.rb index 77cddbfec3..a78e28eb4e 100644 --- a/db/migrate/20150205211843_add_timestamps_to_identities.rb +++ b/db/migrate/20150205211843_add_timestamps_to_identities.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTimestampsToIdentities < ActiveRecord::Migration def change add_timestamps(:identities) diff --git a/db/migrate/20150206181414_add_index_to_created_at.rb b/db/migrate/20150206181414_add_index_to_created_at.rb index fc624fca60..a161fad79d 100644 --- a/db/migrate/20150206181414_add_index_to_created_at.rb +++ b/db/migrate/20150206181414_add_index_to_created_at.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToCreatedAt < ActiveRecord::Migration def change add_index "users", [:created_at, :id] diff --git a/db/migrate/20150206222854_add_notification_email_to_user.rb b/db/migrate/20150206222854_add_notification_email_to_user.rb index ab80f7e582..ebae092cac 100644 --- a/db/migrate/20150206222854_add_notification_email_to_user.rb +++ b/db/migrate/20150206222854_add_notification_email_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationEmailToUser < ActiveRecord::Migration def up add_column :users, :notification_email, :string diff --git a/db/migrate/20150209222013_add_missing_index.rb b/db/migrate/20150209222013_add_missing_index.rb index a816c2e9e8..18e3ac2cbb 100644 --- a/db/migrate/20150209222013_add_missing_index.rb +++ b/db/migrate/20150209222013_add_missing_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMissingIndex < ActiveRecord::Migration def change add_index "services", [:created_at, :id] diff --git a/db/migrate/20150211172122_add_template_to_service.rb b/db/migrate/20150211172122_add_template_to_service.rb index b1bfbc45ee..a3e96b25c5 100644 --- a/db/migrate/20150211172122_add_template_to_service.rb +++ b/db/migrate/20150211172122_add_template_to_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTemplateToService < ActiveRecord::Migration def change add_column :services, :template, :boolean, default: false diff --git a/db/migrate/20150211174341_allow_null_in_services_project_id.rb b/db/migrate/20150211174341_allow_null_in_services_project_id.rb index 68f0281279..fea95c79ad 100644 --- a/db/migrate/20150211174341_allow_null_in_services_project_id.rb +++ b/db/migrate/20150211174341_allow_null_in_services_project_id.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AllowNullInServicesProjectId < ActiveRecord::Migration def change change_column :services, :project_id, :integer, null: true diff --git a/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb b/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb index a043917239..334020376e 100644 --- a/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb +++ b/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTwitterSharingEnabledToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :twitter_sharing_enabled, :boolean, default: true diff --git a/db/migrate/20150213114800_add_hide_no_password_to_user.rb b/db/migrate/20150213114800_add_hide_no_password_to_user.rb index 685f084427..a2af3510b9 100644 --- a/db/migrate/20150213114800_add_hide_no_password_to_user.rb +++ b/db/migrate/20150213114800_add_hide_no_password_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHideNoPasswordToUser < ActiveRecord::Migration def change add_column :users, :hide_no_password, :boolean, default: false diff --git a/db/migrate/20150213121042_add_password_automatically_set_to_user.rb b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb index c3c7c1ffc7..4e84a13f0d 100644 --- a/db/migrate/20150213121042_add_password_automatically_set_to_user.rb +++ b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPasswordAutomaticallySetToUser < ActiveRecord::Migration def change add_column :users, :password_automatically_set, :boolean, default: false diff --git a/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb index 23ac1b399e..78e9fd0c3a 100644 --- a/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb +++ b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddBitbucketAccessTokenAndSecretToUser < ActiveRecord::Migration def change add_column :users, :bitbucket_access_token, :string diff --git a/db/migrate/20150219004514_add_events_to_services.rb b/db/migrate/20150219004514_add_events_to_services.rb index cf73a0174f..560382c3fa 100644 --- a/db/migrate/20150219004514_add_events_to_services.rb +++ b/db/migrate/20150219004514_add_events_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEventsToServices < ActiveRecord::Migration def change add_column :services, :push_events, :boolean, :default => true diff --git a/db/migrate/20150223022001_set_missing_last_activity_at.rb b/db/migrate/20150223022001_set_missing_last_activity_at.rb index 3f6d4d8347..300381ad65 100644 --- a/db/migrate/20150223022001_set_missing_last_activity_at.rb +++ b/db/migrate/20150223022001_set_missing_last_activity_at.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class SetMissingLastActivityAt < ActiveRecord::Migration def up execute "UPDATE projects SET last_activity_at = updated_at WHERE last_activity_at IS NULL" diff --git a/db/migrate/20150225065047_add_note_events_to_services.rb b/db/migrate/20150225065047_add_note_events_to_services.rb index d54ba9e482..7843cabc43 100644 --- a/db/migrate/20150225065047_add_note_events_to_services.rb +++ b/db/migrate/20150225065047_add_note_events_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNoteEventsToServices < ActiveRecord::Migration def change add_column :services, :note_events, :boolean, default: true, null: false diff --git a/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb index 494c3033bf..7d8d65ef2e 100644 --- a/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb +++ b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRestrictedVisibilityLevelsToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :restricted_visibility_levels, :text diff --git a/db/migrate/20150306023106_fix_namespace_duplication.rb b/db/migrate/20150306023106_fix_namespace_duplication.rb index 334e557455..ea53a9d71f 100644 --- a/db/migrate/20150306023106_fix_namespace_duplication.rb +++ b/db/migrate/20150306023106_fix_namespace_duplication.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixNamespaceDuplication < ActiveRecord::Migration def up #fixes path duplication diff --git a/db/migrate/20150306023112_add_unique_index_to_namespace.rb b/db/migrate/20150306023112_add_unique_index_to_namespace.rb index 6472138e3e..f293a9b643 100644 --- a/db/migrate/20150306023112_add_unique_index_to_namespace.rb +++ b/db/migrate/20150306023112_add_unique_index_to_namespace.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUniqueIndexToNamespace < ActiveRecord::Migration def change remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) diff --git a/db/migrate/20150310194358_add_version_check_to_application_settings.rb b/db/migrate/20150310194358_add_version_check_to_application_settings.rb index e9d42c1e74..5d3dae6e7d 100644 --- a/db/migrate/20150310194358_add_version_check_to_application_settings.rb +++ b/db/migrate/20150310194358_add_version_check_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddVersionCheckToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :version_check_enabled, :boolean, default: true diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb index a1d4d9dedc..8adb193b27 100644 --- a/db/migrate/20150313012111_create_subscriptions_table.rb +++ b/db/migrate/20150313012111_create_subscriptions_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateSubscriptionsTable < ActiveRecord::Migration def change create_table :subscriptions do |t| diff --git a/db/migrate/20150320234437_add_location_to_user.rb b/db/migrate/20150320234437_add_location_to_user.rb index 32731d37d7..df04657036 100644 --- a/db/migrate/20150320234437_add_location_to_user.rb +++ b/db/migrate/20150320234437_add_location_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLocationToUser < ActiveRecord::Migration def change add_column :users, :location, :string diff --git a/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb index 42dc8173e4..9f8b6f4bd5 100644 --- a/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb +++ b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class SetIncorrectAssigneeIdToNull < ActiveRecord::Migration def up execute "UPDATE issues SET assignee_id = NULL WHERE assignee_id = -1" diff --git a/db/migrate/20150327122227_add_public_to_key.rb b/db/migrate/20150327122227_add_public_to_key.rb index 6ffbf4cda1..33c20d65e0 100644 --- a/db/migrate/20150327122227_add_public_to_key.rb +++ b/db/migrate/20150327122227_add_public_to_key.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPublicToKey < ActiveRecord::Migration def change add_column :keys, :public, :boolean, default: false, null: false diff --git a/db/migrate/20150327150017_add_import_data_to_project.rb b/db/migrate/20150327150017_add_import_data_to_project.rb index 12c00339ee..67b1554dfd 100644 --- a/db/migrate/20150327150017_add_import_data_to_project.rb +++ b/db/migrate/20150327150017_add_import_data_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportDataToProject < ActiveRecord::Migration def change add_column :projects, :import_data, :text diff --git a/db/migrate/20150327223628_add_devise_two_factor_to_users.rb b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb index 11b026ee8f..eccb0123e7 100644 --- a/db/migrate/20150327223628_add_devise_two_factor_to_users.rb +++ b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDeviseTwoFactorToUsers < ActiveRecord::Migration def change add_column :users, :encrypted_otp_secret, :string diff --git a/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb index 1d161674a9..4c56a2fb78 100644 --- a/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb +++ b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMaxAttachmentSizeToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :max_attachment_size, :integer, default: 10, null: false diff --git a/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb index 913958db7c..fdb6d72917 100644 --- a/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb +++ b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration def change add_column :users, :otp_backup_codes, :text diff --git a/db/migrate/20150406133311_add_invite_data_to_member.rb b/db/migrate/20150406133311_add_invite_data_to_member.rb index 5d3e856ddc..63d0f184f3 100644 --- a/db/migrate/20150406133311_add_invite_data_to_member.rb +++ b/db/migrate/20150406133311_add_invite_data_to_member.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddInviteDataToMember < ActiveRecord::Migration def up add_column :members, :created_by_id, :integer diff --git a/db/migrate/20150411000035_fix_identities.rb b/db/migrate/20150411000035_fix_identities.rb index d9051f9fff..a10fcc001f 100644 --- a/db/migrate/20150411000035_fix_identities.rb +++ b/db/migrate/20150411000035_fix_identities.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixIdentities < ActiveRecord::Migration def up # Up until now, legacy 'ldap' references in the database were charitably diff --git a/db/migrate/20150411180045_rename_buildbox_service.rb b/db/migrate/20150411180045_rename_buildbox_service.rb index 5a0b5d07e5..9f3b25c397 100644 --- a/db/migrate/20150411180045_rename_buildbox_service.rb +++ b/db/migrate/20150411180045_rename_buildbox_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameBuildboxService < ActiveRecord::Migration def up execute "UPDATE services SET type = 'BuildkiteService' WHERE type = 'BuildboxService';" diff --git a/db/migrate/20150413192223_add_public_email_to_users.rb b/db/migrate/20150413192223_add_public_email_to_users.rb index 700e9f343a..0fed5eaf46 100644 --- a/db/migrate/20150413192223_add_public_email_to_users.rb +++ b/db/migrate/20150413192223_add_public_email_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPublicEmailToUsers < ActiveRecord::Migration def change add_column :users, :public_email, :string, default: "", null: false diff --git a/db/migrate/20150417121913_create_project_import_data.rb b/db/migrate/20150417121913_create_project_import_data.rb index c78f5fde85..fc357cbacc 100644 --- a/db/migrate/20150417121913_create_project_import_data.rb +++ b/db/migrate/20150417121913_create_project_import_data.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateProjectImportData < ActiveRecord::Migration def change create_table :project_import_data do |t| diff --git a/db/migrate/20150417122318_remove_import_data_from_project.rb b/db/migrate/20150417122318_remove_import_data_from_project.rb index 46cf63593c..5a008218fa 100644 --- a/db/migrate/20150417122318_remove_import_data_from_project.rb +++ b/db/migrate/20150417122318_remove_import_data_from_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveImportDataFromProject < ActiveRecord::Migration def up remove_column :projects, :import_data diff --git a/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb index 3057ea3c68..3445e9ce59 100644 --- a/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb +++ b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemovePeriodsAtEndsOfUsernames < ActiveRecord::Migration include Gitlab::ShellAdapter diff --git a/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb index 50a9b2439e..129ce4d04a 100644 --- a/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb +++ b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDefaultProjectVisibililtyToApplicationSettings < ActiveRecord::Migration def up add_column :application_settings, :default_project_visibility, :integer diff --git a/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb index 281c88d2a7..8f352414ff 100644 --- a/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration is a duplicate of 20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb # It shold be applied before the index additions to ensure that `name` is case sensitive. diff --git a/db/migrate/20150425164647_remove_duplicate_tags.rb b/db/migrate/20150425164647_remove_duplicate_tags.rb index 13e5038db9..e77623bf50 100644 --- a/db/migrate/20150425164647_remove_duplicate_tags.rb +++ b/db/migrate/20150425164647_remove_duplicate_tags.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveDuplicateTags < ActiveRecord::Migration def up select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag| diff --git a/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb index c1b7868151..cbff98cdbc 100644 --- a/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration comes from acts_as_taggable_on_engine (originally 2) class AddMissingUniqueIndices < ActiveRecord::Migration def self.up diff --git a/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb index 8edb508078..1568d2dd4c 100644 --- a/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration comes from acts_as_taggable_on_engine (originally 3) class AddTaggingsCounterCacheToTags < ActiveRecord::Migration def self.up diff --git a/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb index 71f2d7f433..88829b8771 100644 --- a/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration comes from acts_as_taggable_on_engine (originally 4) class AddMissingTaggableIndex < ActiveRecord::Migration def self.up diff --git a/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb index bfb06bc7cd..642c474532 100644 --- a/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration comes from acts_as_taggable_on_engine (originally 5) # This migration is added to circumvent issue #623 and have special characters # work properly diff --git a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb index 8f1b0cc893..dd13def417 100644 --- a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb +++ b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDefaultSnippetVisibilityToAppSettings < ActiveRecord::Migration def up add_column :application_settings, :default_snippet_visibility, :integer diff --git a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb index 244637e1c4..d2c7f3c442 100644 --- a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb +++ b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveAbandonedGroupMembersRecords < ActiveRecord::Migration def up execute("DELETE FROM members WHERE type = 'GroupMember' AND source_id NOT IN(\ diff --git a/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb index 184e265361..b63ea9aec7 100644 --- a/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb +++ b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRestrictedSignupDomainsToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :restricted_signup_domains, :text diff --git a/db/migrate/20150509180749_convert_legacy_reference_notes.rb b/db/migrate/20150509180749_convert_legacy_reference_notes.rb index b02605489b..cd8bf90108 100644 --- a/db/migrate/20150509180749_convert_legacy_reference_notes.rb +++ b/db/migrate/20150509180749_convert_legacy_reference_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Convert legacy Markdown-emphasized notes to the current, non-emphasized format # # _mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666_ diff --git a/db/migrate/20150516060434_add_note_events_to_web_hooks.rb b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb index 0097587b4f..bf72e5e2e3 100644 --- a/db/migrate/20150516060434_add_note_events_to_web_hooks.rb +++ b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNoteEventsToWebHooks < ActiveRecord::Migration def up add_column :web_hooks, :note_events, :boolean, default: false, null: false diff --git a/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb index 6a78294f0b..9b02eda56a 100644 --- a/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb +++ b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUserOauthApplicationsToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :user_oauth_applications, :bool, default: true diff --git a/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb index 83e0810140..833c36de52 100644 --- a/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb +++ b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAfterSignOutPathForApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :after_sign_out_path, :string diff --git a/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb index 61ff0af41f..1f5cf1fe5f 100644 --- a/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb +++ b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSessionExpireDelayForApplicationSettings < ActiveRecord::Migration def change unless column_exists?(:application_settings, :session_expire_delay) diff --git a/db/migrate/20150610065936_add_dashboard_to_users.rb b/db/migrate/20150610065936_add_dashboard_to_users.rb index 2628e45072..df38472f89 100644 --- a/db/migrate/20150610065936_add_dashboard_to_users.rb +++ b/db/migrate/20150610065936_add_dashboard_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDashboardToUsers < ActiveRecord::Migration def up add_column :users, :dashboard, :integer, default: 0 diff --git a/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb b/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb index 8eed8678b2..da0fd457a3 100644 --- a/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb +++ b/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDefaultOtpRequiredForLoginValue < ActiveRecord::Migration def up execute %q{UPDATE users SET otp_required_for_login = FALSE WHERE otp_required_for_login IS NULL} diff --git a/db/migrate/20150713160110_add_project_view_to_users.rb b/db/migrate/20150713160110_add_project_view_to_users.rb index fe3d206df8..0de5a93035 100644 --- a/db/migrate/20150713160110_add_project_view_to_users.rb +++ b/db/migrate/20150713160110_add_project_view_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectViewToUsers < ActiveRecord::Migration def change add_column :users, :project_view, :integer, default: 0 diff --git a/db/migrate/20150717130904_add_commits_count_to_project.rb b/db/migrate/20150717130904_add_commits_count_to_project.rb index 9b46daa593..5799e068c6 100644 --- a/db/migrate/20150717130904_add_commits_count_to_project.rb +++ b/db/migrate/20150717130904_add_commits_count_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCommitsCountToProject < ActiveRecord::Migration def change add_column :projects, :commit_count, :integer, default: 0 diff --git a/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb index 78d45c7f96..be30e881c7 100644 --- a/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb +++ b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUpdatedByToIssuablesAndNotes < ActiveRecord::Migration def change add_column :notes, :updated_by_id, :integer diff --git a/db/migrate/20150806104937_create_abuse_reports.rb b/db/migrate/20150806104937_create_abuse_reports.rb index e97dc4cf04..3c749b5d9a 100644 --- a/db/migrate/20150806104937_create_abuse_reports.rb +++ b/db/migrate/20150806104937_create_abuse_reports.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateAbuseReports < ActiveRecord::Migration def change create_table :abuse_reports do |t| diff --git a/db/migrate/20150812080800_add_settings_import_sources.rb b/db/migrate/20150812080800_add_settings_import_sources.rb index 276d2fdb2b..07f417fa3e 100644 --- a/db/migrate/20150812080800_add_settings_import_sources.rb +++ b/db/migrate/20150812080800_add_settings_import_sources.rb @@ -1,3 +1,4 @@ +# rubocop:disable all require 'yaml' class AddSettingsImportSources < ActiveRecord::Migration diff --git a/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb b/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb index de2078a926..7eaa7eda31 100644 --- a/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb +++ b/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveOauthTokensFromUsers < ActiveRecord::Migration def change remove_column :users, :github_access_token, :string diff --git a/db/migrate/20150817163600_deduplicate_user_identities.rb b/db/migrate/20150817163600_deduplicate_user_identities.rb index fceffc4801..b0cfad7d20 100644 --- a/db/migrate/20150817163600_deduplicate_user_identities.rb +++ b/db/migrate/20150817163600_deduplicate_user_identities.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class DeduplicateUserIdentities < ActiveRecord::Migration def change execute 'DROP TABLE IF EXISTS tt_migration_DeduplicateUserIdentities;' diff --git a/db/migrate/20150818213832_add_sent_notifications.rb b/db/migrate/20150818213832_add_sent_notifications.rb index 43e8d6a1a8..fa0c3ce0ac 100644 --- a/db/migrate/20150818213832_add_sent_notifications.rb +++ b/db/migrate/20150818213832_add_sent_notifications.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSentNotifications < ActiveRecord::Migration def change create_table :sent_notifications do |t| diff --git a/db/migrate/20150824002011_add_enable_ssl_verification.rb b/db/migrate/20150824002011_add_enable_ssl_verification.rb index 093c068fbd..6e992f0883 100644 --- a/db/migrate/20150824002011_add_enable_ssl_verification.rb +++ b/db/migrate/20150824002011_add_enable_ssl_verification.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEnableSslVerification < ActiveRecord::Migration def change add_column :web_hooks, :enable_ssl_verification, :boolean, default: false diff --git a/db/migrate/20150826001931_add_ci_tables.rb b/db/migrate/20150826001931_add_ci_tables.rb index c4f51363e5..d1f8506d1f 100644 --- a/db/migrate/20150826001931_add_ci_tables.rb +++ b/db/migrate/20150826001931_add_ci_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiTables < ActiveRecord::Migration def change create_table "ci_application_settings", force: true do |t| diff --git a/db/migrate/20150902001023_add_template_to_label.rb b/db/migrate/20150902001023_add_template_to_label.rb index bd381a97b6..0f6ae8d6cc 100644 --- a/db/migrate/20150902001023_add_template_to_label.rb +++ b/db/migrate/20150902001023_add_template_to_label.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTemplateToLabel < ActiveRecord::Migration def change add_column :labels, :template, :boolean, default: false diff --git a/db/migrate/20150914215247_add_ci_tags.rb b/db/migrate/20150914215247_add_ci_tags.rb index df3390e8a8..b647bc9c8a 100644 --- a/db/migrate/20150914215247_add_ci_tags.rb +++ b/db/migrate/20150914215247_add_ci_tags.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiTags < ActiveRecord::Migration def change create_table "ci_taggings", force: true do |t| diff --git a/db/migrate/20150915001905_enable_ssl_verification_by_default.rb b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb index 6e924262a1..3f07013941 100644 --- a/db/migrate/20150915001905_enable_ssl_verification_by_default.rb +++ b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class EnableSslVerificationByDefault < ActiveRecord::Migration def change change_column :web_hooks, :enable_ssl_verification, :boolean, default: true diff --git a/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb index 90ce6c2db3..ea2ab6e409 100644 --- a/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb +++ b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class EnableSslVerificationForWebHooks < ActiveRecord::Migration def up execute("UPDATE web_hooks SET enable_ssl_verification = true") diff --git a/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb index 37a27f1193..a504f25b1b 100644 --- a/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb +++ b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHelpPageTextToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :help_page_text, :text diff --git a/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb index 78d9e5f61a..a18ed93cf3 100644 --- a/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb +++ b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexForCommittedAtAndId < ActiveRecord::Migration def change add_index :ci_commits, [:project_id, :committed_at, :id] diff --git a/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb index 6cf668a170..c9b6e03512 100644 --- a/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb +++ b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiEnabledToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :ci_enabled, :boolean, null: false, default: true diff --git a/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb index 0aad6fe5e6..e1818b566d 100644 --- a/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb +++ b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveInvalidMilestonesFromMergeRequests < ActiveRecord::Migration def up execute("UPDATE merge_requests SET milestone_id = NULL where milestone_id NOT IN (SELECT id FROM milestones)") diff --git a/db/migrate/20150920010715_add_consumed_timestep_to_users.rb b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb index c8438b3f6a..e6975f5b9f 100644 --- a/db/migrate/20150920010715_add_consumed_timestep_to_users.rb +++ b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddConsumedTimestepToUsers < ActiveRecord::Migration def change add_column :users, :consumed_timestep, :integer diff --git a/db/migrate/20150920161119_add_line_code_to_sent_notification.rb b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb index d9af4e7175..1bcb06e4bd 100644 --- a/db/migrate/20150920161119_add_line_code_to_sent_notification.rb +++ b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLineCodeToSentNotification < ActiveRecord::Migration def change add_column :sent_notifications, :line_code, :string diff --git a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb index 1a761fe0f8..905332b7dc 100644 --- a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb +++ b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectIdToCiCommit < ActiveRecord::Migration def up add_column :ci_commits, :gl_project_id, :integer diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb index 2be57b6062..fb0e0ba1fa 100644 --- a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb +++ b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateProjectIdForCiCommits < ActiveRecord::Migration def up subquery = 'SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = ci_commits.project_id' diff --git a/db/migrate/20150930001110_merge_request_error_field.rb b/db/migrate/20150930001110_merge_request_error_field.rb index c2ee498ef3..71a8ae3938 100644 --- a/db/migrate/20150930001110_merge_request_error_field.rb +++ b/db/migrate/20150930001110_merge_request_error_field.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MergeRequestErrorField < ActiveRecord::Migration def up add_column :merge_requests, :merge_error, :string diff --git a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb index 8d47dac644..229c9942b5 100644 --- a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb +++ b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNullToNameForCiProjects < ActiveRecord::Migration def up change_column_null :ci_projects, :name, true diff --git a/db/migrate/20150930110012_add_group_share_lock.rb b/db/migrate/20150930110012_add_group_share_lock.rb index 78d1a4538f..96938bf9ab 100644 --- a/db/migrate/20150930110012_add_group_share_lock.rb +++ b/db/migrate/20150930110012_add_group_share_lock.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddGroupShareLock < ActiveRecord::Migration def change add_column :namespaces, :share_with_group_lock, :boolean, default: false diff --git a/db/migrate/20151002112914_add_stage_idx_to_builds.rb b/db/migrate/20151002112914_add_stage_idx_to_builds.rb index 68a745ffef..4297ba0e7c 100644 --- a/db/migrate/20151002112914_add_stage_idx_to_builds.rb +++ b/db/migrate/20151002112914_add_stage_idx_to_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStageIdxToBuilds < ActiveRecord::Migration def change add_column :ci_builds, :stage_idx, :integer diff --git a/db/migrate/20151002121400_add_index_for_builds.rb b/db/migrate/20151002121400_add_index_for_builds.rb index 4ffc136391..bd945c5454 100644 --- a/db/migrate/20151002121400_add_index_for_builds.rb +++ b/db/migrate/20151002121400_add_index_for_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexForBuilds < ActiveRecord::Migration def up add_index :ci_builds, [:commit_id, :stage_idx, :created_at] diff --git a/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb index e3d2ac1cea..3c0fcf6c45 100644 --- a/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb +++ b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRefAndTagToBuilds < ActiveRecord::Migration def change add_column :ci_builds, :tag, :boolean diff --git a/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb index 01d7b3f677..52217ce5af 100644 --- a/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb +++ b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateRefAndTagToBuild < ActiveRecord::Migration def change execute('UPDATE ci_builds SET ref=(SELECT ref FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE ref IS NULL') diff --git a/db/migrate/20151005075649_add_user_id_to_build.rb b/db/migrate/20151005075649_add_user_id_to_build.rb index 0f4b92b8b7..be9d403e00 100644 --- a/db/migrate/20151005075649_add_user_id_to_build.rb +++ b/db/migrate/20151005075649_add_user_id_to_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUserIdToBuild < ActiveRecord::Migration def change add_column :ci_builds, :user_id, :integer diff --git a/db/migrate/20151005150751_add_layout_option_for_users.rb b/db/migrate/20151005150751_add_layout_option_for_users.rb index ead9b1f897..7e68606969 100644 --- a/db/migrate/20151005150751_add_layout_option_for_users.rb +++ b/db/migrate/20151005150751_add_layout_option_for_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLayoutOptionForUsers < ActiveRecord::Migration def change add_column :users, :layout, :integer, default: 0 diff --git a/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb index be6aa810bb..07dba59874 100644 --- a/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb +++ b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveCiEnabledFromApplicationSettings < ActiveRecord::Migration def change remove_column :application_settings, :ci_enabled, :boolean, null: false, default: true diff --git a/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb index 7f6cd6d5a7..38208e5980 100644 --- a/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb +++ b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class NamespacesProjectsPathLowerIndexes < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb index 2f2dc77678..6080d2a0fc 100644 --- a/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb +++ b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20151008123042_add_type_and_description_to_builds.rb b/db/migrate/20151008123042_add_type_and_description_to_builds.rb index c72b1c611c..a19eb6c6c4 100644 --- a/db/migrate/20151008123042_add_type_and_description_to_builds.rb +++ b/db/migrate/20151008123042_add_type_and_description_to_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTypeAndDescriptionToBuilds < ActiveRecord::Migration def change add_column :ci_builds, :type, :string diff --git a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb index f5c44babd8..306fa7092e 100644 --- a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb +++ b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateNameToDescriptionForBuilds < ActiveRecord::Migration def change execute("UPDATE ci_builds SET type='Ci::Build' WHERE type IS NULL") diff --git a/db/migrate/20151008143519_add_admin_notification_email_setting.rb b/db/migrate/20151008143519_add_admin_notification_email_setting.rb index 0bb581efe2..f48ec9aa4a 100644 --- a/db/migrate/20151008143519_add_admin_notification_email_setting.rb +++ b/db/migrate/20151008143519_add_admin_notification_email_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAdminNotificationEmailSetting < ActiveRecord::Migration def change add_column :application_settings, :admin_notification_email, :string diff --git a/db/migrate/20151012173029_set_jira_service_api_url.rb b/db/migrate/20151012173029_set_jira_service_api_url.rb index 2af99e0db0..2b6f61428c 100644 --- a/db/migrate/20151012173029_set_jira_service_api_url.rb +++ b/db/migrate/20151012173029_set_jira_service_api_url.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class SetJiraServiceApiUrl < ActiveRecord::Migration # This migration can be performed online without errors, but some Jira API calls may be missed # when doing so because api_url is not yet available. diff --git a/db/migrate/20151013092124_add_artifacts_file_to_builds.rb b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb index 5a299f7b26..a54ac9d57a 100644 --- a/db/migrate/20151013092124_add_artifacts_file_to_builds.rb +++ b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddArtifactsFileToBuilds < ActiveRecord::Migration def change add_column :ci_builds, :artifacts_file, :text diff --git a/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb index 52a47aa9c5..eb3351eb76 100644 --- a/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb +++ b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiProjectsGlProjectIdIndex < ActiveRecord::Migration def change add_index :ci_commits, :gl_project_id diff --git a/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb index 7f1af1c758..899e004d61 100644 --- a/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb +++ b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiBuildsAndProjectsIndexes < ActiveRecord::Migration def change add_index :ci_projects, :gitlab_id diff --git a/db/migrate/20151016195706_add_notes_line_code_index.rb b/db/migrate/20151016195706_add_notes_line_code_index.rb index aeeb1a759f..3298630c1e 100644 --- a/db/migrate/20151016195706_add_notes_line_code_index.rb +++ b/db/migrate/20151016195706_add_notes_line_code_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotesLineCodeIndex < ActiveRecord::Migration def change add_index :notes, :line_code diff --git a/db/migrate/20151019111551_fix_build_tags.rb b/db/migrate/20151019111551_fix_build_tags.rb index 299a24b0a7..8c05acfc19 100644 --- a/db/migrate/20151019111551_fix_build_tags.rb +++ b/db/migrate/20151019111551_fix_build_tags.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixBuildTags < ActiveRecord::Migration def up execute("UPDATE taggings SET taggable_type='CommitStatus' WHERE taggable_type='Ci::Build'") diff --git a/db/migrate/20151019111703_fail_build_without_names.rb b/db/migrate/20151019111703_fail_build_without_names.rb index dcdb5d1b25..362e31eb43 100644 --- a/db/migrate/20151019111703_fail_build_without_names.rb +++ b/db/migrate/20151019111703_fail_build_without_names.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FailBuildWithoutNames < ActiveRecord::Migration def up execute("UPDATE ci_builds SET status='failed' WHERE name IS NULL AND status='pending'") diff --git a/db/migrate/20151020145526_add_services_template_index.rb b/db/migrate/20151020145526_add_services_template_index.rb index 1b04f31356..14ff07bd72 100644 --- a/db/migrate/20151020145526_add_services_template_index.rb +++ b/db/migrate/20151020145526_add_services_template_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddServicesTemplateIndex < ActiveRecord::Migration def change add_index :services, :template diff --git a/db/migrate/20151020173516_ci_limits_to_mysql.rb b/db/migrate/20151020173516_ci_limits_to_mysql.rb index 9bb960082f..5314611cbc 100644 --- a/db/migrate/20151020173516_ci_limits_to_mysql.rb +++ b/db/migrate/20151020173516_ci_limits_to_mysql.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CiLimitsToMysql < ActiveRecord::Migration def change return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ diff --git a/db/migrate/20151020173906_add_ci_builds_index_for_status.rb b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb index c3f0e0606d..81a31e46ff 100644 --- a/db/migrate/20151020173906_add_ci_builds_index_for_status.rb +++ b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiBuildsIndexForStatus < ActiveRecord::Migration def change add_index :ci_builds, [:commit_id, :status, :type] diff --git a/db/migrate/20151023112551_fail_build_with_empty_name.rb b/db/migrate/20151023112551_fail_build_with_empty_name.rb index 41c0f0649c..0666dfeaef 100644 --- a/db/migrate/20151023112551_fail_build_with_empty_name.rb +++ b/db/migrate/20151023112551_fail_build_with_empty_name.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FailBuildWithEmptyName < ActiveRecord::Migration def up execute("UPDATE ci_builds SET status='failed' WHERE (name IS NULL OR name='') AND status='pending'") diff --git a/db/migrate/20151023144219_remove_satellites.rb b/db/migrate/20151023144219_remove_satellites.rb index e73f300028..98fe0bd7d1 100644 --- a/db/migrate/20151023144219_remove_satellites.rb +++ b/db/migrate/20151023144219_remove_satellites.rb @@ -1,3 +1,4 @@ +# rubocop:disable all require 'fileutils' class RemoveSatellites < ActiveRecord::Migration diff --git a/db/migrate/20151026182941_add_project_path_index.rb b/db/migrate/20151026182941_add_project_path_index.rb index a62fe199d7..117f65c1a1 100644 --- a/db/migrate/20151026182941_add_project_path_index.rb +++ b/db/migrate/20151026182941_add_project_path_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectPathIndex < ActiveRecord::Migration def up add_index :projects, :path diff --git a/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb b/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb index ceb52f0c22..4a98966946 100644 --- a/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb +++ b/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMergeWhenBuildSucceedsToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :merge_params, :text diff --git a/db/migrate/20151103001141_add_public_to_group.rb b/db/migrate/20151103001141_add_public_to_group.rb index 635346300c..ba1f7c2783 100644 --- a/db/migrate/20151103001141_add_public_to_group.rb +++ b/db/migrate/20151103001141_add_public_to_group.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPublicToGroup < ActiveRecord::Migration def change add_column :namespaces, :public, :boolean, default: false diff --git a/db/migrate/20151103133339_add_shared_runners_setting.rb b/db/migrate/20151103133339_add_shared_runners_setting.rb index 4231dfd5c2..b5b34d4ca6 100644 --- a/db/migrate/20151103133339_add_shared_runners_setting.rb +++ b/db/migrate/20151103133339_add_shared_runners_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSharedRunnersSetting < ActiveRecord::Migration def up add_column :application_settings, :shared_runners_enabled, :boolean, default: true, null: false diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb index 2d04c170a8..745b52e2b2 100644 --- a/db/migrate/20151103134857_create_lfs_objects.rb +++ b/db/migrate/20151103134857_create_lfs_objects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateLfsObjects < ActiveRecord::Migration def change create_table :lfs_objects do |t| diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb index f3f58b931e..3178e85b89 100644 --- a/db/migrate/20151103134958_create_lfs_objects_projects.rb +++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateLfsObjectsProjects < ActiveRecord::Migration def change create_table :lfs_objects_projects do |t| diff --git a/db/migrate/20151104105513_add_file_to_lfs_objects.rb b/db/migrate/20151104105513_add_file_to_lfs_objects.rb index 7c57f3f0df..4e46ae8101 100644 --- a/db/migrate/20151104105513_add_file_to_lfs_objects.rb +++ b/db/migrate/20151104105513_add_file_to_lfs_objects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddFileToLfsObjects < ActiveRecord::Migration def change add_column :lfs_objects, :file, :string diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb index fe4608c666..145b8db148 100644 --- a/db/migrate/20151105094515_create_releases.rb +++ b/db/migrate/20151105094515_create_releases.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateReleases < ActiveRecord::Migration def change create_table :releases do |t| diff --git a/db/migrate/20151106000015_add_is_award_to_notes.rb b/db/migrate/20151106000015_add_is_award_to_notes.rb index 02b271637e..b463d939b7 100644 --- a/db/migrate/20151106000015_add_is_award_to_notes.rb +++ b/db/migrate/20151106000015_add_is_award_to_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIsAwardToNotes < ActiveRecord::Migration def change add_column :notes, :is_award, :boolean, default: false, null: false diff --git a/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb index 01d8c0f043..25106ace7e 100644 --- a/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb +++ b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMaxArtifactsSizeToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :max_artifacts_size, :integer, default: 100, null: false diff --git a/db/migrate/20151109134526_add_issues_state_index.rb b/db/migrate/20151109134526_add_issues_state_index.rb index 1c4d2e3017..7a9970e859 100644 --- a/db/migrate/20151109134526_add_issues_state_index.rb +++ b/db/migrate/20151109134526_add_issues_state_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIssuesStateIndex < ActiveRecord::Migration def change add_index :issues, :state diff --git a/db/migrate/20151109134916_add_projects_visibility_level_index.rb b/db/migrate/20151109134916_add_projects_visibility_level_index.rb index 600b4bafd9..471db437b1 100644 --- a/db/migrate/20151109134916_add_projects_visibility_level_index.rb +++ b/db/migrate/20151109134916_add_projects_visibility_level_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectsVisibilityLevelIndex < ActiveRecord::Migration def change add_index :projects, :visibility_level diff --git a/db/migrate/20151110125604_add_import_error_to_project.rb b/db/migrate/20151110125604_add_import_error_to_project.rb index 7fc990f8d0..793358c305 100644 --- a/db/migrate/20151110125604_add_import_error_to_project.rb +++ b/db/migrate/20151110125604_add_import_error_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportErrorToProject < ActiveRecord::Migration def change add_column :projects, :import_error, :text diff --git a/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb index d10f1f6e60..00a4c74ffb 100644 --- a/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb +++ b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexForLfsOidAndSize < ActiveRecord::Migration def change add_index :lfs_objects, :oid diff --git a/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb index 41b93da0a8..1f192544ea 100644 --- a/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb +++ b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUniqueForLfsOidIndex < ActiveRecord::Migration def change remove_index :lfs_objects, :oid diff --git a/db/migrate/20151118162244_add_projects_public_index.rb b/db/migrate/20151118162244_add_projects_public_index.rb index fded70e3c0..589f124c21 100644 --- a/db/migrate/20151118162244_add_projects_public_index.rb +++ b/db/migrate/20151118162244_add_projects_public_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectsPublicIndex < ActiveRecord::Migration def change add_index :namespaces, :public diff --git a/db/migrate/20151201203948_raise_hook_url_limit.rb b/db/migrate/20151201203948_raise_hook_url_limit.rb index 98a7fca6f6..c490b7ace0 100644 --- a/db/migrate/20151201203948_raise_hook_url_limit.rb +++ b/db/migrate/20151201203948_raise_hook_url_limit.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RaiseHookUrlLimit < ActiveRecord::Migration def change change_column :web_hooks, :url, :string, limit: 2000 diff --git a/db/migrate/20151203162133_add_hide_project_limit_to_users.rb b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb index 6ffadfa189..5dc6d8bf44 100644 --- a/db/migrate/20151203162133_add_hide_project_limit_to_users.rb +++ b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHideProjectLimitToUsers < ActiveRecord::Migration def change add_column :users, :hide_project_limit, :boolean, default: false diff --git a/db/migrate/20151203162134_add_build_events_to_services.rb b/db/migrate/20151203162134_add_build_events_to_services.rb index c5542cb864..455882e5ec 100644 --- a/db/migrate/20151203162134_add_build_events_to_services.rb +++ b/db/migrate/20151203162134_add_build_events_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddBuildEventsToServices < ActiveRecord::Migration def change add_column :services, :build_events, :boolean, default: false, null: false diff --git a/db/migrate/20151209144329_migrate_ci_web_hooks.rb b/db/migrate/20151209144329_migrate_ci_web_hooks.rb index d7e196e676..cb1e556623 100644 --- a/db/migrate/20151209144329_migrate_ci_web_hooks.rb +++ b/db/migrate/20151209144329_migrate_ci_web_hooks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiWebHooks < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb index 7f330a2cf0..6b7a106814 100644 --- a/db/migrate/20151209145909_migrate_ci_emails.rb +++ b/db/migrate/20151209145909_migrate_ci_emails.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiEmails < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20151210030143_add_unlock_token_to_user.rb b/db/migrate/20151210030143_add_unlock_token_to_user.rb index 0ea66ba65d..d23c648f78 100644 --- a/db/migrate/20151210030143_add_unlock_token_to_user.rb +++ b/db/migrate/20151210030143_add_unlock_token_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUnlockTokenToUser < ActiveRecord::Migration def change add_column :users, :unlock_token, :string diff --git a/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb b/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb index 00f88180e4..92c7b5befd 100644 --- a/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb +++ b/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRunnersRegistrationTokenToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :runners_registration_token, :string diff --git a/db/migrate/20151210125232_migrate_ci_slack_service.rb b/db/migrate/20151210125232_migrate_ci_slack_service.rb index f14efa3e95..633d5148d9 100644 --- a/db/migrate/20151210125232_migrate_ci_slack_service.rb +++ b/db/migrate/20151210125232_migrate_ci_slack_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiSlackService < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb index b9e0432357..dae084ce18 100644 --- a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb +++ b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiHipChatService < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20151210125928_add_ci_to_project.rb b/db/migrate/20151210125928_add_ci_to_project.rb index 8c167f64a2..a9ff49a3f7 100644 --- a/db/migrate/20151210125928_add_ci_to_project.rb +++ b/db/migrate/20151210125928_add_ci_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiToProject < ActiveRecord::Migration def change add_column :projects, :ci_id, :integer diff --git a/db/migrate/20151210125929_add_project_id_to_ci.rb b/db/migrate/20151210125929_add_project_id_to_ci.rb index 84273591fa..b5de64b82c 100644 --- a/db/migrate/20151210125929_add_project_id_to_ci.rb +++ b/db/migrate/20151210125929_add_project_id_to_ci.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectIdToCi < ActiveRecord::Migration def change add_column :ci_builds, :gl_project_id, :integer diff --git a/db/migrate/20151210125930_migrate_ci_to_project.rb b/db/migrate/20151210125930_migrate_ci_to_project.rb index c32c7feb19..bb6d74ae21 100644 --- a/db/migrate/20151210125930_migrate_ci_to_project.rb +++ b/db/migrate/20151210125930_migrate_ci_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiToProject < ActiveRecord::Migration def up migrate_project_id_for_table('ci_runner_projects') diff --git a/db/migrate/20151210125931_add_index_to_ci_tables.rb b/db/migrate/20151210125931_add_index_to_ci_tables.rb index 5e129c9303..d87d335cf6 100644 --- a/db/migrate/20151210125931_add_index_to_ci_tables.rb +++ b/db/migrate/20151210125931_add_index_to_ci_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToCiTables < ActiveRecord::Migration def change add_index :ci_builds, :gl_project_id diff --git a/db/migrate/20151210125932_drop_null_for_ci_tables.rb b/db/migrate/20151210125932_drop_null_for_ci_tables.rb index c520c2ed56..e1a0a96458 100644 --- a/db/migrate/20151210125932_drop_null_for_ci_tables.rb +++ b/db/migrate/20151210125932_drop_null_for_ci_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class DropNullForCiTables < ActiveRecord::Migration def change remove_index :ci_variables, :project_id diff --git a/db/migrate/20151218154042_add_tfa_to_application_settings.rb b/db/migrate/20151218154042_add_tfa_to_application_settings.rb index dd95db775c..afdaf76b91 100644 --- a/db/migrate/20151218154042_add_tfa_to_application_settings.rb +++ b/db/migrate/20151218154042_add_tfa_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTfaToApplicationSettings < ActiveRecord::Migration def change change_table :application_settings do |t| diff --git a/db/migrate/20151221234414_add_tfa_additional_fields.rb b/db/migrate/20151221234414_add_tfa_additional_fields.rb index c16df47932..c3e4aaa606 100644 --- a/db/migrate/20151221234414_add_tfa_additional_fields.rb +++ b/db/migrate/20151221234414_add_tfa_additional_fields.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTfaAdditionalFields < ActiveRecord::Migration def change change_table :users do |t| diff --git a/db/migrate/20151224123230_rename_emojis.rb b/db/migrate/20151224123230_rename_emojis.rb index 62d921dfdc..2c24f3beee 100644 --- a/db/migrate/20151224123230_rename_emojis.rb +++ b/db/migrate/20151224123230_rename_emojis.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Migration type: online without errors (works on previous version and new one) class RenameEmojis < ActiveRecord::Migration def up diff --git a/db/migrate/20151228111122_remove_public_from_namespace.rb b/db/migrate/20151228111122_remove_public_from_namespace.rb index f4c848bbf4..bcb322d9cb 100644 --- a/db/migrate/20151228111122_remove_public_from_namespace.rb +++ b/db/migrate/20151228111122_remove_public_from_namespace.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Migration type: online class RemovePublicFromNamespace < ActiveRecord::Migration def change diff --git a/db/migrate/20151228150906_influxdb_settings.rb b/db/migrate/20151228150906_influxdb_settings.rb index 3012bd52cf..2e080a02e6 100644 --- a/db/migrate/20151228150906_influxdb_settings.rb +++ b/db/migrate/20151228150906_influxdb_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class InfluxdbSettings < ActiveRecord::Migration def change add_column :application_settings, :metrics_enabled, :boolean, default: false diff --git a/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb index 259fd0248d..e0dd19b2b0 100644 --- a/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb +++ b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRecaptchaToApplicationSettings < ActiveRecord::Migration def change change_table :application_settings do |t| diff --git a/db/migrate/20151229102248_influxdb_udp_port_setting.rb b/db/migrate/20151229102248_influxdb_udp_port_setting.rb index ae0499f936..3e1bfd4389 100644 --- a/db/migrate/20151229102248_influxdb_udp_port_setting.rb +++ b/db/migrate/20151229102248_influxdb_udp_port_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class InfluxdbUdpPortSetting < ActiveRecord::Migration def change add_column :application_settings, :metrics_port, :integer, default: 8089 diff --git a/db/migrate/20151229112614_influxdb_remote_database_setting.rb b/db/migrate/20151229112614_influxdb_remote_database_setting.rb index f0e1ee1e7a..d2ac906ead 100644 --- a/db/migrate/20151229112614_influxdb_remote_database_setting.rb +++ b/db/migrate/20151229112614_influxdb_remote_database_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class InfluxdbRemoteDatabaseSetting < ActiveRecord::Migration def change remove_column :application_settings, :metrics_database diff --git a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb index 6c282fc503..4fcca06d90 100644 --- a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb +++ b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration def change add_column :ci_builds, :artifacts_metadata, :text diff --git a/db/migrate/20151231152326_add_akismet_to_application_settings.rb b/db/migrate/20151231152326_add_akismet_to_application_settings.rb index 3f52c758f9..7b0fab6f55 100644 --- a/db/migrate/20151231152326_add_akismet_to_application_settings.rb +++ b/db/migrate/20151231152326_add_akismet_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAkismetToApplicationSettings < ActiveRecord::Migration def change change_table :application_settings do |t| diff --git a/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb index 78fdfeaf5c..0bdd639eb2 100644 --- a/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb +++ b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveAlertTypeFromBroadcastMessages < ActiveRecord::Migration def change remove_column :broadcast_messages, :alert_type, :integer diff --git a/db/migrate/20160106162223_add_index_milestones_title.rb b/db/migrate/20160106162223_add_index_milestones_title.rb index 767885e2aa..9b9b6445a0 100644 --- a/db/migrate/20160106162223_add_index_milestones_title.rb +++ b/db/migrate/20160106162223_add_index_milestones_title.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexMilestonesTitle < ActiveRecord::Migration def change add_index :milestones, :title diff --git a/db/migrate/20160106164438_remove_influxdb_credentials.rb b/db/migrate/20160106164438_remove_influxdb_credentials.rb index 47e74400b9..987d75d6fd 100644 --- a/db/migrate/20160106164438_remove_influxdb_credentials.rb +++ b/db/migrate/20160106164438_remove_influxdb_credentials.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveInfluxdbCredentials < ActiveRecord::Migration def change remove_column :application_settings, :metrics_username, :string diff --git a/db/migrate/20160109054846_create_spam_logs.rb b/db/migrate/20160109054846_create_spam_logs.rb index f12fe9f8f7..f710327663 100644 --- a/db/migrate/20160109054846_create_spam_logs.rb +++ b/db/migrate/20160109054846_create_spam_logs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateSpamLogs < ActiveRecord::Migration def change create_table :spam_logs do |t| diff --git a/db/migrate/20160113111034_add_metrics_sample_interval.rb b/db/migrate/20160113111034_add_metrics_sample_interval.rb index b741f5d2c7..c1041da818 100644 --- a/db/migrate/20160113111034_add_metrics_sample_interval.rb +++ b/db/migrate/20160113111034_add_metrics_sample_interval.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMetricsSampleInterval < ActiveRecord::Migration def change add_column :application_settings, :metrics_sample_interval, :integer, diff --git a/db/migrate/20160118155830_add_sentry_to_application_settings.rb b/db/migrate/20160118155830_add_sentry_to_application_settings.rb index fa7ff9d922..a6f715263e 100644 --- a/db/migrate/20160118155830_add_sentry_to_application_settings.rb +++ b/db/migrate/20160118155830_add_sentry_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSentryToApplicationSettings < ActiveRecord::Migration def change change_table :application_settings do |t| diff --git a/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb index 26606b10b5..19ea40b554 100644 --- a/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb +++ b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIpBlockingSettingsToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :ip_blocking_enabled, :boolean, default: false diff --git a/db/migrate/20160119111158_add_services_category.rb b/db/migrate/20160119111158_add_services_category.rb index a9110a8418..f77484b2f9 100644 --- a/db/migrate/20160119111158_add_services_category.rb +++ b/db/migrate/20160119111158_add_services_category.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddServicesCategory < ActiveRecord::Migration def up add_column :services, :category, :string, default: 'common', null: false diff --git a/db/migrate/20160119112418_add_services_default.rb b/db/migrate/20160119112418_add_services_default.rb index 69a42d7b87..7fa531899f 100644 --- a/db/migrate/20160119112418_add_services_default.rb +++ b/db/migrate/20160119112418_add_services_default.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddServicesDefault < ActiveRecord::Migration def up add_column :services, :default, :boolean, default: false diff --git a/db/migrate/20160119145451_add_ldap_email_to_users.rb b/db/migrate/20160119145451_add_ldap_email_to_users.rb index 654d31ab15..5b2b0bd31c 100644 --- a/db/migrate/20160119145451_add_ldap_email_to_users.rb +++ b/db/migrate/20160119145451_add_ldap_email_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLdapEmailToUsers < ActiveRecord::Migration def up add_column :users, :ldap_email, :boolean, default: false, null: false diff --git a/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb b/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb index d6c6aa4a4e..3837208f81 100644 --- a/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb +++ b/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddBaseCommitShaToMergeRequestDiffs < ActiveRecord::Migration def change add_column :merge_request_diffs, :base_commit_sha, :string diff --git a/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb b/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb index d50791410f..9a2570ae54 100644 --- a/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb +++ b/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEmailAuthorInBodyToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :email_author_in_body, :boolean, default: false diff --git a/db/migrate/20160122185421_add_pending_delete_to_project.rb b/db/migrate/20160122185421_add_pending_delete_to_project.rb index 046a5d8fc3..61db852843 100644 --- a/db/migrate/20160122185421_add_pending_delete_to_project.rb +++ b/db/migrate/20160122185421_add_pending_delete_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPendingDeleteToProject < ActiveRecord::Migration def change add_column :projects, :pending_delete, :boolean, default: false diff --git a/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb b/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb index 41821cdcc4..60ecda998d 100644 --- a/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb +++ b/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveIpBlockingSettingsFromApplicationSettings < ActiveRecord::Migration def change remove_column :application_settings, :ip_blocking_enabled, :boolean, default: false diff --git a/db/migrate/20160128233227_change_lfs_objects_size_column.rb b/db/migrate/20160128233227_change_lfs_objects_size_column.rb index e7fd1f7177..645c0cdb19 100644 --- a/db/migrate/20160128233227_change_lfs_objects_size_column.rb +++ b/db/migrate/20160128233227_change_lfs_objects_size_column.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeLfsObjectsSizeColumn < ActiveRecord::Migration def change change_column :lfs_objects, :size, :integer, limit: 8 diff --git a/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb b/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb index d3ea956952..b10c0602e2 100644 --- a/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb +++ b/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveDotAtomPathEndingOfProjects < ActiveRecord::Migration include Gitlab::ShellAdapter diff --git a/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb b/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb index f0d9422651..332b5a756e 100644 --- a/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb +++ b/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMergeCommitShaToMergeRequests < ActiveRecord::Migration def change add_column :merge_requests, :merge_commit_sha, :string diff --git a/db/migrate/20160202091601_add_erasable_to_ci_build.rb b/db/migrate/20160202091601_add_erasable_to_ci_build.rb index f9912f2274..767ae160d0 100644 --- a/db/migrate/20160202091601_add_erasable_to_ci_build.rb +++ b/db/migrate/20160202091601_add_erasable_to_ci_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddErasableToCiBuild < ActiveRecord::Migration def change add_reference :ci_builds, :erased_by, references: :users, index: true diff --git a/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb b/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb index 793984343b..2c5cb307fa 100644 --- a/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb +++ b/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAllowGuestToAccessBuildsProject < ActiveRecord::Migration def change add_column :projects, :public_builds, :boolean, default: true, null: false diff --git a/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb b/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb index f996ae74dc..11b6ff3100 100644 --- a/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb +++ b/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRealSizeToMergeRequestDiffs < ActiveRecord::Migration def change add_column :merge_request_diffs, :real_size, :string diff --git a/db/migrate/20160209130428_add_index_to_snippet.rb b/db/migrate/20160209130428_add_index_to_snippet.rb index 95d5719be5..4d17c3a291 100644 --- a/db/migrate/20160209130428_add_index_to_snippet.rb +++ b/db/migrate/20160209130428_add_index_to_snippet.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToSnippet < ActiveRecord::Migration def change add_index :snippets, :updated_at diff --git a/db/migrate/20160212123307_create_tasks.rb b/db/migrate/20160212123307_create_tasks.rb index c3f6f3abc2..20573b0135 100644 --- a/db/migrate/20160212123307_create_tasks.rb +++ b/db/migrate/20160212123307_create_tasks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateTasks < ActiveRecord::Migration def change create_table :tasks do |t| diff --git a/db/migrate/20160217100506_add_description_to_label.rb b/db/migrate/20160217100506_add_description_to_label.rb index eed6d1f236..af5af16747 100644 --- a/db/migrate/20160217100506_add_description_to_label.rb +++ b/db/migrate/20160217100506_add_description_to_label.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDescriptionToLabel < ActiveRecord::Migration def change add_column :labels, :description, :string diff --git a/db/migrate/20160217174422_add_note_to_tasks.rb b/db/migrate/20160217174422_add_note_to_tasks.rb index da5cb2e05d..a9a2b77e42 100644 --- a/db/migrate/20160217174422_add_note_to_tasks.rb +++ b/db/migrate/20160217174422_add_note_to_tasks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNoteToTasks < ActiveRecord::Migration def change add_reference :tasks, :note, index: true diff --git a/db/migrate/20160220123949_rename_tasks_to_todos.rb b/db/migrate/20160220123949_rename_tasks_to_todos.rb index 30c10d2714..f16b37537f 100644 --- a/db/migrate/20160220123949_rename_tasks_to_todos.rb +++ b/db/migrate/20160220123949_rename_tasks_to_todos.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameTasksToTodos < ActiveRecord::Migration def change rename_table :tasks, :todos diff --git a/db/migrate/20160222153918_create_appearances_ce.rb b/db/migrate/20160222153918_create_appearances_ce.rb index bec66bcc71..b2d5949b23 100644 --- a/db/migrate/20160222153918_create_appearances_ce.rb +++ b/db/migrate/20160222153918_create_appearances_ce.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateAppearancesCe < ActiveRecord::Migration def change unless table_exists?(:appearances) diff --git a/db/migrate/20160223192159_add_confidential_to_issues.rb b/db/migrate/20160223192159_add_confidential_to_issues.rb index e9d47fd589..5b99ce30e9 100644 --- a/db/migrate/20160223192159_add_confidential_to_issues.rb +++ b/db/migrate/20160223192159_add_confidential_to_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddConfidentialToIssues < ActiveRecord::Migration def change add_column :issues, :confidential, :boolean, default: false diff --git a/db/migrate/20160225090018_add_delete_at_to_issues.rb b/db/migrate/20160225090018_add_delete_at_to_issues.rb index 3ddbef9297..139f911e1c 100644 --- a/db/migrate/20160225090018_add_delete_at_to_issues.rb +++ b/db/migrate/20160225090018_add_delete_at_to_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDeleteAtToIssues < ActiveRecord::Migration def change add_column :issues, :deleted_at, :datetime diff --git a/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb b/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb index 9d09105f17..4ca3f0dcdc 100644 --- a/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb +++ b/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDeleteAtToMergeRequests < ActiveRecord::Migration def change add_column :merge_requests, :deleted_at, :datetime diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb index d7b00e3d6e..375e389e07 100644 --- a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb +++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTrigramIndexesForSearching < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20160227120001_add_event_field_for_web_hook.rb b/db/migrate/20160227120001_add_event_field_for_web_hook.rb index 65f2a47bb3..89910893ee 100644 --- a/db/migrate/20160227120001_add_event_field_for_web_hook.rb +++ b/db/migrate/20160227120001_add_event_field_for_web_hook.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEventFieldForWebHook < ActiveRecord::Migration def change add_column :web_hooks, :wiki_page_events, :boolean, default: false, null: false diff --git a/db/migrate/20160227120047_add_event_to_services.rb b/db/migrate/20160227120047_add_event_to_services.rb index f5040d770d..fe7c54ca4e 100644 --- a/db/migrate/20160227120047_add_event_to_services.rb +++ b/db/migrate/20160227120047_add_event_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEventToServices < ActiveRecord::Migration def change add_column :services, :wiki_page_events, :boolean, default: true diff --git a/db/migrate/20160229193553_add_main_language_to_repository.rb b/db/migrate/20160229193553_add_main_language_to_repository.rb index b5446c6a44..ad5167b4c9 100644 --- a/db/migrate/20160229193553_add_main_language_to_repository.rb +++ b/db/migrate/20160229193553_add_main_language_to_repository.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMainLanguageToRepository < ActiveRecord::Migration def change add_column :projects, :main_language, :string diff --git a/db/migrate/20160301124843_add_visibility_level_to_groups.rb b/db/migrate/20160301124843_add_visibility_level_to_groups.rb index d1b921bb20..a874e6758d 100644 --- a/db/migrate/20160301124843_add_visibility_level_to_groups.rb +++ b/db/migrate/20160301124843_add_visibility_level_to_groups.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddVisibilityLevelToGroups < ActiveRecord::Migration def up add_column :namespaces, :visibility_level, :integer, null: false, default: Gitlab::VisibilityLevel::PUBLIC diff --git a/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb b/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb index ffcd64266e..1f400566f9 100644 --- a/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb +++ b/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportCredentialsToProjectImportData < ActiveRecord::Migration def change add_column :project_import_data, :encrypted_credentials, :text diff --git a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb index 6aed0fe03d..ac7eac0ea7 100644 --- a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb +++ b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Loops through old importer projects that kept a token/password in the import URL # and encrypts the credentials into a separate field in project#import_data # #down method not supported diff --git a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb index fc12b5b09e..cac78703bc 100644 --- a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb +++ b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveExpiresAtFromSnippets < ActiveRecord::Migration def change remove_column :snippets, :expires_at, :datetime diff --git a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb index 49e787d9a9..10f2b8cc56 100644 --- a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb +++ b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class DisallowBlankLineCodeOnNote < ActiveRecord::Migration def up execute("UPDATE notes SET line_code = NULL WHERE line_code = ''") diff --git a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb index 72b862d67d..92c0a1e088 100644 --- a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb +++ b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Create visibility level field on DB # Sets default_visibility_level to value on settings if not restricted # If value is restricted takes higher visibility level allowed diff --git a/db/migrate/20160309140734_fix_todos.rb b/db/migrate/20160309140734_fix_todos.rb index ebe0fc8230..94fe1e4fdc 100644 --- a/db/migrate/20160309140734_fix_todos.rb +++ b/db/migrate/20160309140734_fix_todos.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixTodos < ActiveRecord::Migration def up execute <<-SQL diff --git a/db/migrate/20160310124959_add_due_date_to_issues.rb b/db/migrate/20160310124959_add_due_date_to_issues.rb index ec08bd9fdf..a4eb6aaee6 100644 --- a/db/migrate/20160310124959_add_due_date_to_issues.rb +++ b/db/migrate/20160310124959_add_due_date_to_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDueDateToIssues < ActiveRecord::Migration def change add_column :issues, :due_date, :date diff --git a/db/migrate/20160310185910_add_external_flag_to_users.rb b/db/migrate/20160310185910_add_external_flag_to_users.rb index 54937f1eb7..209496dc78 100644 --- a/db/migrate/20160310185910_add_external_flag_to_users.rb +++ b/db/migrate/20160310185910_add_external_flag_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddExternalFlagToUsers < ActiveRecord::Migration def change add_column :users, :external, :boolean, default: false diff --git a/db/migrate/20160314094147_add_priority_to_label.rb b/db/migrate/20160314094147_add_priority_to_label.rb index 8ddf778297..7fb23cba4c 100644 --- a/db/migrate/20160314094147_add_priority_to_label.rb +++ b/db/migrate/20160314094147_add_priority_to_label.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPriorityToLabel < ActiveRecord::Migration def change add_column :labels, :priority, :integer diff --git a/db/migrate/20160314143402_projects_add_pushes_since_gc.rb b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb index 5d30a38bc9..9f8ffe073a 100644 --- a/db/migrate/20160314143402_projects_add_pushes_since_gc.rb +++ b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ProjectsAddPushesSinceGc < ActiveRecord::Migration def change add_column :projects, :pushes_since_gc, :integer, default: 0 diff --git a/db/migrate/20160315135439_project_add_repository_check.rb b/db/migrate/20160315135439_project_add_repository_check.rb index 8687d5d629..8fe649246c 100644 --- a/db/migrate/20160315135439_project_add_repository_check.rb +++ b/db/migrate/20160315135439_project_add_repository_check.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ProjectAddRepositoryCheck < ActiveRecord::Migration def change add_column :projects, :last_repository_check_failed, :boolean diff --git a/db/migrate/20160316123110_ci_runners_token_index.rb b/db/migrate/20160316123110_ci_runners_token_index.rb index 67bf5b4f97..ff3d36d68e 100644 --- a/db/migrate/20160316123110_ci_runners_token_index.rb +++ b/db/migrate/20160316123110_ci_runners_token_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CiRunnersTokenIndex < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb index 6871b3920d..65e0e61c78 100644 --- a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb +++ b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeTargetIdToNullOnTodos < ActiveRecord::Migration def change change_column_null :todos, :target_id, true diff --git a/db/migrate/20160316204731_add_commit_id_to_todos.rb b/db/migrate/20160316204731_add_commit_id_to_todos.rb index ae19fdd1ab..d79858fc92 100644 --- a/db/migrate/20160316204731_add_commit_id_to_todos.rb +++ b/db/migrate/20160316204731_add_commit_id_to_todos.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCommitIdToTodos < ActiveRecord::Migration def change add_column :todos, :commit_id, :string diff --git a/db/migrate/20160317092222_add_moved_to_to_issue.rb b/db/migrate/20160317092222_add_moved_to_to_issue.rb index 461e7fb3a9..9dde668ddf 100644 --- a/db/migrate/20160317092222_add_moved_to_to_issue.rb +++ b/db/migrate/20160317092222_add_moved_to_to_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMovedToToIssue < ActiveRecord::Migration def change add_reference :issues, :moved_to, references: :issues diff --git a/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb b/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb index 370b339d45..07ae7c9547 100644 --- a/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb +++ b/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class IndexNamespacesOnVisibilityLevel < ActiveRecord::Migration def change unless index_exists?(:namespaces, :visibility_level) diff --git a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb index 1fff9759d1..a9a851cfe6 100644 --- a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb +++ b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveTodosForDeletedIssues < ActiveRecord::Migration def up execute <<-SQL diff --git a/db/migrate/20160328112808_create_notification_settings.rb b/db/migrate/20160328112808_create_notification_settings.rb index 4755da8b80..7d77e8004b 100644 --- a/db/migrate/20160328112808_create_notification_settings.rb +++ b/db/migrate/20160328112808_create_notification_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateNotificationSettings < ActiveRecord::Migration def change create_table :notification_settings do |t| diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb index 3c81b2c37b..eb6b7d0721 100644 --- a/db/migrate/20160328115649_migrate_new_notification_setting.rb +++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration will create one row of NotificationSetting for each Member row # It can take long time on big instances. # diff --git a/db/migrate/20160328121138_add_notification_setting_index.rb b/db/migrate/20160328121138_add_notification_setting_index.rb index 8aebce0244..667270d6b0 100644 --- a/db/migrate/20160328121138_add_notification_setting_index.rb +++ b/db/migrate/20160328121138_add_notification_setting_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationSettingIndex < ActiveRecord::Migration def change add_index :notification_settings, :user_id diff --git a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb index 275554e736..a3df8fb4e2 100644 --- a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb +++ b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexOnPendingDeleteProjects < ActiveRecord::Migration def change add_index :projects, :pending_delete diff --git a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb index 54cea964ff..b15af79b9b 100644 --- a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb +++ b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveTodosForDeletedMergeRequests < ActiveRecord::Migration def up execute <<-SQL diff --git a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb index 0d736e323b..dec80497fb 100644 --- a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb +++ b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration def change remove_column :application_settings, :twitter_sharing_enabled, :boolean diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb index 47f0ca8e8d..fcffc98b47 100644 --- a/db/migrate/20160407120251_add_images_enabled_for_project.rb +++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImagesEnabledForProject < ActiveRecord::Migration def change add_column :projects, :container_registry_enabled, :boolean diff --git a/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb index ebfa4bcbc7..920d4d4111 100644 --- a/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb +++ b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRepositoryChecksEnabledSetting < ActiveRecord::Migration def change add_column :application_settings, :repository_checks_enabled, :boolean, default: true diff --git a/db/migrate/20160412173416_add_fields_to_ci_commit.rb b/db/migrate/20160412173416_add_fields_to_ci_commit.rb index 125956a3dd..00162af5cd 100644 --- a/db/migrate/20160412173416_add_fields_to_ci_commit.rb +++ b/db/migrate/20160412173416_add_fields_to_ci_commit.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddFieldsToCiCommit < ActiveRecord::Migration def change add_column :ci_commits, :status, :string diff --git a/db/migrate/20160412173417_update_ci_commit.rb b/db/migrate/20160412173417_update_ci_commit.rb index fd92444dba..858faeb060 100644 --- a/db/migrate/20160412173417_update_ci_commit.rb +++ b/db/migrate/20160412173417_update_ci_commit.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class UpdateCiCommit < ActiveRecord::Migration # This migration can be run online, but needs to be executed for the second time after restarting Unicorn workers # Otherwise Offline migration should be used. diff --git a/db/migrate/20160412173418_add_ci_commit_indexes.rb b/db/migrate/20160412173418_add_ci_commit_indexes.rb index 603d4a4161..414f1f8279 100644 --- a/db/migrate/20160412173418_add_ci_commit_indexes.rb +++ b/db/migrate/20160412173418_add_ci_commit_indexes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiCommitIndexes < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20160413115152_add_token_to_web_hooks.rb b/db/migrate/20160413115152_add_token_to_web_hooks.rb index f04225068c..628b1d51b3 100644 --- a/db/migrate/20160413115152_add_token_to_web_hooks.rb +++ b/db/migrate/20160413115152_add_token_to_web_hooks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTokenToWebHooks < ActiveRecord::Migration def change add_column :web_hooks, :token, :string diff --git a/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb index d493044c67..b53b9bc6c3 100644 --- a/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb +++ b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSharedRunnersTextToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :shared_runners_text, :text diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb index 2ead181921..a3bee9b1bc 100644 --- a/db/migrate/20160416180807_add_award_emoji.rb +++ b/db/migrate/20160416180807_add_award_emoji.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAwardEmoji < ActiveRecord::Migration def change create_table :award_emoji do |t| diff --git a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb index 073bbc0fc2..c226bc11f6 100644 --- a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb +++ b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration def change def up diff --git a/db/migrate/20160416190505_remove_note_is_award.rb b/db/migrate/20160416190505_remove_note_is_award.rb index da16372a29..dd24917feb 100644 --- a/db/migrate/20160416190505_remove_note_is_award.rb +++ b/db/migrate/20160416190505_remove_note_is_award.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveNoteIsAward < ActiveRecord::Migration def change remove_column :notes, :is_award, :boolean diff --git a/db/migrate/20160419120017_add_metrics_packet_size.rb b/db/migrate/20160419120017_add_metrics_packet_size.rb index 78c163d62a..c759427c59 100644 --- a/db/migrate/20160419120017_add_metrics_packet_size.rb +++ b/db/migrate/20160419120017_add_metrics_packet_size.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMetricsPacketSize < ActiveRecord::Migration def change add_column :application_settings, :metrics_packet_size, :integer, default: 1 diff --git a/db/migrate/20160421130527_disable_repository_checks.rb b/db/migrate/20160421130527_disable_repository_checks.rb index 808a4b93c7..7e65ddc45e 100644 --- a/db/migrate/20160421130527_disable_repository_checks.rb +++ b/db/migrate/20160421130527_disable_repository_checks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class DisableRepositoryChecks < ActiveRecord::Migration def up change_column_default :application_settings, :repository_checks_enabled, false diff --git a/db/migrate/20160425045124_create_u2f_registrations.rb b/db/migrate/20160425045124_create_u2f_registrations.rb index 93bdd9de2e..72cbe98ebb 100644 --- a/db/migrate/20160425045124_create_u2f_registrations.rb +++ b/db/migrate/20160425045124_create_u2f_registrations.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateU2fRegistrations < ActiveRecord::Migration def change create_table :u2f_registrations do |t| 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 index facd33875b..bf50616656 100644 --- 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 @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDisabledOauthSignInSourcesToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :disabled_oauth_sign_in_sources, :text diff --git a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb index 84e5e4eabe..c60892a627 100644 --- a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb +++ b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRunUntaggedToCiRunner < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb b/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb index aa560bc0f0..6792ffc957 100644 --- a/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb +++ b/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveWallEnabledFromProjects < ActiveRecord::Migration def change remove_column :projects, :wall_enabled, :boolean, default: true, null: false diff --git a/db/migrate/20160508215820_add_type_to_notes.rb b/db/migrate/20160508215820_add_type_to_notes.rb index 58944d4e65..c1d07c9363 100644 --- a/db/migrate/20160508215820_add_type_to_notes.rb +++ b/db/migrate/20160508215820_add_type_to_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTypeToNotes < ActiveRecord::Migration def change add_column :notes, :type, :string diff --git a/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb index c3f23d89d5..6dd958ff4a 100644 --- a/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb +++ b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class SetTypeOnLegacyDiffNotes < ActiveRecord::Migration def change execute "UPDATE notes SET type = 'LegacyDiffNote' WHERE line_code IS NOT NULL" 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 index 9d729fec18..b6a5bea79b 100644 --- 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 @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHealthCheckAccessTokenToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :health_check_access_token, :string diff --git a/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb index c34e7ba540..8c96353b85 100644 --- a/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb +++ b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSendUserConfirmationEmailToApplicationSettings < ActiveRecord::Migration def up add_column :application_settings, :send_user_confirmation_email, :boolean, default: false diff --git a/db/migrate/20160525205328_remove_main_language_from_projects.rb b/db/migrate/20160525205328_remove_main_language_from_projects.rb index 0f9d60c385..dc4ceacddb 100644 --- a/db/migrate/20160525205328_remove_main_language_from_projects.rb +++ b/db/migrate/20160525205328_remove_main_language_from_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb b/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb index 7910120b4e..3e26be7c09 100644 --- a/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb +++ b/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveNotificationSettingsForDeletedProjects < ActiveRecord::Migration def up execute <<-SQL diff --git a/db/migrate/20160528043124_add_users_state_index.rb b/db/migrate/20160528043124_add_users_state_index.rb index e77a546073..6419d2ae71 100644 --- a/db/migrate/20160528043124_add_users_state_index.rb +++ b/db/migrate/20160528043124_add_users_state_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUsersStateIndex < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb b/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb index e21376bd57..d811fd5271 100644 --- a/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb +++ b/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This is ONLINE migration class AddContainerRegistryTokenExpireDelayToApplicationSettings < ActiveRecord::Migration diff --git a/db/migrate/20160603180330_remove_duplicated_notification_settings.rb b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb index fe1c863b5b..4f4f58b161 100644 --- a/db/migrate/20160603180330_remove_duplicated_notification_settings.rb +++ b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveDuplicatedNotificationSettings < ActiveRecord::Migration def up duplicates = exec_query(%Q{ diff --git a/db/migrate/20160603182247_add_index_to_notification_settings.rb b/db/migrate/20160603182247_add_index_to_notification_settings.rb index 06462042b0..f6ae26d555 100644 --- a/db/migrate/20160603182247_add_index_to_notification_settings.rb +++ b/db/migrate/20160603182247_add_index_to_notification_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToNotificationSettings < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb b/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb index 89826fb96c..3c5d2ad910 100644 --- a/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb +++ b/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAfterSignUpTextToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :after_sign_up_text, :text diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb index 14d7e84d85..be3501c4c2 100644 --- a/db/migrate/limits_to_mysql.rb +++ b/db/migrate/limits_to_mysql.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class LimitsToMysql < ActiveRecord::Migration def up return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ From cef3fd28996edd07b1a9bf00f8a6f1d9bc01c1ac Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 8 Jun 2016 22:48:44 -0300 Subject: [PATCH 449/507] Wrap all rate limiting logic inside GitHub API client --- lib/gitlab/github_import/client.rb | 35 +++++++++++++++- lib/gitlab/github_import/importer.rb | 62 ++++------------------------ 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 67988ea346..d325eca6d9 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -1,6 +1,9 @@ module Gitlab module GithubImport class Client + GITHUB_SAFE_REMAINING_REQUESTS = 100 + GITHUB_SAFE_SLEEP_TIME = 500 + attr_reader :client, :api def initialize(access_token) @@ -11,7 +14,7 @@ module Gitlab ) if access_token - ::Octokit.auto_paginate = true + ::Octokit.auto_paginate = false @api = ::Octokit::Client.new( access_token: access_token, @@ -36,7 +39,7 @@ module Gitlab def method_missing(method, *args, &block) if api.respond_to?(method) - api.send(method, *args, &block) + request { api.send(method, *args, &block) } else super(method, *args, &block) end @@ -55,6 +58,34 @@ module Gitlab def github_options config["args"]["client_options"].deep_symbolize_keys end + + def rate_limit + api.rate_limit! + end + + def rate_limit_exceed? + rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS + end + + def rate_limit_sleep_time + rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME + end + + def request + sleep rate_limit_sleep_time if rate_limit_exceed? + + data = yield + + last_response = api.last_response + + while last_response.rels[:next] + sleep rate_limit_sleep_time if rate_limit_exceed? + last_response = last_response.rels[:next].get + data.concat(last_response.data) if last_response.data.is_a?(Array) + end + + data + end end end end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 5ef9d66ba6..e5cf66a037 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,9 +3,6 @@ module Gitlab class Importer include Gitlab::ShellAdapter - GITHUB_SAFE_REMAINING_REQUESTS = 100 - GITHUB_SAFE_SLEEP_TIME = 500 - attr_reader :client, :project, :repo, :repo_url def initialize(project) @@ -28,52 +25,12 @@ module Gitlab private - def turn_auto_pagination_off! - client.auto_paginate = false - end - - def turn_auto_pagination_on! - client.auto_paginate = true - end - - def rate_limit - client.rate_limit! - end - - def rate_limit_exceed? - rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS - end - - def rate_limit_sleep_time - rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME - end - - def paginate - turn_auto_pagination_off! - - sleep rate_limit_sleep_time if rate_limit_exceed? - - data = yield - - last_response = client.last_response - - while last_response.rels[:next] - sleep rate_limit_sleep_time if rate_limit_exceed? - last_response = last_response.rels[:next].get - data.concat(last_response.data) if last_response.data.is_a?(Array) - end - - turn_auto_pagination_on! - - data - end - def credentials @credentials ||= project.import_data.credentials if project.import_data end def import_labels - labels = paginate { client.labels(repo, per_page: 100) } + labels = client.labels(repo, per_page: 100) labels.each { |raw| LabelFormatter.new(project, raw).create! } true @@ -82,7 +39,7 @@ module Gitlab end def import_milestones - milestones = paginate { client.milestones(repo, state: :all, per_page: 100) } + milestones = client.milestones(repo, state: :all, per_page: 100) milestones.each { |raw| MilestoneFormatter.new(project, raw).create! } true @@ -91,9 +48,9 @@ module Gitlab end def import_issues - data = paginate { client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) } + issues = client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) - data.each do |raw| + issues.each do |raw| gh_issue = IssueFormatter.new(project, raw) if gh_issue.valid? @@ -112,7 +69,7 @@ module Gitlab hooks = client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?) disable_webhooks(hooks) - pull_requests = paginate { client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) } + pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?) source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] } @@ -146,14 +103,12 @@ module Gitlab def update_webhooks(hooks, options) hooks.each do |hook| - sleep rate_limit_sleep_time if rate_limit_exceed? client.edit_hook(repo, hook.id, hook.name, hook.config, options) end end def restore_branches(branches) branches.each do |name, sha| - sleep rate_limit_sleep_time if rate_limit_exceed? client.create_ref(repo, "refs/heads/#{name}", sha) end @@ -162,15 +117,12 @@ module Gitlab def clean_up_restored_branches(branches) branches.each do |name, _| - sleep rate_limit_sleep_time if rate_limit_exceed? client.delete_ref(repo, "heads/#{name}") project.repository.rm_branch(project.creator, name) end end def apply_labels(issuable) - sleep rate_limit_sleep_time if rate_limit_exceed? - issue = client.issue(repo, issuable.iid) if issue.labels.count > 0 @@ -183,12 +135,12 @@ module Gitlab end def import_comments(issuable) - comments = paginate { client.issue_comments(repo, issuable.iid, per_page: 100) } + comments = client.issue_comments(repo, issuable.iid, per_page: 100) create_comments(issuable, comments) end def import_comments_on_diff(merge_request) - comments = paginate { client.pull_request_comments(repo, merge_request.iid, per_page: 100) } + comments = client.pull_request_comments(repo, merge_request.iid, per_page: 100) create_comments(merge_request, comments) end From 921c356b5e776abc072724dd4238029c1bf13961 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 7 Jun 2016 14:51:00 +0200 Subject: [PATCH 450/507] Rename commit to pipeline in TriggerRequest --- app/models/ci/trigger_request.rb | 2 +- app/services/ci/create_trigger_request_service.rb | 2 +- spec/models/build_spec.rb | 2 +- spec/requests/ci/api/builds_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 59fc9951d1..b69ae37668 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -3,7 +3,7 @@ module Ci extend Ci::Model belongs_to :trigger, class_name: 'Ci::Trigger' - belongs_to :commit, class_name: 'Ci::Pipeline', foreign_key: :commit_id + belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id has_many :builds, class_name: 'Ci::Build' serialize :variables diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index c3194f45b1..1e629cf119 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -11,7 +11,7 @@ module Ci trigger_request = trigger.trigger_requests.create!( variables: variables, - commit: pipeline, + pipeline: pipeline, ) if pipeline.create_builds(nil, trigger_request) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 7660ea2659..2beb6cc598 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -219,7 +219,7 @@ describe Ci::Build, models: true do context 'and trigger variables' do let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, commit: pipeline, trigger: trigger) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } let(:trigger_variables) do [ { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 8827164253..e8508f8f95 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -85,7 +85,7 @@ describe Ci::API::API do trigger = FactoryGirl.create(:ci_trigger, project: project) pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: pipeline, trigger: trigger) + trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) pipeline.create_builds(nil, trigger_request) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") From 4663ae064d64c6acd488ef4c28afcf60b843bb85 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 8 Jun 2016 12:50:49 +0200 Subject: [PATCH 451/507] Fix CI TriggerRequest entity --- lib/ci/api/entities.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index b25e0e573a..255217b1a7 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -56,7 +56,7 @@ module Ci class TriggerRequest < Grape::Entity expose :id, :variables - expose :commit, using: Commit + expose :trigger, using: Commit, as: :commit end end end From e796555bdb9884a34a5dcef595815594484aac41 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Jun 2016 09:59:34 +0100 Subject: [PATCH 452/507] Fixed issue where label filtering didnt work Closes #18375 --- app/assets/javascripts/labels_select.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index ec74dfaae1..6b86b51589 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -254,7 +254,7 @@ class @LabelsSelect search: fields: ['title'] selectable: true - + filterable: true toggleLabel: (selected, el) -> selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active') From d301791c2aa101b68f9b5abda24f20dc24a85830 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Jun 2016 10:44:17 +0100 Subject: [PATCH 453/507] Added tests --- app/helpers/dropdowns_helper.rb | 4 ++-- .../shared/issuable/_label_page_default.html.haml | 2 +- spec/features/issues/filter_by_labels_spec.rb | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 14697f774c..6b617e1730 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -67,9 +67,9 @@ module DropdownsHelper end end - def dropdown_filter(placeholder) + def dropdown_filter(placeholder, search_id: nil) content_tag :div, class: "dropdown-input" do - filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder + filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder filter_output << icon('search', class: "dropdown-input-search") filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button") diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index 4e280c371a..0acb825313 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -4,7 +4,7 @@ - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') .dropdown-page-one = dropdown_title(title) - = dropdown_filter(filter_placeholder) + = dropdown_filter(filter_placeholder, search_id: "label-name") = dropdown_content - if @project && show_footer = dropdown_footer do diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index 0ec8b6b180..16c619c928 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -199,4 +199,19 @@ feature 'Issue filtering by Labels', feature: true do end end end + + context 'dropdown filtering', js: true do + it 'should filter by label name' do + page.within '.labels-filter' do + click_button 'Label' + wait_for_ajax + fill_in 'label-name', with: 'bug' + + page.within '.dropdown-content' do + expect(page).not_to have_content 'enhancement' + expect(page).to have_content 'bug' + end + end + end + end end From e7ca709a9249236a3894833af67471ade4eb1d07 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 24 May 2016 10:29:26 +0100 Subject: [PATCH 454/507] Updated labels UI Closes #14227 --- app/assets/stylesheets/pages/labels.scss | 98 +++++++++------------- app/helpers/labels_helper.rb | 6 +- app/views/projects/labels/_label.html.haml | 54 ++++++++---- app/views/shared/_label_row.html.haml | 5 +- 4 files changed, 82 insertions(+), 81 deletions(-) diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 26128fcea8..335bdda13d 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -50,11 +50,26 @@ .label-row { .label-name { - display: inline-block; - width: 170px; + display: block; + margin-bottom: 10px; - @media (max-width: $screen-xs-min) { - display: block; + @media (min-width: $screen-sm-min) { + display: inline-block; + width: 200px; + margin-bottom: 0; + } + } + + .label-description { + display: block; + margin-bottom: 10px; + + @media (min-width: $screen-sm-min) { + display: inline-block; + width: 40%; + margin-left: 10px; + margin-bottom: 0; + vertical-align: middle; } } @@ -68,10 +83,6 @@ padding: 3px 4px; } -.label-subscription { - display: inline-block; -} - .dropdown-labels-error { padding: 5px 10px; margin-bottom: 10px; @@ -79,62 +90,27 @@ color: $white-light; } -@mixin labels-mobile { - @media (max-width: $screen-xs-min) { - display: block; - width: 100%; - margin-left: 0; - padding: 10px 0; - } -} - - .manage-labels-list { + .btn-action { + color: $gl-dark-link-color; - .prepend-left-10, .prepend-description-left { - display: inline-block; - width: 40%; - vertical-align: middle; + .fa { + font-size: 18px; + vertical-align: middle; + } - @include labels-mobile; + &:hover { + color: $gl-link-color; + + &.remove-row { + color: $gl-danger; + } + } } - .prepend-description-left { - width: 57%; - - @include labels-mobile; - } - - .pull-info-right { - float: right; - - @media (max-width: $screen-xs-min) { - float: none; - } - - .action-buttons { - border-color: transparent; - padding: 6px; - color: $gl-text-color; - - &.label-subscribe-button { - padding-left: 0; - } - } - - i { - color: $gl-text-color; - } - - .append-right-20 { - a { - color: $gl-text-color; - } - - @media (max-width: $screen-xs-min) { - display: block; - margin-bottom: 10px; - } + .dropdown { + @media (min-width: $screen-sm-min) { + float: right; } } } @@ -186,3 +162,7 @@ color: inherit; } } + +.label-options-toggle { + width: 100%; +} diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index c99b137cda..76e000ef01 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -32,7 +32,7 @@ module LabelsHelper # link_to_label(label) { "My Custom Label Text" } # # Returns a String - def link_to_label(label, project: nil, type: :issue, tooltip: true, &block) + def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: '', &block) project ||= @project || label.project link = send("namespace_project_#{type.to_s.pluralize}_path", project.namespace, @@ -40,9 +40,9 @@ module LabelsHelper label_name: [label.name]) if block_given? - link_to link, &block + link_to link, class: css_class, &block else - link_to render_colored_label(label, tooltip: tooltip), link + link_to render_colored_label(label, tooltip: tooltip), link, class: css_class end end diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 1c51ea676c..9fdebe82f7 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,28 +1,48 @@ - label_css_id = dom_id(label) %li{id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label - .pull-info-right - %span.append-right-20 - = link_to_label(label, type: :merge_request) do - = pluralize label.open_merge_requests_count, 'merge request' - %span.append-right-20 - = link_to_label(label) do - = pluralize label.open_issues_count(current_user), 'open issue' + .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown + %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } + Options + %span.caret + .dropdown-menu.dropdown-menu-align-right + %ul + %li + = link_to_label(label, type: :merge_request) do + = pluralize label.open_merge_requests_count, 'merge request' + %li + = link_to_label(label) do + = pluralize label.open_issues_count(current_user), 'open issue' + - if current_user + %li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } } + %span= label_subscription_toggle_button_text(label) + - if can? current_user, :admin_label, @project + %li + = link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label) + %li + = link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + + .pull-right.hidden-xs.hidden-sm.hidden-md + = link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do + = pluralize label.open_merge_requests_count, 'merge request' + = link_to_label(label, css_class: 'btn btn-transparent btn-action') do + = pluralize label.open_issues_count(current_user), 'open issue' - if current_user - .label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } - .subscription-status{ data: { status: label_subscription_status(label) } } - - %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } } + .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", data: { toggle: "tooltip", status: label_subscription_status(label) } } %span= label_subscription_toggle_button_text(label) - - if can?(current_user, :admin_label, @project) - = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: { toggle: 'tooltip' } do + - if can? current_user, :admin_label, @project + = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do + %span.sr-only Edit %i.fa.fa-pencil-square-o - = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: { confirm: 'Remove this label? Are you sure?', toggle: 'tooltip' } do + = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do + %span.sr-only Delete %i.fa.fa-trash-o -- if current_user - :javascript - new Subscription('##{label_css_id} .label-subscription'); + - if current_user + :javascript + new Subscription('##{dom_id(label)} .label-subscription'); diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index d315a3fe93..478c04318c 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -8,5 +8,6 @@ = icon('star') %span.label-name = link_to_label(label, tooltip: false) - %span.prepend-left-10 - = markdown(label.description, pipeline: :single_line) + - if label.description + %span.label-description + = markdown(label.description, pipeline: :single_line) From 051dc1d263b6be305c30e928238f4f7389200433 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 24 May 2016 14:16:22 +0100 Subject: [PATCH 455/507] Fixed failing tests --- app/helpers/labels_helper.rb | 2 +- features/steps/project/issues/labels.rb | 2 +- features/steps/project/labels.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 76e000ef01..5074e64576 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -32,7 +32,7 @@ module LabelsHelper # link_to_label(label) { "My Custom Label Text" } # # Returns a String - def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: '', &block) + def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block) project ||= @project || label.project link = send("namespace_project_#{type.to_s.pluralize}_path", project.namespace, diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index e02b57bbf8..2937d5d7ca 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -9,7 +9,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I remove label \'bug\'' do page.within "#label_#{bug_label.id}" do - click_link 'Delete' + first(:link, 'Delete').click end end diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb index 5bb0218902..eff05783bc 100644 --- a/features/steps/project/labels.rb +++ b/features/steps/project/labels.rb @@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps private def subscribe_button - first('.label-subscribe-button span') + first('.label-subscribe-button span', visible: true) end end From 9830f9a23b8b212132b624b5d687c3cb815fd50d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 25 May 2016 09:43:03 +0100 Subject: [PATCH 456/507] Updated subscribe icon --- app/assets/javascripts/subscription.js.coffee | 5 +++++ app/assets/stylesheets/pages/labels.scss | 16 ++++++++++++++++ app/views/projects/labels/_label.html.haml | 10 ++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 1a430f3aa4..08d494aba9 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -19,3 +19,8 @@ class @Subscription action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe' btn.find('span').text(action) @subscription_status.find('>div').toggleClass('hidden') + + if btn.attr('data-original-title') + btn.tooltip('hide') + .attr('data-original-title', action) + .tooltip('fixTitle') diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 335bdda13d..bc65404a74 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -166,3 +166,19 @@ .label-options-toggle { width: 100%; } + +.label-subscribe-button { + .label-subscribe-button-loading { + display: none; + } + + &.disabled { + .label-subscribe-button-icon { + display: none; + } + + .label-subscribe-button-loading { + display: block; + } + } +} diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 9fdebe82f7..73c6f2a046 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -32,16 +32,18 @@ - if current_user .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } - %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", data: { toggle: "tooltip", status: label_subscription_status(label) } } - %span= label_subscription_toggle_button_text(label) + %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } } + %span.sr-only= label_subscription_toggle_button_text(label) + = icon('eye', class: 'label-subscribe-button-icon') + = icon('spinner spin', class: 'label-subscribe-button-loading') - if can? current_user, :admin_label, @project = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do %span.sr-only Edit - %i.fa.fa-pencil-square-o + = icon('pencil-square-o') = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do %span.sr-only Delete - %i.fa.fa-trash-o + = icon('trash-o') - if current_user :javascript From 8e8ec82d35a1e905d95b96dfb476e00eff2f9bc0 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 25 May 2016 15:08:00 +0100 Subject: [PATCH 457/507] Tests update --- features/steps/project/labels.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb index eff05783bc..59a780073c 100644 --- a/features/steps/project/labels.rb +++ b/features/steps/project/labels.rb @@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps private def subscribe_button - first('.label-subscribe-button span', visible: true) + first('.js-subscribe-button span', visible: true) end end From b0a80f69e3f96b4f259a8b4ee9cedc7e6b64745a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 26 May 2016 09:37:54 +0100 Subject: [PATCH 458/507] Fixed failing label subscribe test --- features/steps/project/labels.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb index 59a780073c..118ffef477 100644 --- a/features/steps/project/labels.rb +++ b/features/steps/project/labels.rb @@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps private def subscribe_button - first('.js-subscribe-button span', visible: true) + first('.js-subscribe-button', visible: true) end end From d1506ebeeca6e9bf9d524b0b4174bd81ae5c57a4 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 27 May 2016 10:46:06 -0600 Subject: [PATCH 459/507] Add ISSUE_TEMPLATE.md and PULL_REQUEST_TEMPLATE.md to point contributors toward the GitLab.com repository. This adds templates for Issues/Pull Requests in a `.github` directory. These only effect issues/PRs opened in the GitHub mirror of the GitLab project. As we're shutting these down, I thought it'd be good to direct users/contributors to open issues/contribute code in the "correct" project. --- .github/ISSUE_TEMPLATE.md | 3 +++ .github/PULL_REQUEST_TEMPLATE.md | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..9231f80914 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,3 @@ +We’re closing our issue tracker on GitHub so we can focus on the GitLab.com issue tracker and respond to issues more quickly. + +We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). You can login on GitLab using your GitHub account if you'd like to contribute an issue. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..0a7ed80a81 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +Thank you for taking the time to contribute back to GitLab! Due to the high number of contributions we get, we’re unable to review requests on GitHub right now. + +We encourage you to open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests). You can login on GitLab.com using your GitHub account. From 7722caf9d5be1ca419d9c672187fb62c6d21caed Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 9 Jun 2016 11:25:47 -0600 Subject: [PATCH 460/507] Address feedback about wording. --- .github/ISSUE_TEMPLATE.md | 4 ++-- .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9231f80914..2e88b7aa0a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,3 @@ -We’re closing our issue tracker on GitHub so we can focus on the GitLab.com issue tracker and respond to issues more quickly. +We’re closing our issue tracker on GitHub so we can focus on the GitLab.com project and respond to issues more quickly. -We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). You can login on GitLab using your GitHub account if you'd like to contribute an issue. +We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). You can log into GitLab.com using your GitHub account. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0a7ed80a81..c3b0402644 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,3 @@ -Thank you for taking the time to contribute back to GitLab! Due to the high number of contributions we get, we’re unable to review requests on GitHub right now. +Thank you for taking the time to contribute back to GitLab! -We encourage you to open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests). You can login on GitLab.com using your GitHub account. +Please open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests), we look forward to reviewing your contribution! You can log into GitLab.com using your GitHub account. From cda68e926c663eb007d2d6ce13f7340eb35af4af Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 8 Jun 2016 18:41:50 -0500 Subject: [PATCH 461/507] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4b75030db9..d81a31f8b4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -55,6 +55,7 @@ v 8.9.0 (unreleased) - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented - Improve issuables APIs performance when accessing notes !4471 - External links now open in a new tab + - Markdown editor now correctly resets the input value on edit cancellation !4175 v 8.8.4 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds @@ -67,7 +68,6 @@ v 8.8.4 (unreleased) - Fix importer for GitHub comments on diff - Disable Webhooks before proceeding with the GitHub import - Added descriptions to notification settings dropdown - - Markdown editor now correctly resets the input value on edit cancellation !4175 v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 From aff3c6999bfcbaea613e64c9bb95d42a3b5b3695 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 9 Jun 2016 14:07:58 -0300 Subject: [PATCH 462/507] Toggling a task in a description with mentions doesn't creates a Todo --- app/services/todo_service.rb | 11 +++++++++-- spec/services/todo_service_spec.rb | 30 ++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index d836512417..8e03ff8ddd 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -20,7 +20,7 @@ class TodoService # * mark all pending todos related to the issue for the current user as done # def update_issue(issue, current_user) - create_mention_todos(issue.project, issue, current_user) + update_issuable(issue, current_user) end # When close an issue we should: @@ -53,7 +53,7 @@ class TodoService # * create a todo for each mentioned user on merge request # def update_merge_request(merge_request, current_user) - create_mention_todos(merge_request.project, merge_request, current_user) + update_issuable(merge_request, current_user) end # When close a merge request we should: @@ -153,6 +153,13 @@ class TodoService create_mention_todos(issuable.project, issuable, author) end + def update_issuable(issuable, author) + # Skip toggling a task list item in a description + return if issuable.tasks? && issuable.updated_tasks.any? + + create_mention_todos(issuable.project, issuable, author) + end + def handle_note(note, author) # Skip system notes, and notes on project snippet return if note.system? || note.for_snippet? diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 6e7ecbd39b..489c920f19 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -18,7 +18,7 @@ describe TodoService, services: true do end describe 'Issues' do - let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) } + let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) } @@ -101,6 +101,19 @@ describe TodoService, services: true do should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end + + it 'does not create todo when when tasks are marked as completed' do + issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") + + service.update_issue(issue, author) + + should_not_create_todo(user: admin, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: assignee, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: member, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) + end end describe '#close_issue' do @@ -210,7 +223,7 @@ describe TodoService, services: true do end describe 'Merge Requests' do - let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: mentions) } + let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) } describe '#new_merge_request' do @@ -253,6 +266,19 @@ describe TodoService, services: true do expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count) end + + it 'does not create todo when when tasks are marked as completed' do + mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") + + service.update_merge_request(mr_assigned, author) + + should_not_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: assignee, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) + end end describe '#close_merge_request' do From 0098468dfb5927b4034d38c7faac44ac238b9385 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 9 Jun 2016 14:08:30 -0300 Subject: [PATCH 463/507] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 17fc4801a6..2be7f1568a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -56,6 +56,7 @@ v 8.9.0 (unreleased) - Improve issuables APIs performance when accessing notes !4471 - External links now open in a new tab - Markdown editor now correctly resets the input value on edit cancellation !4175 + - Toggling a task list item in a issue/mr description does not creates a Todo for mentions v 8.8.4 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds From 6ae82d57075cc8c6eff613877781ac14a09a2040 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 24 May 2016 18:40:27 -0500 Subject: [PATCH 464/507] Fix local timeago on user dashboard --- app/assets/javascripts/activities.js.coffee | 5 ++++- app/assets/javascripts/lib/datetime_utility.js.coffee | 9 ++++++++- app/assets/javascripts/pager.js.coffee | 3 ++- app/assets/javascripts/user_tabs.js.coffee | 3 +++ app/assets/stylesheets/framework/tw_bootstrap.scss | 5 +++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index 5092e824e6..eae985e21a 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -1,11 +1,14 @@ class @Activities constructor: -> - Pager.init 20, true + Pager.init 20, true, false, @fixTooltips $(".event-filter-link").on "click", (event) => event.preventDefault() @toggleFilter($(event.currentTarget)) @reloadActivities() + fixTooltips: -> + gl.utils.localTimeAgo($('.js-timeago', '#activity')) + reloadActivities: -> $(".content_list").html '' Pager.init 20, true diff --git a/app/assets/javascripts/lib/datetime_utility.js.coffee b/app/assets/javascripts/lib/datetime_utility.js.coffee index ad1d1c7048..948d6dbf07 100644 --- a/app/assets/javascripts/lib/datetime_utility.js.coffee +++ b/app/assets/javascripts/lib/datetime_utility.js.coffee @@ -12,6 +12,13 @@ $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) ) - $timeagoEls.timeago() if setTimeago + if setTimeago + $timeagoEls.timeago() + $timeagoEls.tooltip('destroy') + + # Recreate with custom template + $timeagoEls.tooltip( + template: '' + ) ) window diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee index 0ff83b7f0c..8049c5c30e 100644 --- a/app/assets/javascripts/pager.js.coffee +++ b/app/assets/javascripts/pager.js.coffee @@ -1,5 +1,5 @@ @Pager = - init: (@limit = 0, preload, @disable = false) -> + init: (@limit = 0, preload, @disable = false, @callback = $.noop) -> @loading = $('.loading').first() if preload @@ -19,6 +19,7 @@ @loading.hide() success: (data) -> Pager.append(data.count, data.html) + Pager.callback() dataType: "json" append: (count, html) -> diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee index 70614396a4..29dad21fae 100644 --- a/app/assets/javascripts/user_tabs.js.coffee +++ b/app/assets/javascripts/user_tabs.js.coffee @@ -122,6 +122,9 @@ class @UserTabs @parentEl.find(tabSelector).html(data.html) @loaded[action] = true + # Fix tooltips + gl.utils.localTimeAgo($('.js-timeago', tabSelector)) + loadActivities: (source) -> return if @loaded['activity'] is true diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 6a45c34ccb..e3154657c5 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -192,3 +192,8 @@ .text-info:hover { color: $brand-info; } + +// Prevent datetimes on tooltips to break into two lines +.local-timeago { + white-space: nowrap; +} From 340aa444b709291958f2ab3a21352d9e182705d8 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 25 May 2016 14:32:37 -0500 Subject: [PATCH 465/507] Add tests for dates on tooltips --- app/views/users/show.html.haml | 4 +- .../dashboard/datetime_on_tooltips_spec.rb | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 spec/features/dashboard/datetime_on_tooltips_spec.rb diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 8268380daf..92305594a8 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -79,10 +79,10 @@ %li.js-contributed-tab = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do Contributed projects - %li.projects-tab + %li.js-projects-tab = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects - %li.snippets-tab + %li.js-snippets-tab = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do Snippets diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb new file mode 100644 index 0000000000..e77db0dd2a --- /dev/null +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +feature 'Tooltips on .timeago dates', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, name: 'test', namespace: user.namespace) } + let(:created_date) { Date.yesterday.to_time } + let(:expected_format) { created_date.strftime('%b %d, %Y %l:%M%P UTC') } + + context 'on the activity tab' do + before do + project.team << [user, :master] + + Event.create( project: project, author_id: user.id, action: Event::JOINED, + updated_at: created_date, created_at: created_date) + + login_as user + visit user_path(user) + wait_for_ajax() + + page.find('.js-timeago').hover + end + + it 'has the datetime formated correctly' do + expect(page).to have_selector('.local-timeago', text: expected_format) + end + end + + context 'on the snippets tab' do + before do + project.team << [user, :master] + create(:snippet, author: user, updated_at: created_date, created_at: created_date) + + login_as user + visit user_snippets_path(user) + wait_for_ajax() + + page.find('.js-timeago').hover + end + + it 'has the datetime formated correctly' do + expect(page).to have_selector('.local-timeago', text: expected_format) + end + end +end From 89523396ba3e0af1a0b3a90cdae5b28d06219816 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 9 Jun 2016 12:31:18 -0500 Subject: [PATCH 466/507] Update method name for better understanding --- app/assets/javascripts/activities.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index eae985e21a..ed5a5d0260 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -1,12 +1,12 @@ class @Activities constructor: -> - Pager.init 20, true, false, @fixTooltips + Pager.init 20, true, false, @updateTooltips $(".event-filter-link").on "click", (event) => event.preventDefault() @toggleFilter($(event.currentTarget)) @reloadActivities() - fixTooltips: -> + updateTooltips: -> gl.utils.localTimeAgo($('.js-timeago', '#activity')) reloadActivities: -> From 541e663c125f322cdbb680a9081f2e8470d9ef4e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 9 Jun 2016 14:35:11 -0500 Subject: [PATCH 467/507] Change date format to be non zero padded in order to fix failing test --- spec/features/dashboard/datetime_on_tooltips_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index e77db0dd2a..365cb445df 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -6,7 +6,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do let(:user) { create(:user) } let(:project) { create(:project, name: 'test', namespace: user.namespace) } let(:created_date) { Date.yesterday.to_time } - let(:expected_format) { created_date.strftime('%b %d, %Y %l:%M%P UTC') } + let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P UTC') } context 'on the activity tab' do before do From cc971f03096b8298567caf0b9e0ae738bead03a7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 6 Jun 2016 09:26:47 +0100 Subject: [PATCH 468/507] Improved the UX of issue & milestone date picker Closes #18198 --- app/assets/stylesheets/framework/jquery.scss | 43 +++++++++++++------ app/views/groups/milestones/new.html.haml | 3 +- app/views/projects/milestones/_form.html.haml | 3 +- app/views/shared/issuable/_form.html.haml | 4 +- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 525ed81b05..30a5b837d6 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -2,6 +2,7 @@ font-family: $regular_font; font-size: $font-size-base; + &.ui-datepicker, &.ui-datepicker-inline { border: 1px solid #ddd; padding: 10px; @@ -10,6 +11,25 @@ .ui-datepicker-header { background: #fff; border-color: #ddd; + + .ui-datepicker-prev, + .ui-datepicker-next { + top: 4px; + } + + .ui-datepicker-prev { + left: 2px; + } + + .ui-datepicker-next { + right: 2px; + } + + .ui-state-hover { + background: transparent; + border: 0; + cursor: pointer; + } } .ui-datepicker-calendar td a { @@ -36,21 +56,18 @@ } .ui-state-highlight { - border: 1px solid #eee; - background: #eee; + border: 0; + background: transparent; } - .ui-state-active { - border: 1px solid $gl-primary; - background: $gl-primary; - color: #fff; - } - - .ui-state-hover, - .ui-state-focus { - border: 1px solid $row-hover; - background: $row-hover; - color: #333; + .ui-datepicker-calendar { + .ui-state-active, + .ui-state-hover, + .ui-state-focus { + border: 1px solid $gl-primary; + background: $gl-primary; + color: #fff; + } } } diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 7d9d27ae1f..ca6c4326d1 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -39,9 +39,8 @@ .col-md-6 .form-group = f.label :due_date, "Due Date", class: "control-label" - .col-sm-10= f.hidden_field :due_date .col-sm-10 - .datepicker + = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" .form-actions = f.submit 'Create Milestone', class: "btn-create btn" diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 687222fa92..f5e2b927da 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -17,9 +17,8 @@ .col-md-6 .form-group = f.label :due_date, "Due Date", class: "control-label" - .col-sm-10= f.hidden_field :due_date .col-sm-10 - .datepicker + = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" .form-actions - if @milestone.new_record? diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index b430251dbf..17e2a7e929 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -88,9 +88,9 @@ .col-lg-6 .form-group = f.label :due_date, "Due date", class: "control-label" - = f.hidden_field :due_date, id: "issuable-due-date" .col-sm-10 - .datepicker + .issuable-form-select-holder + = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" - if issuable.can_move?(current_user) %hr From be7b67d2930ddafaa82ef1bb1df36f2af0ad4941 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 6 Jun 2016 09:29:55 +0100 Subject: [PATCH 469/507] CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 2be7f1568a..618e7103fa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -57,6 +57,7 @@ v 8.9.0 (unreleased) - External links now open in a new tab - Markdown editor now correctly resets the input value on edit cancellation !4175 - Toggling a task list item in a issue/mr description does not creates a Todo for mentions + - Improved UX of date pickers on issue & milestone forms v 8.8.4 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds From 7c88141b958c372b8a787c1d724bd8e5bd66f024 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 6 Jun 2016 11:11:57 +0100 Subject: [PATCH 470/507] Fixed tests --- spec/features/issues_spec.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 460d7f82b3..f6fb6a72d2 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -75,12 +75,13 @@ describe 'Issues', feature: true do fill_in 'issue_title', with: 'bug 345' fill_in 'issue_description', with: 'bug description' + find('#issuable-due-date').click - page.within '.datepicker' do + page.within '.ui-datepicker' do click_link date.day end - expect(find('#issuable-due-date', visible: false).value).to eq date.to_s + expect(find('#issuable-due-date').value).to eq date.to_s click_button 'Submit issue' @@ -100,18 +101,19 @@ describe 'Issues', feature: true do it 'should save with due date' do date = Date.today.at_beginning_of_month - expect(find('#issuable-due-date', visible: false).value).to eq date.to_s + expect(find('#issuable-due-date').value).to eq date.to_s date = date.tomorrow fill_in 'issue_title', with: 'bug 345' fill_in 'issue_description', with: 'bug description' + find('#issuable-due-date').click - page.within '.datepicker' do + page.within '.ui-datepicker' do click_link date.day end - expect(find('#issuable-due-date', visible: false).value).to eq date.to_s + expect(find('#issuable-due-date').value).to eq date.to_s click_button 'Save changes' From e885c2fd314bf1d4bc5943f68170150e92d756f4 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 9 Jun 2016 23:15:50 +0300 Subject: [PATCH 471/507] Ignore frequent emojis in search. --- app/assets/javascripts/awards_handler.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 58fd8f0590..ddbce1f532 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -336,7 +336,7 @@ class @AwardsHandler if $.cookie 'frequently_used_emojis' frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - ul = $("
                                          ") + ul = $("
                                            ") for emoji in frequentlyUsedEmojis $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) @@ -367,4 +367,4 @@ class @AwardsHandler searchEmojis: (term) -> - $(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone() + $(".emoji-menu-list:not(.frequent) [data-emoji*='#{term}']").closest('li').clone() From 4456b41b6a2355678d60dccb5982db518ea788ff Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Jun 2016 13:51:10 +0100 Subject: [PATCH 472/507] Checks for undefined when inserting autocomplete into textarea --- app/assets/javascripts/gfm_auto_complete.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index b13a431a52..2c7a8fe2a3 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -35,7 +35,7 @@ GitLab.GfmAutoComplete = $.fn.atwho.default.callbacks.filter(query, data, searchKey) beforeInsert: (value) -> - if value.indexOf('undefined') + if value.indexOf('undefined') is 1 @at else value From ef48dd01cfcd32ace716c716078d7ca5137cf0ca Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Jun 2016 14:01:24 +0100 Subject: [PATCH 473/507] Checks based on whether data is loaded not undefined --- app/assets/javascripts/gfm_auto_complete.js.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 2c7a8fe2a3..76c3083232 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -3,6 +3,7 @@ window.GitLab ?= {} GitLab.GfmAutoComplete = dataLoading: false + dataLoaded: false dataSource: '' @@ -35,7 +36,7 @@ GitLab.GfmAutoComplete = $.fn.atwho.default.callbacks.filter(query, data, searchKey) beforeInsert: (value) -> - if value.indexOf('undefined') is 1 + if not GitLab.GfmAutoComplete.dataLoaded @at else value @@ -182,6 +183,8 @@ GitLab.GfmAutoComplete = $.getJSON(dataSource) loadData: (data) -> + @dataLoaded = true + # load members @input.atwho 'load', '@', data.members # load issues From bf92ea687f7085d8ea0168f47507d0bc459b6d36 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 9 Jun 2016 14:51:57 -0700 Subject: [PATCH 474/507] Properly quote table name in Rake task for MySQL and PostgreSQL compatibility !4318 broke the gitlab:db:drop_tables functionality for PostgreSQL. Closes #15259 --- lib/tasks/gitlab/db.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index e313553254..7230b9485b 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -34,7 +34,7 @@ namespace :gitlab do # PG: http://www.postgresql.org/docs/current/static/ddl-depend.html # MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html # Add `IF EXISTS` because cascade could have already deleted a table. - tables.each { |t| connection.execute("DROP TABLE IF EXISTS `#{t}` CASCADE") } + tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{connection.quote_table_name(t)} CASCADE") } end desc 'Configures the database by running migrate, or by loading the schema and seeding if needed' From 12483e898b2dc94d03c4a633c199256be31a38fb Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 9 Jun 2016 08:42:50 -0500 Subject: [PATCH 475/507] Update activity SVG path --- app/views/layouts/nav/_project.html.haml | 1 - app/views/shared/icons/_activity.svg | 15 ++++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2a58ef224b..ca99ba8def 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -129,5 +129,4 @@ %li.hidden = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do Commits - .fade-right diff --git a/app/views/shared/icons/_activity.svg b/app/views/shared/icons/_activity.svg index c87794b906..d465504b15 100644 --- a/app/views/shared/icons/_activity.svg +++ b/app/views/shared/icons/_activity.svg @@ -3,13 +3,14 @@ path-1 Created with Sketch. - - - + - - - - + + + + + + + \ No newline at end of file From e328eab0da46f1a5d4fbf2289911ce8f4093cfb6 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 9 Jun 2016 18:32:10 -0400 Subject: [PATCH 476/507] Update CHANGELOG for 8.8.4 and 8.8.5 [ci skip] --- CHANGELOG | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 833a3c1848..b00c149a75 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.9.0 (unreleased) - Allow customisable text on the 'nearly there' page after a user signs up - Bump recaptcha gem to 3.0.0 to remove deprecated stoken support - Allow forking projects with restricted visibility level + - Added descriptions to notification settings dropdown - Improve note validation to prevent errors when creating invalid note via API - Reduce number of fog gem dependencies - Remove project notification settings associated with deleted projects @@ -22,6 +23,7 @@ v 8.9.0 (unreleased) - `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix - Bump nokogiri to 1.6.8 - Use gitlab-shell v3.0.0 + - Upgrade to jQuery 2 - Use Knapsack to evenly distribute tests across multiple nodes - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged - Don't allow MRs to be merged when commits were added since the last review / page load @@ -60,17 +62,17 @@ v 8.9.0 (unreleased) - Toggling a task list item in a issue/mr description does not creates a Todo for mentions - Improved UX of date pickers on issue & milestone forms -v 8.8.4 (unreleased) +v 8.8.5 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds - Fix issue with arrow keys not working in search autocomplete dropdown - Fix todos page throwing errors when you have a project pending deletion - Reduce number of SQL queries when rendering user references - - Upgrade to jQuery 2 - - Remove prev/next buttons on issues and merge requests - Import GitHub repositories respecting the API rate limit - Fix importer for GitHub comments on diff - Disable Webhooks before proceeding with the GitHub import - - Added descriptions to notification settings dropdown + +v 8.8.4 + - Fix LDAP-based login for users with 2FA enabled. !4493 v 8.8.3 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 From 94826d9abe13a20aae0868a096a03d2a4e706d82 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 10 Jun 2016 01:56:41 +0300 Subject: [PATCH 477/507] Minor MR comment fixes. yes -> true no -> false . frequent -> .frequent-emojis --- app/assets/javascripts/awards_handler.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index ddbce1f532..136db8ee14 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -100,7 +100,7 @@ class @AwardsHandler $menu.css(css) - addAward: (votesBlock, awardUrl, emoji, checkMutuality = yes, callback) -> + addAward: (votesBlock, awardUrl, emoji, checkMutuality = true, callback) -> emoji = @normilizeEmojiName emoji @@ -111,7 +111,7 @@ class @AwardsHandler $('.emoji-menu').removeClass 'is-visible' - addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = yes) -> + addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = true) -> @checkMutuality votesBlock, emoji if checkForMutuality @addEmojiToFrequentlyUsedList emoji @@ -153,7 +153,7 @@ class @AwardsHandler if isAlreadyVoted @showEmojiLoader $emojiButton - @addAward votesBlock, awardUrl, mutualVote, no, -> + @addAward votesBlock, awardUrl, mutualVote, false, -> $emojiButton.removeClass 'is-loading' @@ -336,14 +336,14 @@ class @AwardsHandler if $.cookie 'frequently_used_emojis' frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - ul = $("
                                              ") + ul = $("
                                                ") for emoji in frequentlyUsedEmojis $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) $('input.emoji-search').after(ul).after($('
                                                ').text('Frequently used')) - @frequentEmojiBlockRendered = yes + @frequentEmojiBlockRendered = true setupSearch: -> @@ -367,4 +367,4 @@ class @AwardsHandler searchEmojis: (term) -> - $(".emoji-menu-list:not(.frequent) [data-emoji*='#{term}']").closest('li').clone() + $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='#{term}']").closest('li').clone() From 99d5a91d7a1942f092c87010cbf93279979822a1 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 10 Jun 2016 14:04:27 +0530 Subject: [PATCH 478/507] Fix failing `EmailOnPush` spec. --- spec/lib/disable_email_interceptor_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb index c2a7b20b84..309a88151c 100644 --- a/spec/lib/disable_email_interceptor_spec.rb +++ b/spec/lib/disable_email_interceptor_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe DisableEmailInterceptor, lib: true do before do - ActionMailer::Base.register_interceptor(DisableEmailInterceptor) + Mail.register_interceptor(DisableEmailInterceptor) end it 'should not send emails' do @@ -14,7 +14,7 @@ describe DisableEmailInterceptor, lib: true do # Removing interceptor from the list because unregister_interceptor is # implemented in later version of mail gem # See: https://github.com/mikel/mail/pull/705 - Mail.class_variable_set(:@@delivery_interceptors, []) + Mail.unregister_interceptor(DisableEmailInterceptor) end def deliver_mail From e0a90c467c4ffaf6fec03ad54afa5f18b71a6aa3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Jun 2016 09:10:36 +0100 Subject: [PATCH 479/507] Made the awardable buttons prettier when active Closes #18379 --- app/assets/stylesheets/pages/awards.scss | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 05d1ee5b99..6211f3a52e 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -101,13 +101,21 @@ line-height: 20px; outline: 0; + &:hover, &.active, &:active { - background-color: $white-dark; + background-color: $row-hover; + border-color: $row-hover-border; box-shadow: none; outline: 0; } + &.btn { + &:focus { + outline: 0; + } + } + &.is-loading { .award-control-icon-normal, .emoji-icon { From b2b3fb6c0130af4d8b08ed2a92d76c269fb226eb Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Jun 2016 15:58:02 +0100 Subject: [PATCH 480/507] Revert change to search all users --- app/assets/javascripts/users_select.js.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index de0eae58bf..88246b0feb 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -95,7 +95,7 @@ class @UsersSelect data: (term, callback) => isAuthorFilter = $('.js-author-search') - @users term, term is '' and isAuthorFilter, (users) => + @users term, (users) => if term.length is 0 showDivider = 0 @@ -221,7 +221,7 @@ class @UsersSelect multiple: $(select).hasClass('multiselect') minimumInputLength: 0 query: (query) => - @users query.term, @projectId?, (users) => + @users query.term, (users) => data = { results: users } if query.term.length == 0 @@ -304,7 +304,7 @@ class @UsersSelect # Return users list. Filtered by query # Only active users retrieved - users: (query, fromProject, callback) => + users: (query, callback) => url = @buildUrl(@usersPath) $.ajax( @@ -313,7 +313,7 @@ class @UsersSelect search: query per_page: 20 active: true - project_id: @projectId if fromProject + project_id: @projectId group_id: @groupId current_user: @showCurrentUser author_id: @authorId From a4b3bdabd52494f46fee73271d7b343e6b7e08e9 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Jun 2016 08:52:08 +0100 Subject: [PATCH 481/507] removed tests needed for any author :poop: --- spec/features/issues/filter_issues_spec.rb | 36 ---------------------- 1 file changed, 36 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 7efbaaa048..1f0594e6b0 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -294,40 +294,4 @@ describe 'Filter issues', feature: true do end end end - - describe 'filter by any author', js: true do - before do - user2 = create(:user, name: "tester") - create(:issue, project: project, author: user) - create(:issue, project: project, author: user2) - - visit namespace_project_issues_path(project.namespace, project) - end - - it 'should show filter by any author link' do - click_button "Author" - fill_in "Search authors", with: "tester" - - page.within ".dropdown-menu-author" do - expect(page).to have_content "tester" - end - end - - it 'should show filter issues by any author' do - page.within '.issues-list' do - expect(page).to have_selector ".issue", count: 2 - end - - click_button "Author" - fill_in "Search authors", with: "tester" - - page.within ".dropdown-menu-author" do - click_link "tester" - end - - page.within '.issues-list' do - expect(page).to have_selector ".issue", count: 1 - end - end - end end From 1d95958ba06c7d42c76f8a1599f831ce5251acca Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Jun 2016 08:28:03 +0100 Subject: [PATCH 482/507] Changelog fix for search dropdown arrow keys fix --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b00c149a75..604007d822 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 8.9.0 (unreleased) - Fix endless redirections when accessing user OAuth applications when they are disabled - Allow enabling wiki page events from Webhook management UI - Bump rouge to 1.11.0 + - Fix issue with arrow keys not working in search autocomplete dropdown - Make EmailsOnPushWorker use Sidekiq mailers queue - Fix wiki page events' webhook to point to the wiki repository - Fix issue todo not remove when leave project !4150 (Long Nguyen) @@ -64,7 +65,6 @@ v 8.9.0 (unreleased) v 8.8.5 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds - - Fix issue with arrow keys not working in search autocomplete dropdown - Fix todos page throwing errors when you have a project pending deletion - Reduce number of SQL queries when rendering user references - Import GitHub repositories respecting the API rate limit From 10f17c2fcd50d4e4b5bad59a5ae2fb4a13a037d8 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Jun 2016 09:00:57 +0100 Subject: [PATCH 483/507] Correctly shows label errors in dropdown Fixes #18344 --- app/assets/javascripts/labels_select.js.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index ec74dfaae1..439c315339 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -95,8 +95,11 @@ class @LabelsSelect $newLabelCreateButton.enable() if label.message? + errors = _.map label.message, (value, key) -> + "#{key} #{value[0]}" + $newLabelError - .text label.message + .html errors.join("
                                                ") .show() else $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' From 701e2df7e55113dafd48c570baad44bf7f24f863 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 10 Jun 2016 12:29:15 +0200 Subject: [PATCH 484/507] Satisfy Rubocop --- lib/api/helpers.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 0e47bb0b8a..e1d3bbcc02 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -412,11 +412,11 @@ module API def send_git_blob(repository, blob) env['api.format'] = :txt content_type 'text/plain' - header *Gitlab::Workhorse.send_git_blob(repository, blob) + header(*Gitlab::Workhorse.send_git_blob(repository, blob)) end def send_git_archive(repository, ref:, format:) - header *Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format) + header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) end end end From dc6ec2adf82c2b2fe1ab6ef076432fd741d3afbb Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 20 Apr 2016 10:11:22 +0100 Subject: [PATCH 485/507] CI build page UI update Added sidebar Removed elements not present in design --- app/assets/javascripts/application.js.coffee | 4 +- app/assets/javascripts/ci/build.coffee | 91 ++++++--- app/assets/stylesheets/framework/sidebar.scss | 4 +- .../stylesheets/framework/variables.scss | 3 + app/assets/stylesheets/pages/builds.scss | 91 +++++++-- app/assets/stylesheets/pages/issuable.scss | 6 +- app/assets/stylesheets/pages/xterm.scss | 19 +- app/controllers/projects/builds_controller.rb | 2 +- app/helpers/nav_helper.rb | 2 + app/views/projects/artifacts/browse.html.haml | 1 + app/views/projects/builds/_header.html.haml | 16 ++ app/views/projects/builds/_sidebar.html.haml | 93 +++++++++ app/views/projects/builds/_user.html.haml | 4 + app/views/projects/builds/show.html.haml | 189 ++---------------- features/steps/project/builds/artifacts.rb | 4 +- spec/features/builds_spec.rb | 27 +-- 16 files changed, 306 insertions(+), 250 deletions(-) create mode 100644 app/views/projects/builds/_header.html.haml create mode 100644 app/views/projects/builds/_sidebar.html.haml create mode 100644 app/views/projects/builds/_user.html.haml diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index b28327ce12..e0ca546350 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -267,8 +267,8 @@ $ -> $(document).trigger('breakpoint:change', [bootstrapBreakpoint]) $(window) - .off "resize" - .on "resize", (e) -> + .off "resize.app" + .on "resize.app", (e) -> fitSidebarForSize() gl.awardsHandler = new AwardsHandler() diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 98d05e4127..f763ba96e3 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -1,19 +1,31 @@ -class CiBuild +class @CiBuild @interval: null @state: null - constructor: (build_url, build_status, build_state) -> + constructor: (@build_url, @build_status, @state) -> clearInterval(CiBuild.interval) - @state = build_state + # Init breakpoint checker + @bp = Breakpoints.get() + @hideSidebar() + $('.js-build-sidebar').niceScroll() + $(document) + .off 'click', '.js-sidebar-build-toggle' + .on 'click', '.js-sidebar-build-toggle', @toggleSidebar - @initScrollButtonAffix() + $(window) + .off 'resize.build' + .on 'resize.build', @hideSidebar - if build_status == "running" || build_status == "pending" + if $('#build-trace').length + @getInitialBuildTrace() + @initScrollButtonAffix() + + if @build_status is "running" or @build_status is "pending" # # Bind autoscroll button to follow build output # - $("#autoscroll-button").bind "click", -> + $('#autoscroll-button').on 'click', -> state = $(this).data("state") if "enabled" is state $(this).data "state", "disabled" @@ -27,26 +39,37 @@ class CiBuild # Only valid for runnig build when output changes during time # CiBuild.interval = setInterval => - if window.location.href.split("#").first() is build_url - last_state = @state - $.ajax - url: build_url + "/trace.json?state=" + encodeURIComponent(@state) - dataType: "json" - success: (log) => - return unless last_state is @state - - if log.state and log.status is "running" - @state = log.state - if log.append - $('.fa-refresh').before log.html - else - $('#build-trace code').html log.html - $('#build-trace code').append '' - @checkAutoscroll() - else if log.status isnt build_status - Turbolinks.visit build_url + if window.location.href.split("#").first() is @build_url + @getBuildTrace() , 4000 + getInitialBuildTrace: -> + $.ajax + url: @build_url + dataType: 'json' + success: (build_data) -> + $('.js-build-output').html build_data.trace_html + + if build_data.status is 'success' or build_data.status is 'failed' + $('.js-build-refresh').remove() + + getBuildTrace: -> + $.ajax + url: "#{@build_url}/trace.json?state=#{encodeURIComponent(@state)}" + dataType: "json" + success: (log) => + if log.state + @state = log.state + + if log.status is "running" + if log.append + $('.js-build-output').append log.html + else + $('.js-build-output').html log.html + @checkAutoscroll() + else if log.status isnt @build_status + Turbolinks.visit @build_url + checkAutoscroll: -> $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") @@ -61,4 +84,22 @@ class CiBuild $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top) ) -@CiBuild = CiBuild + shouldHideSidebar: -> + bootstrapBreakpoint = @bp.getBreakpointSize() + + bootstrapBreakpoint is 'xs' or bootstrapBreakpoint is 'sm' + + toggleSidebar: => + if @shouldHideSidebar() + $('.js-build-sidebar') + .toggleClass 'right-sidebar-expanded right-sidebar-collapsed' + + hideSidebar: => + if @shouldHideSidebar() + $('.js-build-sidebar') + .removeClass 'right-sidebar-expanded' + .addClass 'right-sidebar-collapsed' + else + $('.js-build-sidebar') + .removeClass 'right-sidebar-collapsed' + .addClass 'right-sidebar-expanded' diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 9498541374..06a688690f 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -273,7 +273,9 @@ padding-right: 0; @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - padding-right: $sidebar_collapsed_width; + &:not(.build-sidebar) { + padding-right: $sidebar_collapsed_width; + } } @media (min-width: $screen-md-min) { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 99e3df119e..847b2f80bd 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -260,3 +260,6 @@ $calendar-header-color: #b8b8b8; $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); $calendar-unselectable-bg: #faf9f9; + +$ci-output-bg: #1d1f21; +$ci-text-color: #c5c8c6; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 44222e8e8a..e8f1935d23 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -53,37 +53,92 @@ left: 70px; } } +} - .build-widget { - padding: 10px; - background: $background-color; - margin-bottom: 20px; - border-radius: 4px; +.build-header { + position: relative; + padding-right: 40px; - .title { - margin-top: 0; - color: #666; - line-height: 1.5; - } - .attr-name { - color: #777; + @media (min-width: $screen-sm-min) { + padding-right: 0; + } + + a { + color: $gl-gray; + + &:hover { + color: $gl-link-color; + text-decoration: none; } } - .alert-disabled { - background: $background-color; + code { + color: $code-color; + } - a { - color: #3084bb !important; - } + .avatar { + float: none; + margin-right: 2px; + margin-left: 2px; } } table.builds { - .build-link { a { color: $gl-dark-link-color; } } } + +.build-trace { + background: $ci-output-bg; + color: $ci-text-color; + white-space: pre; + overflow-x: auto; + font-size: 12px; + + .fa-refresh { + font-size: 24px; + } + + .bash { + display: block; + } +} + +.right-sidebar.build-sidebar { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + + &.right-sidebar-collapsed { + display: none; + } + + .block { + width: 100%; + } + + .build-sidebar-header { + padding-top: 0; + + .gutter-toggle { + margin-top: 0; + } + } +} + +.build-detail-row { + margin-bottom: 5px; +} + +.build-light-text { + color: $gl-placeholder-color; +} + +.build-gutter-toggle { + position: absolute; + top: 50%; + right: 0; + margin-top: -17px; +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 787c387379..ea453ce356 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -29,7 +29,7 @@ } } -.issuable-sidebar { +.right-sidebar { a { color: inherit; } @@ -74,6 +74,10 @@ } } + .block-first { + padding-top: 0; + } + .title { color: $gl-text-color; margin-bottom: 10px; diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 3f28e40292..8d855ce99b 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -11,18 +11,15 @@ $magenta: #cd00cd; $cyan: #00cdcd; $white: #e5e5e5; - $l-black: #7f7f7f; - $l-red: #f00; - $l-green: #0f0; - $l-yellow: #ff0; - $l-blue: #5c5cff; - $l-magenta: #f0f; - $l-cyan: #0ff; - $l-white: #fff; + $l-black: #373b41; + $l-red: #c66; + $l-green: #b5bd68; + $l-yellow: #f0c674; + $l-blue: #81a2be; + $l-magenta: #b294bb; + $l-cyan: #8abeb7; + $l-white: $ci-text-color; - .term-bold { - font-weight: bold; - } .term-italic { font-style: italic; } diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 9b80efa5f1..14c8282634 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].presence).merge!(id: @build.id, status: @build.status) end end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index f685e54753..39831ce2e4 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -30,6 +30,8 @@ module NavHelper else "page-gutter right-sidebar-expanded" end + elsif current_path?('builds#show') + "page-gutter build-sidebar right-sidebar-expanded" end end diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index ede01dcc1a..539d07d634 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,4 +1,5 @@ - page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' +- header_title project_title(@project, "Builds", project_builds_path(@project)) .top-block.row-content-block.clearfix .pull-right diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml new file mode 100644 index 0000000000..46561249bb --- /dev/null +++ b/app/views/projects/builds/_header.html.haml @@ -0,0 +1,16 @@ +.content-block.build-header + = ci_status_with_icon(@build.status) + Build + %strong ##{@build.id} + for commit + = link_to ci_status_path(@build.commit) do + %strong= @build.pipeline.short_sha + from + = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do + %code + = @build.ref + - if @build.user + = render "user" + = time_ago_with_tooltip(@build.created_at) + %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } + = icon('angle-double-left') diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml new file mode 100644 index 0000000000..5d931389df --- /dev/null +++ b/app/views/projects/builds/_sidebar.html.haml @@ -0,0 +1,93 @@ +%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar + .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default + Build + %strong ##{@build.id} + %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } + = icon('angle-double-right') + - if @build.coverage + .block.block-first + .title + Test coverage + %p.build-detail-row + #{@build.coverage}% + + - if can?(current_user, :read_build, @project) && @build.artifacts? + .block{ class: ("block-first" if !@build.coverage) } + .title + Build artifacts + .btn-group.btn-group-justified{ role: :group } + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do + Download + + - if @build.artifacts_metadata? + = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do + Browse + + .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && @build.artifacts?)) } + .title + Build details + - if @build.retryable? + = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post + - if @build.merge_request + %p.build-detail-row + %span.build-light-text Merge Request: + = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request) + - if @build.duration + %p.build-detail-row + %span.build-light-text Duration: + #{duration_in_words(@build.finished_at, @build.started_at)} + - if @build.finished_at + %p.build-detail-row + %span.build-light-text Finished: + #{time_ago_with_tooltip(@build.finished_at)} + - if @build.erased_at + %p.build-detail-row + %span.build-light-text Erased: + #{time_ago_with_tooltip(@build.erased_at)} + %p.build-detail-row + %span.build-light-text Runner: + - if @build.runner && current_user && current_user.admin + = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id) + - elsif @build.runner + \##{@build.runner.id} + .btn-group.btn-group-justified{ role: :group } + - if @build.has_trace? + = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' + - if @build.active? + = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post + - if can?(current_user, :update_build, @project) && @build.erasable? + = link_to erase_namespace_project_build_path(@project.namespace, @project, @build), + class: "btn btn-sm btn-default", method: :post, + data: { confirm: "Are you sure you want to erase this build?" } do + Erase + + - if @build.trigger_request + .build-widget + %h4.title + Trigger + + %p + %span.build-light-text Token: + #{@build.trigger_request.trigger.short_token} + + - if @build.trigger_request.variables + %p + %span.build-light-text Variables: + + %code + - @build.trigger_request.variables.each do |key, value| + #{key}=#{value} + + .block + .title + Commit message + %p.build-light-text.append-bottom-0 + #{@build.pipeline.git_commit_message} + + - if @build.tags.any? + .block + .title + Tags + - @build.tag_list.each do |tag| + %span.label.label-primary + = tag diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/builds/_user.html.haml new file mode 100644 index 0000000000..2642de8021 --- /dev/null +++ b/app/views/projects/builds/_user.html.haml @@ -0,0 +1,4 @@ +by +%a{ href: user_path(@build.user) } + = image_tag avatar_icon(@build.user, 24), class: "avatar s24" + %strong= @build.user.to_reference diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 5477fc65c2..a26f8aeb31 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,18 +1,10 @@ - page_title "#{@build.name} (##{@build.id})", "Builds" - trace_with_state = @build.trace_with_state +- header_title project_title(@project, "Builds", project_builds_path(@project)) .build-page - .row-content-block.top-block - Build ##{@build.id} for commit - %strong.monospace= link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline) - from - = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) - - merge_request = @build.merge_request - - if merge_request - via - = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request) + = render "header" - #up-build-trace - builds = @build.pipeline.builds.latest.to_a - if builds.size > 1 %ul.nav-links.no-top.no-bottom @@ -33,18 +25,6 @@ · %i.fa.fa-warning This build was retried. - - .row-content-block.middle-block - .build-head - .clearfix - = ci_status_with_icon(@build.status) - - if @build.duration - %span - %i.fa.fa-time - #{duration_in_words(@build.finished_at, @build.started_at)} - .pull-right - #{time_ago_with_tooltip(@build.finished_at) if @build.finished_at} - - if @build.stuck? - unless @build.any_runners_online? .bs-callout.bs-callout-warning @@ -64,158 +44,27 @@ = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do Runners page - .row.prepend-top-default - .col-md-9 - .clearfix - - if @build.active? - .autoscroll-container - %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll - .clearfix + .prepend-top-default + - if @build.active? + .autoscroll-container + %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll #js-build-scroll.scroll-controls - = link_to '#up-build-trace', class: 'btn' do + = link_to '#build-trace', class: 'btn' do %i.fa.fa-angle-up = link_to '#down-build-trace', class: 'btn' do %i.fa.fa-angle-down + - if @build.erased? + .erased.alert.alert-warning + - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by + Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} + - else + %pre.build-trace#build-trace + %code.bash.js-build-output + = icon("refresh spin", class: "js-build-refresh") - - if @build.erased? - .erased.alert.alert-warning - - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by - Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} - - else - %pre.trace#build-trace - %code.bash - = preserve do - = raw trace_with_state[:html] - - if @build.active? - %i{:class => "fa fa-refresh fa-spin"} + #down-build-trace - %div#down-build-trace += render "sidebar" - .col-md-3 - - if @build.coverage - .build-widget - %h4.title - Test coverage - %h1 #{@build.coverage}% - - - if can?(current_user, :read_build, @project) && @build.artifacts? - .build-widget.artifacts - %h4.title Build artifacts - .center - .btn-group{ role: :group } - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do - = icon('download') - Download - - - if @build.artifacts_metadata? - = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do - = icon('folder-open') - Browse - - .build-widget.build-controls - %h4.title - Build ##{@build.id} - - if can?(current_user, :update_build, @project) - .center - .btn-group{ role: :group } - - if @build.active? - = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post - - elsif @build.retryable? - = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post - - - if @build.erasable? - = link_to erase_namespace_project_build_path(@project.namespace, @project, @build), - class: 'btn btn-sm btn-warning', method: :post, - data: { confirm: 'Are you sure you want to erase this build?' } do - = icon('eraser') - Erase - - if @build.has_trace? - = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), - class: 'btn btn-sm btn-success', target: '_blank' - - .clearfix - - if @build.duration - %p - %span.attr-name Duration: - #{duration_in_words(@build.finished_at, @build.started_at)} - %p - %span.attr-name Created: - #{time_ago_with_tooltip(@build.created_at)} - - if @build.finished_at - %p - %span.attr-name Finished: - #{time_ago_with_tooltip(@build.finished_at)} - - if @build.erased_at - %p - %span.attr-name Erased: - #{time_ago_with_tooltip(@build.erased_at)} - %p - %span.attr-name Runner: - - if @build.runner && current_user && current_user.admin - = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id) - - elsif @build.runner - \##{@build.runner.id} - - - if @build.trigger_request - .build-widget - %h4.title - Trigger - - %p - %span.attr-name Token: - #{@build.trigger_request.trigger.short_token} - - - if @build.trigger_request.variables - %p - %span.attr-name Variables: - - %code - - @build.trigger_request.variables.each do |key, value| - #{key}=#{value} - - .build-widget - %h4.title - Commit - .pull-right - %small - = link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline), class: "monospace" - %p - %span.attr-name Branch: - = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) - %p - %span.attr-name Author: - #{@build.pipeline.git_author_name} - %p - %span.attr-name Message: - #{@build.pipeline.git_commit_message} - - - if @build.tags.any? - .build-widget - %h4.title - Tags - - @build.tag_list.each do |tag| - %span.label.label-primary - = tag - - - if @builds.present? - .build-widget - %h4.title #{pluralize(@builds.count(:id), "other build")} for - = succeed ":" do - = link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline), class: "monospace" - %table.table.builds - - @builds.each_with_index do |build, i| - %tr.build - %td - = ci_icon_for_status(build.status) - %td - = link_to namespace_project_build_path(@project.namespace, @project, build) do - - if build.name - = build.name - - else - %span ##{build.id} - - %td.status= build.status - - - :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace_with_state[:state]}") +:javascript + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace_with_state[:state]}") diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb index 1bdb57af9d..2876e8812e 100644 --- a/features/steps/project/builds/artifacts.rb +++ b/features/steps/project/builds/artifacts.rb @@ -5,11 +5,11 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps include RepoHelpers step 'I click artifacts download button' do - page.within('.artifacts') { click_link 'Download' } + click_link 'Download' end step 'I click artifacts browse button' do - page.within('.artifacts') { click_link 'Browse' } + click_link 'Browse' end step 'I should see content of artifacts archive' do diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index df221ab1f3..cb94432dbd 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -93,9 +93,7 @@ describe "Builds" do end it 'has button to download artifacts' do - page.within('.artifacts') do - expect(page).to have_content 'Download' - end + expect(page).to have_content 'Download' end end @@ -107,9 +105,7 @@ describe "Builds" do end it do - page.within('.build-controls') do - expect(page).to have_link 'Raw' - end + expect(page).to have_link 'Raw' end end end @@ -165,15 +161,10 @@ describe "Builds" do end describe "GET /:project/builds/:id/download" do - context "Build from project" do - before do - @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@project.namespace, @project, @build) - page.within('.artifacts') { click_link 'Download' } - end - - it { expect(page.status_code).to eq(200) } - it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } + before do + @build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link 'Download' end context "Build from other project" do @@ -246,10 +237,8 @@ describe "Builds" do it { expect(page.status_code).to eq(200) } end - context "Build from other project" do - before do - visit status_namespace_project_build_path(@project.namespace, @project, @build2) - end + it 'sends the right headers' do + click_link 'Raw' it { expect(page.status_code).to eq(404) } end From a6345c14011e9ffa8cca45f9058a1529c7be5b62 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Jun 2016 12:02:37 +0100 Subject: [PATCH 486/507] Fixed failing tests --- spec/features/builds_spec.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index cb94432dbd..0c865e915c 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -237,8 +237,10 @@ describe "Builds" do it { expect(page.status_code).to eq(200) } end - it 'sends the right headers' do - click_link 'Raw' + context "Build from other project" do + before do + visit status_namespace_project_build_path(@project.namespace, @project, @build2) + end it { expect(page.status_code).to eq(404) } end From bd257c3d38a704e6fd2d020943d1a96bbfe97d6c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Jun 2016 13:37:39 +0100 Subject: [PATCH 487/507] Fixed merge conflict that caused tests to fail with build --- app/views/projects/builds/_header.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 46561249bb..51b5bd9db4 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -3,7 +3,7 @@ Build %strong ##{@build.id} for commit - = link_to ci_status_path(@build.commit) do + = link_to ci_status_path(@build.pipeline) do %strong= @build.pipeline.short_sha from = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do From e7950bd9429d104cc5ef7ca0e0195ecbace4b295 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Jun 2016 10:38:31 +0100 Subject: [PATCH 488/507] Fixed project dropdown being overlapped by sidebar Closes #18410 --- app/assets/stylesheets/framework/header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index c46d6b1478..b8d4233537 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -185,7 +185,7 @@ header { margin-left: 0; .header-content { - padding-left: $sidebar_width; + margin-left: $sidebar_width; transition-duration: .3s; } } From 9dfb809c578735d807070d77ad567ab5c3de9b6c Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 8 Jun 2016 19:39:33 +0200 Subject: [PATCH 489/507] Fix UTF-8 handling in incremental trace update API --- app/models/ci/build.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b8ada6361a..6a64ca451f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -194,7 +194,7 @@ module Ci def trace_length if raw_trace - raw_trace.length + raw_trace.bytesize else 0 end @@ -216,7 +216,7 @@ module Ci recreate_trace_dir File.truncate(path_to_trace, offset) if File.exist?(path_to_trace) - File.open(path_to_trace, 'a') do |f| + File.open(path_to_trace, 'ab') do |f| f.write(trace_part) end end From 72647eda3121566c92941a1e98088c31a77cb9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 10 Jun 2016 13:44:45 +0200 Subject: [PATCH 490/507] Don't require Gitlab::Redis in mail_room.yml if it's already defined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- config/mail_room.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/mail_room.yml b/config/mail_room.yml index 761a32adb9..7cab24b295 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -2,7 +2,7 @@ <% require "yaml" require "json" -require_relative "lib/gitlab/redis" +require_relative "lib/gitlab/redis" unless defined?(Gitlab::Redis) rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" From c43279a8d9b22b063c1963aae3452f2fe96ea3f2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 10 Jun 2016 13:58:33 +0200 Subject: [PATCH 491/507] Fix expose of TriggerRequest --- lib/ci/api/entities.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index 255217b1a7..a902ced35d 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -56,7 +56,7 @@ module Ci class TriggerRequest < Grape::Entity expose :id, :variables - expose :trigger, using: Commit, as: :commit + expose :pipeline, using: Commit, as: :commit end end end From 07dbd6b3884c4f188b2c3f29dd7419791f1051eb Mon Sep 17 00:00:00 2001 From: Rui Anderson Date: Wed, 27 Apr 2016 15:34:42 -0300 Subject: [PATCH 492/507] Allow or not merge MR with failed build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CHANGELOG | 1 + app/assets/javascripts/project_new.js.coffee | 19 +-- app/controllers/projects_controller.rb | 2 +- app/models/merge_request.rb | 6 +- .../_merge_request_settings.html.haml | 11 ++ app/views/projects/edit.html.haml | 2 + .../merge_requests/widget/_open.html.haml | 2 + .../widget/open/_accept.html.haml | 27 ++-- .../widget/open/_build_failed.html.haml | 6 + ...low_merge_if_build_succeeds_to_projects.rb | 15 +++ db/schema.rb | 1 + lib/api/merge_requests.rb | 2 +- .../only_allow_merge_if_build_succeeds.rb | 105 ++++++++++++++++ spec/models/merge_request_spec.rb | 117 ++++++++++++++++++ spec/requests/api/merge_requests_spec.rb | 8 ++ 15 files changed, 301 insertions(+), 23 deletions(-) create mode 100644 app/views/projects/_merge_request_settings.html.haml create mode 100644 app/views/projects/merge_requests/widget/open/_build_failed.html.haml create mode 100644 db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb create mode 100644 spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb diff --git a/CHANGELOG b/CHANGELOG index b00c149a75..176fc16943 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ v 8.9.0 (unreleased) - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Changed the Slack build message to use the singular duration if necessary (Aran Koning) - Links from a wiki page to other wiki pages should be rewritten as expected + - Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos) - Fix issues filter when ordering by milestone - Todos will display target state if issuable target is 'Closed' or 'Merged' - Fix bug when sorting issues by milestone due date and filtering by two or more labels diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee index 63dee4ed5d..e48343a19b 100644 --- a/app/assets/javascripts/project_new.js.coffee +++ b/app/assets/javascripts/project_new.js.coffee @@ -7,12 +7,17 @@ class @ProjectNew @toggleSettingsOnclick() - toggleSettings: -> - checked = $("#project_builds_enabled").prop("checked") - if checked - $('.builds-feature').show() - else - $('.builds-feature').hide() + toggleSettings: => + @_showOrHide('#project_builds_enabled', '.builds-feature') + @_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature') toggleSettingsOnclick: -> - $("#project_builds_enabled").on 'click', @toggleSettings + $('#project_builds_enabled, #project_merge_requests_enabled').on 'click', @toggleSettings + + _showOrHide: (checkElement, container) -> + $container = $(container) + + if $(checkElement).prop('checked') + $container.show() + else + $container.hide() diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3af62c7696..a6479c42d9 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -234,7 +234,7 @@ class ProjectsController < Projects::ApplicationController :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, + :public_builds, :only_allow_merge_if_build_succeeds ) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b0ed818285..43c6bcb871 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -260,7 +260,7 @@ class MergeRequest < ActiveRecord::Base end def mergeable? - return false unless open? && !work_in_progress? && !broken? + return false if !open? || work_in_progress? || broken? || cannot_be_merged_because_build_failed? check_if_can_be_merged @@ -481,6 +481,10 @@ class MergeRequest < ActiveRecord::Base ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) end + def cannot_be_merged_because_build_failed? + project.only_allow_merge_if_build_succeeds? && ci_commit && ci_commit.failed? + end + def state_human_name if merged? "Merged" diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml new file mode 100644 index 0000000000..da522b5341 --- /dev/null +++ b/app/views/projects/_merge_request_settings.html.haml @@ -0,0 +1,11 @@ +%fieldset.builds-feature + %h5.prepend-top-0 + Merge Requests + .form-group + .checkbox + = f.label :only_allow_merge_if_build_succeeds do + = f.check_box :only_allow_merge_if_build_succeeds + %strong Only allow merge requests to be merged if the build succeeds + .help-block + Builds need to be configured to enable this feature. + = link_to icon('question-circle'), help_page_path('workflow', 'merge_requests#only-allow-merge-requests-to-be-merged-if-the-build-succeeds') diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 18b125ff9d..8449fe1e4e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -84,6 +84,8 @@ %br %span.descr Enable Container Registry for this repository %hr + = render 'merge_request_settings', f: f + %hr = render 'builds_settings', f: f %hr %fieldset.features.append-bottom-default diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 13359abede..8de587009e 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -17,6 +17,8 @@ = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' + - elsif @merge_request.cannot_be_merged_because_build_failed? + = render 'projects/merge_requests/widget/open/build_failed' - elsif @merge_request.can_be_merged? = render 'projects/merge_requests/widget/open/accept' diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 60d7d6ff1f..941513febb 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -10,19 +10,20 @@ %span.btn-group = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do Merge When Build Succeeds - = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do - %span.caret - %span.sr-only - Select Merge Moment - %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } - %li - = link_to "#", class: "merge_when_build_succeeds" do - = icon('check fw') - Merge When Build Succeeds - %li - = link_to "#", class: "accept_merge_request" do - = icon('warning fw') - Merge Immediately + - unless @project.only_allow_merge_if_build_succeeds? + = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do + %span.caret + %span.sr-only + Select Merge Moment + %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } + %li + = link_to "#", class: "merge_when_build_succeeds" do + = icon('check fw') + Merge When Build Succeeds + %li + = link_to "#", class: "accept_merge_request" do + = icon('warning fw') + Merge Immediately - else = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do Accept Merge Request diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml new file mode 100644 index 0000000000..14f51af536 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml @@ -0,0 +1,6 @@ +%h4 + = icon('exclamation-triangle') + The build for this merge request failed + +%p + Please retry the build or push a new commit to fix the failure. diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb new file mode 100644 index 0000000000..69d64ccd00 --- /dev/null +++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb @@ -0,0 +1,15 @@ +class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_column_with_default(:projects, + :only_allow_merge_if_build_succeeds, + :boolean, + default: false) + end + + def down + remove_column(:projects, :only_allow_merge_if_build_succeeds) + end +end diff --git a/db/schema.rb b/db/schema.rb index b7adf48fdb..03070f0d59 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -779,6 +779,7 @@ ActiveRecord::Schema.define(version: 20160608155312) do t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" t.boolean "container_registry_enabled" + t.boolean "only_allow_merge_if_build_succeeds", default: false 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/merge_requests.rb b/lib/api/merge_requests.rb index 43221d5622..5822e19cd4 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -228,7 +228,7 @@ module API # Merge request can not be merged # because user dont have permissions to push into target branch unauthorized! unless merge_request.can_be_merged_by?(current_user) - not_allowed! if !merge_request.open? || merge_request.work_in_progress? + not_allowed! if !merge_request.open? || merge_request.work_in_progress? || merge_request.cannot_be_merged_because_build_failed? merge_request.check_if_can_be_merged diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb new file mode 100644 index 0000000000..1627aa7287 --- /dev/null +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +feature 'Only allow merge requests to be merged if the build succeeds', feature: true, js: true do + let(:user) { create(:user) } + + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) } + + before do + login_as user + + project.team << [user, :master] + end + + context "project hasn't ci enabled" do + it "allows MR to be merged" do + visit_merge_request(merge_request) + expect(page).to have_button "Accept Merge Request" + end + end + + context "when project has ci enabled" do + let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:ci_build) { create(:ci_build, commit: ci_commit) } + + before do + project.enable_ci + end + + context "when merge requests can only be merged if the build succeeds" do + before do + project.update_attribute(:only_allow_merge_if_build_succeeds, true) + end + + context "when ci is running" do + it "doesn't allow to merge immediately" do + ci_commit.statuses.update_all(status: :pending) + visit_merge_request(merge_request) + + expect(page).to have_button "Merge When Build Succeeds" + expect(page).to_not have_button "Select Merge Moment" + end + end + + context "when ci failed" do + it "doesn't allow MR to be merged" do + ci_commit.statuses.update_all(status: :failed) + visit_merge_request(merge_request) + + expect(page).to_not have_button "Accept Merge Request" + expect(page).to have_content("Please retry the build or push code to fix the failure.") + end + end + + context "when ci succeed" do + it "allows MR to be merged" do + ci_commit.statuses.update_all(status: :success) + visit_merge_request(merge_request) + + expect(page).to have_button "Accept Merge Request" + end + end + end + + context "when merge requests can be merged when the build failed" do + before do + project.update_attribute(:only_allow_merge_if_build_succeeds, false) + end + + context "when ci is running" do + it "allows MR to be merged immediately" do + ci_commit.statuses.update_all(status: :pending) + visit_merge_request(merge_request) + + expect(page).to have_button "Merge When Build Succeeds" + + click_button "Select Merge Moment" + expect(page).to have_content "Merge Immediately" + end + end + + context "when ci failed" do + it "allows MR to be merged" do + ci_commit.statuses.update_all(status: :failed) + visit_merge_request(merge_request) + + expect(page).to have_button "Accept Merge Request" + end + end + + context "when ci succeed" do + it "allows MR to be merged" do + ci_commit.statuses.update_all(status: :success) + visit_merge_request(merge_request) + + expect(page).to have_button "Accept Merge Request" + end + end + end + end + + def visit_merge_request(merge_request) + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1b7cbc3efd..76912eed83 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -455,4 +455,121 @@ describe MergeRequest, models: true do expect(user2.assigned_open_merge_request_count).to eq(1) end end + + describe '#check_if_can_be_merged' do + let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) } + + subject { create(:merge_request, source_project: project, merge_status: :unchecked) } + + context 'when it is not broken and has no conflicts' do + it 'is marked as mergeable' do + allow(subject).to receive(:broken?) { false } + allow(project).to receive_message_chain(:repository, :can_be_merged?) { true } + + expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged') + end + end + + context 'when broken' do + before { allow(subject).to receive(:broken?) { true } } + + it 'becomes unmergeable' do + expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') + end + end + + context 'when it has conflicts' do + before do + allow(subject).to receive(:broken?) { false } + allow(project).to receive_message_chain(:repository, :can_be_merged?) { false } + end + + it 'becomes unmergeable' do + expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') + end + end + end + + describe '#mergeable?' do + let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) } + + subject { create(:merge_request, source_project: project) } + + it "checks if merge request can be merged" do + allow(subject).to receive(:cannot_be_merged_because_build_failed?) { false } + expect(subject).to receive(:check_if_can_be_merged) + + subject.mergeable? + end + + context 'when not open' do + before { subject.close } + + it 'returns false' do + expect(subject.mergeable?).to be_falsey + end + end + + context 'when working in progress' do + before { subject.title = 'WIP MR' } + + it 'returns false' do + expect(subject.mergeable?).to be_falsey + end + end + + context 'when broken' do + before { allow(subject).to receive(:broken?) { true } } + + it 'returns false' do + expect(subject.mergeable?).to be_falsey + end + end + + context 'when failed' do + before { allow(subject).to receive(:broken?) { false } } + + context "when project settings restrict to merge only if build succeeds" do + before { allow(subject).to receive(:cannot_be_merged_because_build_failed?) { true } } + it 'returns false if project settings restrict to merge only if build succeeds' do + expect(subject.mergeable?).to be_falsey + end + end + end + end + + describe '#cannot_be_merged_because_build_failed?' do + let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) } + let(:commit_status) { create(:commit_status, status: 'failed', project: project) } + let(:ci_commit) { create(:ci_empty_pipeline) } + + subject { build(:merge_request, target_project: project) } + + before do + ci_commit.statuses << commit_status + allow(subject).to receive(:ci_commit) { ci_commit } + end + + it "returns true if it's only allowed to merge green build and build has been failed" do + expect(subject.cannot_be_merged_because_build_failed?).to be_truthy + end + + context 'when no ci_commit is associated' do + before do + allow(subject).to receive(:ci_commit) { nil } + end + + it 'returns false' do + expect(subject.cannot_be_merged_because_build_failed?).to be_falsey + end + end + + context "when isn't only allowed to merge green build at project settings" do + subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) } + + it 'returns false' do + expect(subject.cannot_be_merged_because_build_failed?).to be_falsey + end + end + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 9da69a913a..91c25a0948 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -419,6 +419,14 @@ describe API::API, api: true do expect(json_response['message']).to eq('405 Method Not Allowed') end + it "should return 405 if merge_request build is failed it's restrict to merge only when susccess" do + allow_any_instance_of(MergeRequest).to receive(:cannot_be_merged_because_build_failed?).and_return(true) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + expect(response.status).to eq(405) + expect(json_response['message']).to eq('405 Method Not Allowed') + end + it "should return 401 if user has no permissions to merge" do user2 = create(:user) project.team << [user2, :reporter] From 6dff7c1771e0cfeb6906244649b3683090bc2929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 7 Jun 2016 13:01:34 +0200 Subject: [PATCH 493/507] Improve initial implementation of the 'only_allow_merge_if_build_succeeds.rb' feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on the feedback from reviewers. Signed-off-by: Rémy Coutable --- app/models/merge_request.rb | 19 +++- .../merge_requests/widget/_open.html.haml | 2 +- lib/api/merge_requests.rb | 5 +- .../only_allow_merge_if_build_succeeds.rb | 92 +++++++++---------- spec/models/merge_request_spec.rb | 63 +++++++++---- spec/requests/api/merge_requests_spec.rb | 5 +- 6 files changed, 113 insertions(+), 73 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 43c6bcb871..949cafc065 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -260,13 +260,20 @@ class MergeRequest < ActiveRecord::Base end def mergeable? - return false if !open? || work_in_progress? || broken? || cannot_be_merged_because_build_failed? - - check_if_can_be_merged + mergeable_state? && check_if_can_be_merged can_be_merged? end + def mergeable_state? + return false unless open? + return false if work_in_progress? + return false if broken? + return false if cannot_be_merged_because_build_is_not_success? + + true + end + def gitlab_merge_status if work_in_progress? "work_in_progress" @@ -481,8 +488,10 @@ class MergeRequest < ActiveRecord::Base ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) end - def cannot_be_merged_because_build_failed? - project.only_allow_merge_if_build_succeeds? && ci_commit && ci_commit.failed? + def cannot_be_merged_because_build_is_not_success? + return false unless project.only_allow_merge_if_build_succeeds? + + ci_commit && !ci_commit.success? end def state_human_name diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 8de587009e..9ea4df4357 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -17,7 +17,7 @@ = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - - elsif @merge_request.cannot_be_merged_because_build_failed? + - elsif @merge_request.cannot_be_merged_because_build_is_not_success? && @ci_commit && @ci_commit.failed? = render 'projects/merge_requests/widget/open/build_failed' - elsif @merge_request.can_be_merged? = render 'projects/merge_requests/widget/open/accept' diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 5822e19cd4..24df3e397e 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -228,11 +228,10 @@ module API # Merge request can not be merged # because user dont have permissions to push into target branch unauthorized! unless merge_request.can_be_merged_by?(current_user) - not_allowed! if !merge_request.open? || merge_request.work_in_progress? || merge_request.cannot_be_merged_because_build_failed? - merge_request.check_if_can_be_merged + not_allowed! unless merge_request.mergeable_state? - render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? + render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? if params[:sha] && merge_request.source_sha != params[:sha] render_api_error!("SHA does not match HEAD of source branch: #{merge_request.source_sha}", 409) diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb index 1627aa7287..52612c9182 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb @@ -1,99 +1,99 @@ require 'spec_helper' -feature 'Only allow merge requests to be merged if the build succeeds', feature: true, js: true do - let(:user) { create(:user) } - +feature 'Only allow merge requests to be merged if the build succeeds', feature: true do let(:project) { create(:project, :public) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: project) } before do - login_as user + login_as merge_request.author - project.team << [user, :master] + project.team << [merge_request.author, :master] end - context "project hasn't ci enabled" do - it "allows MR to be merged" do + context 'project does not have CI enabled' do + it 'allows MR to be merged' do visit_merge_request(merge_request) - expect(page).to have_button "Accept Merge Request" + + expect(page).to have_button 'Accept Merge Request' end end - context "when project has ci enabled" do - let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } - let!(:ci_build) { create(:ci_build, commit: ci_commit) } + context 'when project has CI enabled' do + let(:ci_commit) { create(:ci_empty_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } - before do - project.enable_ci - end - - context "when merge requests can only be merged if the build succeeds" do + context 'when merge requests can only be merged if the build succeeds' do before do project.update_attribute(:only_allow_merge_if_build_succeeds, true) end - context "when ci is running" do - it "doesn't allow to merge immediately" do - ci_commit.statuses.update_all(status: :pending) + context 'when CI is running' do + before { ci_commit.update_column(:status, :running) } + + it 'does not allow to merge immediately' do visit_merge_request(merge_request) - expect(page).to have_button "Merge When Build Succeeds" - expect(page).to_not have_button "Select Merge Moment" + expect(page).to have_button 'Merge When Build Succeeds' + expect(page).not_to have_button 'Select Merge Moment' end end - context "when ci failed" do - it "doesn't allow MR to be merged" do - ci_commit.statuses.update_all(status: :failed) + context 'when CI failed' do + before { ci_commit.update_column(:status, :failed) } + + it 'does not allow MR to be merged' do visit_merge_request(merge_request) - expect(page).to_not have_button "Accept Merge Request" - expect(page).to have_content("Please retry the build or push code to fix the failure.") + expect(page).not_to have_button 'Accept Merge Request' + expect(page).to have_content('Please retry the build or push a new commit to fix the failure.') end end - context "when ci succeed" do - it "allows MR to be merged" do - ci_commit.statuses.update_all(status: :success) + context 'when CI succeeded' do + before { ci_commit.update_column(:status, :success) } + + it 'allows MR to be merged' do visit_merge_request(merge_request) - expect(page).to have_button "Accept Merge Request" + expect(page).to have_button 'Accept Merge Request' end end end - context "when merge requests can be merged when the build failed" do + context 'when merge requests can be merged when the build failed' do before do project.update_attribute(:only_allow_merge_if_build_succeeds, false) end - context "when ci is running" do - it "allows MR to be merged immediately" do - ci_commit.statuses.update_all(status: :pending) + context 'when CI is running' do + before { ci_commit.update_column(:status, :running) } + + it 'allows MR to be merged immediately', js: true do visit_merge_request(merge_request) - expect(page).to have_button "Merge When Build Succeeds" + expect(page).to have_button 'Merge When Build Succeeds' - click_button "Select Merge Moment" - expect(page).to have_content "Merge Immediately" + click_button 'Select Merge Moment' + expect(page).to have_content 'Merge Immediately' end end - context "when ci failed" do - it "allows MR to be merged" do - ci_commit.statuses.update_all(status: :failed) + context 'when CI failed' do + before { ci_commit.update_column(:status, :failed) } + + it 'allows MR to be merged' do visit_merge_request(merge_request) - expect(page).to have_button "Accept Merge Request" + expect(page).to have_button 'Accept Merge Request' end end - context "when ci succeed" do - it "allows MR to be merged" do - ci_commit.statuses.update_all(status: :success) + context 'when CI succeeded' do + before { ci_commit.update_column(:status, :success) } + + it 'allows MR to be merged' do visit_merge_request(merge_request) - expect(page).to have_button "Accept Merge Request" + expect(page).to have_button 'Accept Merge Request' end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 76912eed83..f8f1bbf303 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -491,12 +491,39 @@ describe MergeRequest, models: true do end describe '#mergeable?' do - let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) } + let(:project) { create(:project) } subject { create(:merge_request, source_project: project) } - it "checks if merge request can be merged" do - allow(subject).to receive(:cannot_be_merged_because_build_failed?) { false } + it 'calls mergeable_state?' do + expect(subject).to receive(:mergeable_state?) + + expect(subject.mergeable?).to be_truthy + end + + it 'calls check_if_can_be_merged' do + allow(subject).to receive(:mergeable_state?) { true } + expect(subject).to receive(:check_if_can_be_merged) + + expect(subject.mergeable?).to be_truthy + end + + it 'calls can_be_merged?' do + allow(subject).to receive(:mergeable_state?) { true } + allow(subject).to receive(:can_be_merged?) { true } + expect(subject).to receive(:check_if_can_be_merged) + + expect(subject.mergeable?).to be_truthy + end + end + + describe '#mergeable_state?' do + let(:project) { create(:project) } + + subject { create(:merge_request, source_project: project) } + + it 'checks if merge request can be merged' do + allow(subject).to receive(:cannot_be_merged_because_build_is_not_success?) { false } expect(subject).to receive(:check_if_can_be_merged) subject.mergeable? @@ -506,7 +533,7 @@ describe MergeRequest, models: true do before { subject.close } it 'returns false' do - expect(subject.mergeable?).to be_falsey + expect(subject.mergeable_state?).to be_falsey end end @@ -514,7 +541,7 @@ describe MergeRequest, models: true do before { subject.title = 'WIP MR' } it 'returns false' do - expect(subject.mergeable?).to be_falsey + expect(subject.mergeable_state?).to be_falsey end end @@ -522,23 +549,27 @@ describe MergeRequest, models: true do before { allow(subject).to receive(:broken?) { true } } it 'returns false' do - expect(subject.mergeable?).to be_falsey + expect(subject.mergeable_state?).to be_falsey end end context 'when failed' do before { allow(subject).to receive(:broken?) { false } } - context "when project settings restrict to merge only if build succeeds" do - before { allow(subject).to receive(:cannot_be_merged_because_build_failed?) { true } } - it 'returns false if project settings restrict to merge only if build succeeds' do - expect(subject.mergeable?).to be_falsey + context 'when project settings restrict to merge only if build succeeds and build failed' do + before do + project.only_allow_merge_if_build_succeeds = true + allow(subject).to receive(:cannot_be_merged_because_build_is_not_success?) { true } + end + + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey end end end end - describe '#cannot_be_merged_because_build_failed?' do + describe '#cannot_be_merged_because_build_is_not_success?' do let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) } let(:commit_status) { create(:commit_status, status: 'failed', project: project) } let(:ci_commit) { create(:ci_empty_pipeline) } @@ -550,8 +581,8 @@ describe MergeRequest, models: true do allow(subject).to receive(:ci_commit) { ci_commit } end - it "returns true if it's only allowed to merge green build and build has been failed" do - expect(subject.cannot_be_merged_because_build_failed?).to be_truthy + it 'returns true if it is only allowed to merge green build and build has been failed' do + expect(subject.cannot_be_merged_because_build_is_not_success?).to be_truthy end context 'when no ci_commit is associated' do @@ -560,15 +591,15 @@ describe MergeRequest, models: true do end it 'returns false' do - expect(subject.cannot_be_merged_because_build_failed?).to be_falsey + expect(subject.cannot_be_merged_because_build_is_not_success?).to be_falsey end end - context "when isn't only allowed to merge green build at project settings" do + context 'when is not only allowed to merge green build at project settings' do subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) } it 'returns false' do - expect(subject.cannot_be_merged_because_build_failed?).to be_falsey + expect(subject.cannot_be_merged_because_build_is_not_success?).to be_falsey end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 91c25a0948..a52148e8b8 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -419,10 +419,11 @@ describe API::API, api: true do expect(json_response['message']).to eq('405 Method Not Allowed') end - it "should return 405 if merge_request build is failed it's restrict to merge only when susccess" do - allow_any_instance_of(MergeRequest).to receive(:cannot_be_merged_because_build_failed?).and_return(true) + it 'returns 405 if the build failed for a merge request that requires success' do + allow_any_instance_of(MergeRequest).to receive(:cannot_be_merged_because_build_is_not_success?).and_return(true) put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + expect(response.status).to eq(405) expect(json_response['message']).to eq('405 Method Not Allowed') end From 282674c1108bdd761f4027910a32396fe253bc95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 7 Jun 2016 13:02:55 +0200 Subject: [PATCH 494/507] Add documentation for the 'only_allow_merge_if_build_succeeds.rb' feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- doc/workflow/merge_requests.md | 11 +++++++++++ .../only_allow_merge_if_build_succeeds.png | Bin 0 -> 17552 bytes 2 files changed, 11 insertions(+) create mode 100644 doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md index 1b5718c91c..d2ec56e650 100644 --- a/doc/workflow/merge_requests.md +++ b/doc/workflow/merge_requests.md @@ -2,6 +2,17 @@ Merge requests allow you to exchange changes you made to source code +## Only allow merge requests to be merged if the build succeeds + +You can prevent merge requests from being merged if their build did not succeed +in the project settings page. + +![only_allow_merge_if_build_succeeds](merge_requests/only_allow_merge_if_build_succeeds.png) + +Navigate to project settings page and select the `Only allow merge requests to be merged if the build succeeds` check box. + +Please note that you need to have builds configured to enable this feature. + ## Checkout merge requests locally Locate the section for your GitLab remote in the `.git/config` file. It looks like this: diff --git a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png b/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png new file mode 100644 index 0000000000000000000000000000000000000000..18bebf5fe9269fdb393b8c7bd1a548b57a876707 GIT binary patch literal 17552 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 CFyu!7 literal 0 HcmV?d00001 From 5324c9364346f74ea73c6be27785704e8e2281f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 9 Jun 2016 16:08:38 +0200 Subject: [PATCH 495/507] Rename MergeRequest#cannot_be_merged_because_build_is_not_success? to #mergeable_ci_state? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The logic of the method was obviously inverted. Signed-off-by: Rémy Coutable --- app/models/merge_request.rb | 12 ++-- .../merge_requests/widget/_open.html.haml | 2 +- db/schema.rb | 36 +++++----- spec/models/merge_request_spec.rb | 69 ++++++++++--------- spec/requests/api/merge_requests_spec.rb | 2 +- 5 files changed, 64 insertions(+), 57 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 949cafc065..f919cfe697 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -260,7 +260,9 @@ class MergeRequest < ActiveRecord::Base end def mergeable? - mergeable_state? && check_if_can_be_merged + return false unless mergeable_state? + + check_if_can_be_merged can_be_merged? end @@ -269,7 +271,7 @@ class MergeRequest < ActiveRecord::Base return false unless open? return false if work_in_progress? return false if broken? - return false if cannot_be_merged_because_build_is_not_success? + return false unless mergeable_ci_state? true end @@ -488,10 +490,10 @@ class MergeRequest < ActiveRecord::Base ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) end - def cannot_be_merged_because_build_is_not_success? - return false unless project.only_allow_merge_if_build_succeeds? + def mergeable_ci_state? + return true unless project.only_allow_merge_if_build_succeeds? - ci_commit && !ci_commit.success? + !ci_commit || ci_commit.success? end def state_human_name diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 9ea4df4357..84fbf618ec 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -17,7 +17,7 @@ = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - - elsif @merge_request.cannot_be_merged_because_build_is_not_success? && @ci_commit && @ci_commit.failed? + - elsif !@merge_request.mergeable_ci_state? && @ci_commit && @ci_commit.failed? = render 'projects/merge_requests/widget/open/build_failed' - elsif @merge_request.can_be_merged? = render 'projects/merge_requests/widget/open/accept' diff --git a/db/schema.rb b/db/schema.rb index 03070f0d59..09ff52ccc8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -747,39 +747,39 @@ ActiveRecord::Schema.define(version: 20160608155312) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false + t.boolean "issues_enabled", default: true, null: false + t.boolean "merge_requests_enabled", default: true, null: false + t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false + t.string "issues_tracker", default: "gitlab", null: false t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false + t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false + t.integer "visibility_level", default: 0, null: false + t.boolean "archived", default: false, null: false t.string "avatar" t.string "import_status" - t.float "repository_size", default: 0.0 - t.integer "star_count", default: 0, null: false + t.float "repository_size", default: 0.0 + t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 + t.integer "commit_count", default: 0 t.text "import_error" t.integer "ci_id" - t.boolean "builds_enabled", default: true, null: false - t.boolean "shared_runners_enabled", default: true, null: false + t.boolean "builds_enabled", default: true, null: false + t.boolean "shared_runners_enabled", default: true, null: false t.string "runners_token" t.string "build_coverage_regex" - t.boolean "build_allow_git_fetch", default: true, null: false - t.integer "build_timeout", default: 3600, null: false - t.boolean "pending_delete", default: false - t.boolean "public_builds", default: true, null: false - t.integer "pushes_since_gc", default: 0 + t.boolean "build_allow_git_fetch", default: true, null: false + t.integer "build_timeout", default: 3600, null: false + t.boolean "pending_delete", default: false + t.boolean "public_builds", default: true, null: false + t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" t.boolean "container_registry_enabled" - t.boolean "only_allow_merge_if_build_succeeds", default: false + t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f8f1bbf303..c543cbcfab 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -495,23 +495,16 @@ describe MergeRequest, models: true do subject { create(:merge_request, source_project: project) } - it 'calls mergeable_state?' do - expect(subject).to receive(:mergeable_state?) + it 'returns false if #mergeable_state? is false' do + expect(subject).to receive(:mergeable_state?) { false } - expect(subject.mergeable?).to be_truthy + expect(subject.mergeable?).to be_falsey end - it 'calls check_if_can_be_merged' do + it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do allow(subject).to receive(:mergeable_state?) { true } expect(subject).to receive(:check_if_can_be_merged) - - expect(subject.mergeable?).to be_truthy - end - - it 'calls can_be_merged?' do - allow(subject).to receive(:mergeable_state?) { true } - allow(subject).to receive(:can_be_merged?) { true } - expect(subject).to receive(:check_if_can_be_merged) + expect(subject).to receive(:can_be_merged?) { true } expect(subject.mergeable?).to be_truthy end @@ -523,7 +516,7 @@ describe MergeRequest, models: true do subject { create(:merge_request, source_project: project) } it 'checks if merge request can be merged' do - allow(subject).to receive(:cannot_be_merged_because_build_is_not_success?) { false } + allow(subject).to receive(:mergeable_ci_state?) { true } expect(subject).to receive(:check_if_can_be_merged) subject.mergeable? @@ -559,7 +552,7 @@ describe MergeRequest, models: true do context 'when project settings restrict to merge only if build succeeds and build failed' do before do project.only_allow_merge_if_build_succeeds = true - allow(subject).to receive(:cannot_be_merged_because_build_is_not_success?) { true } + allow(subject).to receive(:mergeable_ci_state?) { false } end it 'returns false' do @@ -569,37 +562,49 @@ describe MergeRequest, models: true do end end - describe '#cannot_be_merged_because_build_is_not_success?' do + describe '#mergeable_ci_state?' do let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) } - let(:commit_status) { create(:commit_status, status: 'failed', project: project) } let(:ci_commit) { create(:ci_empty_pipeline) } subject { build(:merge_request, target_project: project) } - before do - ci_commit.statuses << commit_status - allow(subject).to receive(:ci_commit) { ci_commit } - end + context 'when it is only allowed to merge when build is green' do + context 'and a failed ci_commit is associated' do + before do + ci_commit.statuses << create(:commit_status, status: 'failed', project: project) + allow(subject).to receive(:ci_commit) { ci_commit } + end - it 'returns true if it is only allowed to merge green build and build has been failed' do - expect(subject.cannot_be_merged_because_build_is_not_success?).to be_truthy - end - - context 'when no ci_commit is associated' do - before do - allow(subject).to receive(:ci_commit) { nil } + it { expect(subject.mergeable_ci_state?).to be_falsey } end - it 'returns false' do - expect(subject.cannot_be_merged_because_build_is_not_success?).to be_falsey + context 'when no ci_commit is associated' do + before do + allow(subject).to receive(:ci_commit) { nil } + end + + it { expect(subject.mergeable_ci_state?).to be_truthy } end end - context 'when is not only allowed to merge green build at project settings' do + context 'when merges are not restricted to green builds' do subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) } - it 'returns false' do - expect(subject.cannot_be_merged_because_build_is_not_success?).to be_falsey + context 'and a failed ci_commit is associated' do + before do + ci_commit.statuses << create(:commit_status, status: 'failed', project: project) + allow(subject).to receive(:ci_commit) { ci_commit } + end + + it { expect(subject.mergeable_ci_state?).to be_truthy } + end + + context 'when no ci_commit is associated' do + before do + allow(subject).to receive(:ci_commit) { nil } + end + + it { expect(subject.mergeable_ci_state?).to be_truthy } end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index a52148e8b8..1356f87b0e 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -420,7 +420,7 @@ describe API::API, api: true do end it 'returns 405 if the build failed for a merge request that requires success' do - allow_any_instance_of(MergeRequest).to receive(:cannot_be_merged_because_build_is_not_success?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false) put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) From 34bef254644b4ecda4ac61a82447a23f081aeca0 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 8 Jun 2016 20:03:19 +0200 Subject: [PATCH 496/507] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index b00c149a75..c0918cfb9f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -70,6 +70,7 @@ v 8.8.5 (unreleased) - Import GitHub repositories respecting the API rate limit - Fix importer for GitHub comments on diff - Disable Webhooks before proceeding with the GitHub import + - Fix incremental trace upload API when using multi-byte UTF-8 chars in trace v 8.8.4 - Fix LDAP-based login for users with 2FA enabled. !4493 From 3579edba1f0d27095502775c64bdd73a3927dca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 10 Jun 2016 14:41:38 +0200 Subject: [PATCH 497/507] Rename ci_commit -> pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/models/merge_request.rb | 2 +- .../merge_requests/widget/_open.html.haml | 2 +- .../only_allow_merge_if_build_succeeds.rb | 14 ++++++------ spec/models/merge_request_spec.rb | 22 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f919cfe697..29f3657091 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -493,7 +493,7 @@ class MergeRequest < ActiveRecord::Base def mergeable_ci_state? return true unless project.only_allow_merge_if_build_succeeds? - !ci_commit || ci_commit.success? + !pipeline || pipeline.success? end def state_human_name diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 84fbf618ec..0e0af57d76 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -17,7 +17,7 @@ = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - - elsif !@merge_request.mergeable_ci_state? && @ci_commit && @ci_commit.failed? + - elsif !@merge_request.mergeable_ci_state? && @pipeline && @pipeline.failed? = render 'projects/merge_requests/widget/open/build_failed' - elsif @merge_request.can_be_merged? = render 'projects/merge_requests/widget/open/accept' diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb index 52612c9182..65e9185ec2 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb @@ -19,7 +19,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: end context 'when project has CI enabled' do - let(:ci_commit) { create(:ci_empty_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } context 'when merge requests can only be merged if the build succeeds' do before do @@ -27,7 +27,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: end context 'when CI is running' do - before { ci_commit.update_column(:status, :running) } + before { pipeline.update_column(:status, :running) } it 'does not allow to merge immediately' do visit_merge_request(merge_request) @@ -38,7 +38,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: end context 'when CI failed' do - before { ci_commit.update_column(:status, :failed) } + before { pipeline.update_column(:status, :failed) } it 'does not allow MR to be merged' do visit_merge_request(merge_request) @@ -49,7 +49,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: end context 'when CI succeeded' do - before { ci_commit.update_column(:status, :success) } + before { pipeline.update_column(:status, :success) } it 'allows MR to be merged' do visit_merge_request(merge_request) @@ -65,7 +65,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: end context 'when CI is running' do - before { ci_commit.update_column(:status, :running) } + before { pipeline.update_column(:status, :running) } it 'allows MR to be merged immediately', js: true do visit_merge_request(merge_request) @@ -78,7 +78,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: end context 'when CI failed' do - before { ci_commit.update_column(:status, :failed) } + before { pipeline.update_column(:status, :failed) } it 'allows MR to be merged' do visit_merge_request(merge_request) @@ -88,7 +88,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: end context 'when CI succeeded' do - before { ci_commit.update_column(:status, :success) } + before { pipeline.update_column(:status, :success) } it 'allows MR to be merged' do visit_merge_request(merge_request) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c543cbcfab..3b199f4d98 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -564,23 +564,23 @@ describe MergeRequest, models: true do describe '#mergeable_ci_state?' do let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) } - let(:ci_commit) { create(:ci_empty_pipeline) } + let(:pipeline) { create(:ci_empty_pipeline) } subject { build(:merge_request, target_project: project) } context 'when it is only allowed to merge when build is green' do - context 'and a failed ci_commit is associated' do + context 'and a failed pipeline is associated' do before do - ci_commit.statuses << create(:commit_status, status: 'failed', project: project) - allow(subject).to receive(:ci_commit) { ci_commit } + pipeline.statuses << create(:commit_status, status: 'failed', project: project) + allow(subject).to receive(:pipeline) { pipeline } end it { expect(subject.mergeable_ci_state?).to be_falsey } end - context 'when no ci_commit is associated' do + context 'when no pipeline is associated' do before do - allow(subject).to receive(:ci_commit) { nil } + allow(subject).to receive(:pipeline) { nil } end it { expect(subject.mergeable_ci_state?).to be_truthy } @@ -590,18 +590,18 @@ describe MergeRequest, models: true do context 'when merges are not restricted to green builds' do subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) } - context 'and a failed ci_commit is associated' do + context 'and a failed pipeline is associated' do before do - ci_commit.statuses << create(:commit_status, status: 'failed', project: project) - allow(subject).to receive(:ci_commit) { ci_commit } + pipeline.statuses << create(:commit_status, status: 'failed', project: project) + allow(subject).to receive(:pipeline) { pipeline } end it { expect(subject.mergeable_ci_state?).to be_truthy } end - context 'when no ci_commit is associated' do + context 'when no pipeline is associated' do before do - allow(subject).to receive(:ci_commit) { nil } + allow(subject).to receive(:pipeline) { nil } end it { expect(subject.mergeable_ci_state?).to be_truthy } From 998c688699db68a6dd1fb5e6459b1a787deabcef Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 10 Jun 2016 12:07:37 +0100 Subject: [PATCH 498/507] Updated tests --- spec/features/builds_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 0c865e915c..b8ecc356b4 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -184,7 +184,7 @@ describe "Builds" do @build.run! @build.trace = 'BUILD TRACE' visit namespace_project_build_path(@project.namespace, @project, @build) - page.within('.build-controls') { click_link 'Raw' } + page.within('.js-build-sidebar') { click_link 'Raw' } end it 'sends the right headers' do From 24920bc52a5658dd1d16d38ba3dc46f92dfe7675 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 8 Jun 2016 17:03:51 +0200 Subject: [PATCH 499/507] Add Project.where_paths_in This method can be used to find multiple projects for multiple paths. For example, take this snippet: Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) This will return an ActiveRecord::Relation containing the GitLab CE and GitLab EE projects. This method takes care of matching rows both case-sensitively and case-insensitively where needed. Project.find_with_namespace in turn has been modified to use Project.where_paths_in without nuking any scoping (instead it uses reorder(nil)). This means that any default scopes (e.g. those used for "pending_delete" stay intact). The method Project.where_paths_in was added so the various Markdown filters can use a single query to grab all the projects referenced in a set of documents, something Project.find_with_namespace didn't allow. --- app/models/project.rb | 71 +++++++++++++++++++++++++++++++------ spec/models/project_spec.rb | 33 +++++++++++++++++ 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index f47ef8a81d..7bee7550d6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -253,20 +253,69 @@ class Project < ActiveRecord::Base non_archived.where(table[:name].matches(pattern)) end - def find_with_namespace(id) - namespace_path, project_path = id.split('/', 2) + # Finds a single project for the given path. + # + # path - The full project path (including namespace path). + # + # Returns a Project, or nil if no project could be found. + def find_with_namespace(path) + where_paths_in([path]).reorder(nil).take + end - return nil if !namespace_path || !project_path + # Builds a relation to find multiple projects by their full paths. + # + # Each path must be in the following format: + # + # namespace_path/project_path + # + # For example: + # + # gitlab-org/gitlab-ce + # + # Usage: + # + # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) + # + # This would return the projects with the full paths matching the values + # given. + # + # paths - An Array of full paths (namespace path + project path) for which + # to find the projects. + # + # Returns an ActiveRecord::Relation. + def where_paths_in(paths) + wheres = [] + cast_lower = Gitlab::Database.postgresql? - # Use of unscoped ensures we're not secretly adding any ORDER BYs, which - # have a negative impact on performance (and aren't needed for this - # query). - projects = unscoped. - joins(:namespace). - iwhere('namespaces.path' => namespace_path) + paths.each do |path| + namespace_path, project_path = path.split('/', 2) - projects.find_by('projects.path' => project_path) || - projects.iwhere('projects.path' => project_path).take + next unless namespace_path && project_path + + namespace_path = connection.quote(namespace_path) + project_path = connection.quote(project_path) + + where = "(namespaces.path = #{namespace_path} + AND projects.path = #{project_path})" + + if cast_lower + where = "( + #{where} + OR ( + LOWER(namespaces.path) = LOWER(#{namespace_path}) + AND LOWER(projects.path) = LOWER(#{project_path}) + ) + )" + end + + wheres << where + end + + if wheres.empty? + none + else + joins(:namespace).where(wheres.join(' OR ')) + end end def visibility_levels diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 553556ed32..f687e3171a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -859,4 +859,37 @@ describe Project, models: true do it { is_expected.to be_falsey } end end + + describe '.where_paths_in' do + context 'without any paths' do + it 'returns an empty relation' do + expect(Project.where_paths_in([])).to eq([]) + end + end + + context 'without any valid paths' do + it 'returns an empty relation' do + expect(Project.where_paths_in(%w[foo])).to eq([]) + end + end + + context 'with valid paths' do + let!(:project1) { create(:project) } + let!(:project2) { create(:project) } + + it 'returns the projects matching the paths' do + projects = Project.where_paths_in([project1.path_with_namespace, + project2.path_with_namespace]) + + expect(projects).to contain_exactly(project1, project2) + end + + it 'returns projects regardless of the casing of paths' do + projects = Project.where_paths_in([project1.path_with_namespace.upcase, + project2.path_with_namespace.upcase]) + + expect(projects).to contain_exactly(project1, project2) + end + end + end end From 136a4ea39bb82d7e88b79a3bb7b2f3b4a5ec42ab Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Fri, 3 Jun 2016 10:21:18 +0200 Subject: [PATCH 500/507] Cache the presence of an issue_tracker at project level Using update_column to store the boolean flag to avoid any side effects with the current state of the project instance --- CHANGELOG | 1 + app/models/project.rb | 18 +++++- app/models/service.rb | 10 +++ ..._has_external_issue_tracker_to_projects.rb | 10 +++ db/schema.rb | 1 + spec/models/project_spec.rb | 63 +++++++++++++++++++ spec/models/service_spec.rb | 33 ++++++++++ 7 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb diff --git a/CHANGELOG b/CHANGELOG index 1c64739f70..0c712b445a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -62,6 +62,7 @@ v 8.9.0 (unreleased) - Markdown editor now correctly resets the input value on edit cancellation !4175 - Toggling a task list item in a issue/mr description does not creates a Todo for mentions - Improved UX of date pickers on issue & milestone forms + - Cache on the database if a project has an active external issue tracker. v 8.8.5 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds diff --git a/app/models/project.rb b/app/models/project.rb index f47ef8a81d..472e79cfcf 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -523,9 +523,21 @@ class Project < ActiveRecord::Base end def external_issue_tracker - return @external_issue_tracker if defined?(@external_issue_tracker) - @external_issue_tracker ||= - services.issue_trackers.active.without_defaults.first + if has_external_issue_tracker.nil? # To populate existing projects + cache_has_external_issue_tracker + end + + if has_external_issue_tracker? + return @external_issue_tracker if defined?(@external_issue_tracker) + + @external_issue_tracker = services.external_issue_trackers.first + else + nil + end + end + + def cache_has_external_issue_tracker + update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) end def can_have_issues_tracker_id? diff --git a/app/models/service.rb b/app/models/service.rb index de3fd24584..bf35239750 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -16,6 +16,7 @@ class Service < ActiveRecord::Base after_initialize :initialize_properties after_commit :reset_updated_properties + after_commit :cache_project_has_external_issue_tracker belongs_to :project has_one :service_hook @@ -34,6 +35,7 @@ class Service < ActiveRecord::Base scope :note_hooks, -> { where(note_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } + scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } default_value_for :category, 'common' @@ -192,4 +194,12 @@ class Service < ActiveRecord::Base service.project_id = project_id service if service.save end + + private + + def cache_project_has_external_issue_tracker + if project && !project.destroyed? + project.cache_has_external_issue_tracker + end + end end diff --git a/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb b/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb new file mode 100644 index 0000000000..be295f0181 --- /dev/null +++ b/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb @@ -0,0 +1,10 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddHasExternalIssueTrackerToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def change + add_column(:projects, :has_external_issue_tracker, :boolean) + end +end diff --git a/db/schema.rb b/db/schema.rb index 09ff52ccc8..aac327797e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -780,6 +780,7 @@ ActiveRecord::Schema.define(version: 20160608155312) do t.datetime "last_repository_check_at" t.boolean "container_registry_enabled" t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false + t.boolean "has_external_issue_tracker" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 553556ed32..b6f36d3481 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -258,6 +258,69 @@ describe Project, models: true do end end + describe :external_issue_tracker do + let(:project) { create(:project) } + let(:ext_project) { create(:redmine_project) } + + context 'on existing projects with no value for has_external_issue_tracker' do + before(:each) do + project.update_column(:has_external_issue_tracker, nil) + ext_project.update_column(:has_external_issue_tracker, nil) + end + + it 'updates the has_external_issue_tracker boolean' do + expect do + project.external_issue_tracker + end.to change { project.reload.has_external_issue_tracker }.to(false) + + expect do + ext_project.external_issue_tracker + end.to change { ext_project.reload.has_external_issue_tracker }.to(true) + end + end + + it 'returns nil and does not query services when there is no external issue tracker' do + project.build_missing_services + project.reload + + expect(project).not_to receive(:services) + + expect(project.external_issue_tracker).to eq(nil) + end + + it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do + ext_project.reload # Factory returns a project with changed attributes + ext_project.build_missing_services + ext_project.reload + + expect(ext_project).to receive(:services).once.and_call_original + + 2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) } + end + end + + describe :cache_has_external_issue_tracker do + let(:project) { create(:project) } + + it 'stores true if there is any external_issue_tracker' do + services = double(:service, external_issue_trackers: [RedmineService.new]) + expect(project).to receive(:services).and_return(services) + + expect do + project.cache_has_external_issue_tracker + end.to change { project.has_external_issue_tracker}.to(true) + end + + it 'stores false if there is no external_issue_tracker' do + services = double(:service, external_issue_trackers: []) + expect(project).to receive(:services).and_return(services) + + expect do + project.cache_has_external_issue_tracker + end.to change { project.has_external_issue_tracker}.to(false) + end + end + describe :can_have_issues_tracker_id? do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 8592e112c5..2f000dbc01 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -204,4 +204,37 @@ describe Service, models: true do expect(service.bamboo_url_was).to be_nil end end + + describe "callbacks" do + let(:project) { create(:project) } + let!(:service) do + RedmineService.new( + project: project, + active: true, + properties: { + project_url: 'http://redmine/projects/project_name_in_redmine', + issues_url: "http://redmine/#{project.id}/project_name_in_redmine/:id", + new_issue_url: 'http://redmine/projects/project_name_in_redmine/issues/new' + } + ) + end + + describe "on create" do + it "updates the has_external_issue_tracker boolean" do + expect do + service.save! + end.to change { service.project.has_external_issue_tracker }.from(nil).to(true) + end + end + + describe "on update" do + it "updates the has_external_issue_tracker boolean" do + service.save! + + expect do + service.update_attributes(active: false) + end.to change { service.project.has_external_issue_tracker }.from(true).to(false) + end + end + end end From be98ee2586466b8f9f4e96e4b54490c412f6b43b Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Mon, 6 Jun 2016 10:06:37 +0200 Subject: [PATCH 501/507] Fixing specs stubbed objects cannot access database --- spec/lib/banzai/pipeline/wiki_pipeline_spec.rb | 4 ++-- spec/models/project_services/bamboo_service_spec.rb | 2 +- spec/models/project_services/teamcity_service_spec.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index ea4ab2c852..72bc6a0b70 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -52,8 +52,8 @@ describe Banzai::Pipeline::WikiPipeline do end describe "Links" do - let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") } - let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } + let(:namespace) { create(:namespace, name: "wiki_link_ns") } + let(:project) { create(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } let(:project_wiki) { ProjectWiki.new(project, double(:user)) } let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index e771f35811..ec81f05fc7 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -194,7 +194,7 @@ describe BambooService, models: true do def service(bamboo_url: 'http://gitlab.com') described_class.create( - project: build_stubbed(:empty_project), + project: create(:empty_project), properties: { bamboo_url: bamboo_url, username: 'mic', diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index ad24b89517..24a708ca84 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -182,7 +182,7 @@ describe TeamcityService, models: true do def service(teamcity_url: 'http://gitlab.com') described_class.create( - project: build_stubbed(:empty_project), + project: create(:empty_project), properties: { teamcity_url: teamcity_url, username: 'mic', From 0e7abb4c2851131ccc5a81e1923824ac845bbe3f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 10 Jun 2016 15:53:38 +0200 Subject: [PATCH 502/507] Fix incorrect registry key value Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/18441 --- config/gitlab.yml.example | 2 +- doc/administration/container_registry.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 0510e7df59..1048ef6e24 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -181,7 +181,7 @@ production: &base # host: registry.example.com # port: 5005 # api_url: http://localhost:5000/ # internal address to the registry, will be used by GitLab to directly communicate with API - # key_path: config/registry.key + # key: config/registry.key # path: shared/registry # issuer: gitlab-issuer diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index caf9a5bef2..7870669fa7 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -62,7 +62,7 @@ registry: host: registry.gitlab.example.com port: 5005 api_url: http://localhost:5000/ - key_path: config/registry.key + key: config/registry.key path: shared/registry issuer: gitlab-issuer ``` @@ -75,7 +75,7 @@ where: | `host` | The host URL under which the Registry will run and the users will be able to use. | | `port` | The port under which the external Registry domain will listen on. | | `api_url` | The internal API URL under which the Registry is exposed to. It defaults to `http://localhost:5000`. | -| `key_path`| The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation][token-config]. | +| `key` | The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation][token-config]. | | `path` | This should be the same directory like specified in Registry's `rootdirectory`. Read the [storage configuration documentation][storage-config]. This path needs to be readable by the GitLab user, the web-server user and the Registry user. Read more in [#container-registry-storage-path](#container-registry-storage-path). | | `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. | From 1976a44649edcac40058cf39600e134ec6112408 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Jun 2016 16:17:28 +0300 Subject: [PATCH 503/507] Move Labels and Milestones as sub tab to Issues/MR Signed-off-by: Dmitriy Zaporozhets --- app/views/layouts/nav/_project.html.haml | 16 +---- app/views/projects/issues/_head.html.haml | 25 ++++++++ app/views/projects/issues/index.html.haml | 33 +++++----- app/views/projects/labels/index.html.haml | 63 ++++++++++--------- .../projects/merge_requests/_head.html.haml | 5 -- .../projects/merge_requests/index.html.haml | 26 ++++---- app/views/projects/milestones/index.html.haml | 29 +++++---- 7 files changed, 107 insertions(+), 90 deletions(-) create mode 100644 app/views/projects/issues/_head.html.haml delete mode 100644 app/views/projects/merge_requests/_head.html.haml diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index ca99ba8def..9dc76fce9e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -33,7 +33,7 @@ = navbar_icon('activity') %span Activity - + - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do = link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do @@ -62,13 +62,6 @@ %span Graphs - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - = navbar_icon('milestones') - %span - Milestones - - if project_nav_tab? :issues = nav_link(controller: :issues) do = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do @@ -86,13 +79,6 @@ Merge Requests %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - = icon('tags fw') - %span - Labels - - if project_nav_tab? :wiki = nav_link(controller: :wikis) do = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml new file mode 100644 index 0000000000..d0481be993 --- /dev/null +++ b/app/views/projects/issues/_head.html.haml @@ -0,0 +1,25 @@ +%ul.nav-links.sub-nav + %div{ class: (container_class) } + - if project_nav_tab?(:issues) + = nav_link(controller: :issues) do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do + %span + Issues + + - if project_nav_tab?(:merge_requests) + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do + %span + Merge Requests + + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + %span + Labels + + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + %span + Milestones diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7c1457553d..cd876b5ea6 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,23 +1,26 @@ +- @no_container = true - page_title "Issues" += render "projects/issues/head" = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") -.top-area - = render 'shared/issuable/nav', type: :issues - .nav-controls - - if current_user - = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do - = icon('rss') - %span.icon-label - Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do - New Issue +%div{ class: (container_class) } + .top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + - if current_user + = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do + = icon('rss') + %span.icon-label + Subscribe + = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) + - if can? current_user, :create_issue, @project + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + New Issue -= render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/filter', type: :issues -.issues-holder - = render "issues" + .issues-holder + = render "issues" diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 93583c9260..6e1baa46b0 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,35 +1,38 @@ +- @no_container = true - page_title "Labels" - hide_class = '' += render "projects/issues/head" -.top-area - .nav-text - Labels can be applied to issues and merge requests. - .nav-controls - - if can?(current_user, :admin_label, @project) - = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do - New label +%div{ class: (container_class) } + .top-area + .nav-text + Labels can be applied to issues and merge requests. + .nav-controls + - if can?(current_user, :admin_label, @project) + = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do + New label -.labels - - if can?(current_user, :admin_label, @project) - -# Only show it in the first page - - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1') - .prioritized-labels{ class: ('hide' if hide) } - %h5 Prioritized Labels - %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } - - if @prioritized_labels.present? - = render @prioritized_labels - - else - %p.empty-message No prioritized labels yet - .other-labels + .labels - if can?(current_user, :admin_label, @project) - %h5{ class: ('hide' if hide) } Other Labels - - if @labels.present? - %ul.content-list.manage-labels-list.js-other-labels - = render @labels - = paginate @labels, theme: 'gitlab' - - else - .nothing-here-block - - if can?(current_user, :admin_label, @project) - Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. - - else - No labels created + -# Only show it in the first page + - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1') + .prioritized-labels{ class: ('hide' if hide) } + %h5 Prioritized Labels + %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } + - if @prioritized_labels.present? + = render @prioritized_labels + - else + %p.empty-message No prioritized labels yet + .other-labels + - if can?(current_user, :admin_label, @project) + %h5{ class: ('hide' if hide) } Other Labels + - if @labels.present? + %ul.content-list.manage-labels-list.js-other-labels + = render @labels + = paginate @labels, theme: 'gitlab' + - else + .nothing-here-block + - if can?(current_user, :admin_label, @project) + Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. + - else + No labels created diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml deleted file mode 100644 index 19e4dab874..0000000000 --- a/app/views/projects/merge_requests/_head.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -.top-tabs - = link_to namespace_project_merge_requests_path(@project.namespace, @project), class: "tab #{'active' if current_page?(namespace_project_merge_requests_path(@project.namespace, @project)) }" do - %span - Merge Requests - diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index c8653cb0c3..9f948d41dd 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,18 +1,20 @@ +- @no_container = true - page_title "Merge Requests" - += render "projects/issues/head" = render 'projects/last_push' -.top-area - = render 'shared/issuable/nav', type: :merge_requests - .nav-controls - = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) +%div{ class: (container_class) } + .top-area + = render 'shared/issuable/nav', type: :merge_requests + .nav-controls + = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - - if merge_project - = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do - New Merge Request + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project + = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do + New Merge Request -= render 'shared/issuable/filter', type: :merge_requests + = render 'shared/issuable/filter', type: :merge_requests -.merge-requests-holder - = render 'merge_requests' + .merge-requests-holder + = render 'merge_requests' diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 60a5b83434..b0e0bdfff5 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,19 +1,22 @@ +- @no_container = true - page_title "Milestones" += render "projects/issues/head" -.top-area - = render 'shared/milestones_filter' +%div{ class: (container_class) } + .top-area + = render 'shared/milestones_filter' - .nav-controls - - if can?(current_user, :admin_milestone, @project) - = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do - New Milestone + .nav-controls + - if can?(current_user, :admin_milestone, @project) + = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do + New Milestone -.milestones - %ul.content-list - = render @milestones + .milestones + %ul.content-list + = render @milestones - - if @milestones.blank? - %li - .nothing-here-block No milestones to show + - if @milestones.blank? + %li + .nothing-here-block No milestones to show - = paginate @milestones, theme: "gitlab" + = paginate @milestones, theme: "gitlab" From d4b2f5d0f3baf4e3644c0e0f71ffb2995fb72a57 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Jun 2016 18:33:44 +0300 Subject: [PATCH 504/507] Render only issues/mr in subnav depends on context Signed-off-by: Dmitriy Zaporozhets --- app/views/layouts/nav/_project.html.haml | 2 +- app/views/projects/issues/_head.html.haml | 4 ++-- features/project/active_tab.feature | 12 ++++++++---- features/steps/project/active_tab.rb | 16 ++++++++-------- features/steps/shared/active_tab.rb | 4 ++-- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 9dc76fce9e..79cc5c0288 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -63,7 +63,7 @@ Graphs - if project_nav_tab? :issues - = nav_link(controller: :issues) do + = nav_link(controller: [:issues, :labels, :milestones]) do = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do = navbar_icon('issues') %span diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index d0481be993..583cc1b975 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -1,12 +1,12 @@ %ul.nav-links.sub-nav %div{ class: (container_class) } - - if project_nav_tab?(:issues) + - if project_nav_tab?(:issues) && current_controller?(:issues) = nav_link(controller: :issues) do = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do %span Issues - - if project_nav_tab?(:merge_requests) + - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) = nav_link(controller: :merge_requests) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do %span diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 26e6750302..c4f987a792 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -107,12 +107,16 @@ Feature: Project Active Tab Scenario: On Project Issues/Milestones Given I visit my project's issues page - And I click the "Milestones" tab - Then the active main tab should be Milestones + And I click the "Milestones" sub tab + Then the active main tab should be Issues + Then the active sub tab should be Milestones And no other main tabs should be active + And no other sub tabs should be active Scenario: On Project Issues/Labels Given I visit my project's issues page - And I click the "Labels" tab - Then the active main tab should be Labels + And I click the "Labels" sub tab + Then the active main tab should be Issues + Then the active sub tab should be Labels And no other main tabs should be active + And no other sub tabs should be active diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 745fd3471c..8004346318 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -77,14 +77,14 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps # Sub Tabs: Issues - step 'I click the "Milestones" tab' do - page.within('.layout-nav') do + step 'I click the "Milestones" sub tab' do + page.within('.sub-nav') do click_link('Milestones') end end - step 'I click the "Labels" tab' do - page.within('.layout-nav') do + step 'I click the "Labels" sub tab' do + page.within('.sub-nav') do click_link('Labels') end end @@ -93,11 +93,11 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps ensure_active_sub_tab('Issues') end - step 'the active main tab should be Milestones' do - ensure_active_main_tab('Milestones') + step 'the active sub tab should be Milestones' do + ensure_active_sub_tab('Milestones') end - step 'the active main tab should be Labels' do - ensure_active_main_tab('Labels') + step 'the active sub tab should be Labels' do + ensure_active_sub_tab('Labels') end end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index ace717b990..4eef7aff21 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -6,7 +6,7 @@ module SharedActiveTab end def ensure_active_sub_tab(content) - expect(find('div.content ul.nav-links li.active')).to have_content(content) + expect(find('.sub-nav li.active')).to have_content(content) end def ensure_active_sub_nav(content) @@ -18,7 +18,7 @@ module SharedActiveTab end step 'no other sub tabs should be active' do - expect(page).to have_selector('div.content ul.nav-links li.active', count: 1) + expect(page).to have_selector('.sub-nav li.active', count: 1) end step 'no other sub navs should be active' do From 95f15b14d19203e99cb1e25c8c40edb61bf914e5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Jun 2016 19:05:55 +0300 Subject: [PATCH 505/507] Render issues link on issues subnav unless you visit merge request controller Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/issues/_head.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 583cc1b975..166dae248b 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -1,6 +1,6 @@ %ul.nav-links.sub-nav %div{ class: (container_class) } - - if project_nav_tab?(:issues) && current_controller?(:issues) + - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) = nav_link(controller: :issues) do = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do %span From 308bb56781554b9d6436de674ae0291505fd2686 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 10 Jun 2016 19:12:24 +0300 Subject: [PATCH 506/507] Add CHANGELOG item for labels/milestones navigation change Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 0c712b445a..6487d33d5e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -63,6 +63,7 @@ v 8.9.0 (unreleased) - Toggling a task list item in a issue/mr description does not creates a Todo for mentions - Improved UX of date pickers on issue & milestone forms - Cache on the database if a project has an active external issue tracker. + - Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav v 8.8.5 (unreleased) - Ensure branch cleanup regardless of whether the GitHub import process succeeds From a1db70770ef16b60075ff9ca650eb34f0e002b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 10 Jun 2016 15:21:02 +0200 Subject: [PATCH 507/507] Remove unused MergeRequest#gitlab_merge_status method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/models/merge_request.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 29f3657091..7b8858b24d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -276,14 +276,6 @@ class MergeRequest < ActiveRecord::Base true end - def gitlab_merge_status - if work_in_progress? - "work_in_progress" - else - merge_status_name - end - end - def can_cancel_merge_when_build_succeeds?(current_user) can_be_merged_by?(current_user) || self.author == current_user end