From e3f8ed5cefcae3df9179f7b1198bf8026918653e Mon Sep 17 00:00:00 2001 From: Felix Ruess Date: Thu, 4 Feb 2016 17:51:12 +0000 Subject: [PATCH 0001/1306] doc: fix git lfs workaround for using http instead of https the url should only point to `info/lfs` instead of `info/lfs/batch/objects` --- doc/workflow/lfs/manage_large_binaries_with_git_lfs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index b59e92cb317..596478bf3cd 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -104,7 +104,7 @@ To prevent this from happening, set the lfs url in project Git config: ```bash -git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/objects/batch" +git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs" ``` ### Credentials are always required when pushing an object From 33520f1f699502740e67682b17ccd9432b6e693b Mon Sep 17 00:00:00 2001 From: a-tal Date: Fri, 26 Feb 2016 20:49:50 -0800 Subject: [PATCH 0002/1306] fix example urls for (de)associating runners to projects --- doc/api/runners.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/runners.md b/doc/api/runners.md index cc6c6b7cb2f..ddfa298f79d 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -275,7 +275,7 @@ POST /projects/:id/runners | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners" -F "runner_id=9" +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" -F "runner_id=9" ``` Example response: @@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners/9" +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9" ``` Example response: From 6a0ea605e8b48deacbb4e93f7bb1d9b9abd2f7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Emin=20=C4=B0NA=C3=87?= Date: Wed, 16 Mar 2016 03:16:25 +0200 Subject: [PATCH 0003/1306] Change deprecated usage of rendering without response body `render nothing: true` has been deprecated. For more information see [pr](https://github.com/rails/rails/pull/20336) --- app/controllers/admin/abuse_reports_controller.rb | 2 +- app/controllers/admin/broadcast_messages_controller.rb | 2 +- app/controllers/admin/keys_controller.rb | 2 +- app/controllers/admin/spam_logs_controller.rb | 2 +- app/controllers/admin/users_controller.rb | 2 +- app/controllers/concerns/toggle_subscription_action.rb | 2 +- app/controllers/dashboard/todos_controller.rb | 4 ++-- app/controllers/groups/group_members_controller.rb | 2 +- app/controllers/profiles/emails_controller.rb | 2 +- app/controllers/profiles/keys_controller.rb | 2 +- app/controllers/projects/milestones_controller.rb | 2 +- app/controllers/projects/notes_controller.rb | 4 ++-- app/controllers/projects/project_members_controller.rb | 4 ++-- app/controllers/projects/protected_branches_controller.rb | 2 +- spec/controllers/projects/raw_controller_spec.rb | 2 +- 15 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index 2463cfa87be..76fc10bcc10 100644 --- a/app/controllers/admin/abuse_reports_controller.rb +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -9,6 +9,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController abuse_report.remove_user if params[:remove_user] abuse_report.destroy - render nothing: true + head :ok end end diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index fc342924987..82055006ac0 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -32,7 +32,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_default(default: { action: 'index' }) } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb index cb33fdd9763..054bb52b696 100644 --- a/app/controllers/admin/keys_controller.rb +++ b/app/controllers/admin/keys_controller.rb @@ -6,7 +6,7 @@ class Admin::KeysController < Admin::ApplicationController respond_to do |format| format.html - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index 377e9741e5f..3a2f0185315 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -11,7 +11,7 @@ class Admin::SpamLogsController < Admin::ApplicationController redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed." else spam_log.destroy - render nothing: true + head :ok end end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 87f4fb455b8..39c0c22f9b6 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -135,7 +135,7 @@ class Admin::UsersController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb index 8a43c0b93c4..9e3b9be2ff4 100644 --- a/app/controllers/concerns/toggle_subscription_action.rb +++ b/app/controllers/concerns/toggle_subscription_action.rb @@ -6,7 +6,7 @@ module ToggleSubscriptionAction subscribable_resource.toggle_subscription(current_user) - render nothing: true + head :ok end private diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 43cf8fa71af..d8ba51294cf 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -10,7 +10,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' } - format.js { render nothing: true } + format.js { head :ok } end end @@ -19,7 +19,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 0e902c4bb43..68f70120894 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -43,7 +43,7 @@ class Groups::GroupMembersController < Groups::ApplicationController respond_to do |format| format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 0ede9b8e21b..1c24c4db993 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -24,7 +24,7 @@ class Profiles::EmailsController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_emails_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index b88c080352b..9906493666a 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -27,7 +27,7 @@ class Profiles::KeysController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_keys_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index da46731d945..6579f4f8c89 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -68,7 +68,7 @@ class Projects::MilestonesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_milestones_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 1b9dd568043..d91ab1cee1d 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -44,7 +44,7 @@ class Projects::NotesController < Projects::ApplicationController end respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end @@ -53,7 +53,7 @@ class Projects::NotesController < Projects::ApplicationController note.update_attribute(:attachment, nil) respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e7bddc4a6f1..b150e9ef029 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -55,7 +55,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController format.html do redirect_to namespace_project_project_members_path(@project.namespace, @project) end - format.js { render nothing: true } + format.js { head :ok } end end @@ -81,7 +81,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController respond_to do |format| format.html { redirect_to dashboard_projects_path, notice: "You left the project." } - format.js { render nothing: true } + format.js { head :ok } end else if current_user == @project.owner diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index e49259c34b6..efa7bf14d0f 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -39,7 +39,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_protected_branches_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 1caa476d37d..fb29274c687 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -42,7 +42,7 @@ describe Projects::RawController do before do public_project.lfs_objects << lfs_object allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) - allow(controller).to receive(:send_file) { controller.render nothing: true } + allow(controller).to receive(:send_file) { controller.head :ok } end it 'serves the file' do From 19a5e7c95e91baca58836ad3ae189190c9ba4ca2 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 23 Mar 2016 14:04:09 +0100 Subject: [PATCH 0004/1306] 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 cd26dca0998..7e274b4209b 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 0005/1306] 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 657ee94cfd7..5f5dc1adadf 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 00000000000..129e87dbf13 --- /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 4a3c23b7c1c..47ab1a89b8d 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 0006/1306] 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 47ab1a89b8d..021eab89c2a 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 0007/1306] 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 021eab89c2a..eace7516e91 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 7e274b4209b..ef0b83fd475 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 0008/1306] 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 ef0b83fd475..1e3f3f3e617 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 0009/1306] 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 eace7516e91..40c149abda2 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 0010/1306] 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 1e3f3f3e617..3a6a9b7a70d 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 0011/1306] 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 3a6a9b7a70d..a26b986aeb0 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 0012/1306] 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 a26b986aeb0..967e0ab6e74 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 0013/1306] 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 967e0ab6e74..c1aad48ad04 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 0014/1306] 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 129e87dbf13..a26ab736115 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 c1aad48ad04..1fa14cadc0f 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 1824fb0603c798ec467ea3529570031e7dbb2986 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 5 Apr 2016 15:20:58 +0000 Subject: [PATCH 0015/1306] Fix broken link in CI quickstart docs The space between the [label] and the (link) caused it to be interpreted literally. --- doc/ci/quick_start/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 9aba4326e11..aae9ccae1d3 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -212,8 +212,8 @@ If you want to receive e-mail notifications about the result status of the builds, you should explicitly enable the **Builds Emails** service under your project's settings. -For more information read the [Builds emails service documentation] -(../../project_services/builds_emails.md). +For more information read the +[Builds emails service documentation](../../project_services/builds_emails.md). ## Builds badge From ac4d3dc5ccba32e026250ab48fe7f29bcf4ddd97 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 6 Apr 2016 17:23:16 +0200 Subject: [PATCH 0016/1306] 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 1fa14cadc0f..5d41d973083 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 0017/1306] 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 cdcaae8094c..e2363b91265 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 0018/1306] 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 a26ab736115..6dd7a683b0e 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 c3ddd4c2680..5b2982e4994 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 0019/1306] 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 5d41d973083..8b217684911 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 0020/1306] 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 6dd7a683b0e..11e17510cb9 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 35266de2f0e91ac73995ab8ced1bbcb12e35f773 Mon Sep 17 00:00:00 2001 From: Chris McKnight Date: Wed, 6 Jan 2016 11:20:52 -0600 Subject: [PATCH 0021/1306] Updates git lfs initialize command --- doc/workflow/lfs/manage_large_binaries_with_git_lfs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index ba91685a20b..83db44c10b1 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -44,7 +44,7 @@ check it into your Git repository: ```bash git clone git@gitlab.example.com:group/project.git -git lfs init # initialize the Git LFS project project +git lfs install # initialize the Git LFS project project git lfs track "*.iso" # select the file extensions that you want to treat as large files ``` @@ -152,4 +152,4 @@ If you are using OS X you can use `osxkeychain` to store and encrypt your creden For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). More details about various methods of storing the user credentials can be found -on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file +on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). From e4d9d4e55b67ab04e46ee3f72c5496f91687a205 Mon Sep 17 00:00:00 2001 From: "P.S.V.R" Date: Mon, 11 Apr 2016 16:45:00 +0800 Subject: [PATCH 0022/1306] fix #15127 ActiveJob::DeserializationError thrown send_devise_notification pre-maturely enqueued the task when the user instance has not yet been committed into the database, causing a record-not-found in the other sidekiq process. devise-async has already been taking care of asynchronous mail sending, we just need to run it inside queue `mailers` instead of `mailer` to enable it. --- app/models/user.rb | 5 ----- config/initializers/devise_async.rb | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 2b0bee2099f..531dc9a5aed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -845,11 +845,6 @@ class User < ActiveRecord::Base other.select(:id)]) end - # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration - def send_devise_notification(notification, *args) - devise_mailer.send(notification, self, *args).deliver_later - end - def ensure_external_user_rights return unless self.external? diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb index 05a1852cdbd..fa602cbe554 100644 --- a/config/initializers/devise_async.rb +++ b/config/initializers/devise_async.rb @@ -1 +1,2 @@ Devise::Async.backend = :sidekiq +Devise::Async.queue = :mailers From e84c155f092600b90be291f0f7bb649811fa53fb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 12 Apr 2016 16:16:39 +0200 Subject: [PATCH 0023/1306] WIP --- .../projects/pipelines_controller.rb | 102 ++++++++++++++++++ app/models/ability.rb | 7 +- app/views/layouts/nav/_project.html.haml | 7 ++ .../projects/ci/commits/_commit.html.haml | 73 +++++++++++++ .../ci_commits/_header_title.html.haml | 1 + app/views/projects/ci_commits/index.html.haml | 65 +++++++++++ app/views/projects/ci_commits/new.html.haml | 25 +++++ config/routes.rb | 7 ++ 8 files changed, 286 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index e69de29bb2d..764c8cc9cca 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -0,0 +1,102 @@ +class Projects::PipelineController < Projects::ApplicationController + before_action :ci_commit, except: [:index, :new, :create] + before_action :authorize_read_pipeline! + before_action :authorize_create_pipeline!, only: [:new, :create] + before_action :authorize_update_pipeline!, only: [:retry, :cancel] + layout 'project' + + def index + @scope = params[:scope] + @all_commits = project.ci_commits + @commits = @all_commits.order(id: :desc) + @commits = + case @scope + when 'latest' + @commits + when 'running' + @commits.running_or_pending + when 'branches' + refs = project.repository.branches.map(&:name) + ids = @all_commits.where(ref: refs).group(:ref).select('max(id)') + @commits.where(id: ids) + when 'tags' + refs = project.repository.tags.map(&:name) + ids = @all_commits.where(ref: refs).group(:ref).select('max(id)') + @commits.where(id: ids) + else + @commits + end + @commits = @commits.page(params[:page]).per(30) + end + + def new + end + + def create + ref_names = project.repository.ref_names + unless ref_names.include?(params[:ref]) + @error = 'Reference not found' + render action: 'new' + return + end + + commit = project.commit(params[:ref]) + unless commit + @error = 'Commit not found' + render action: 'new' + return + end + + ci_commit = project.ci_commit(commit.id, params[:ref]) + if ci_commit + @error = 'Pipeline already created' + render action: 'new' + return + end + + # Skip creating ci_commit when no gitlab-ci.yml is found + commit = project.ci_commits.new(sha: commit.id, ref: params[:ref], before_sha: Gitlab::Git::BLANK_SHA) + unless commit.config_processor + @error = commit.yaml_errors || 'Missing .gitlab-ci.yml file' + render action: 'new' + return + end + + Ci::Commit.transaction do + commit.save! + commit.create_builds(params[:ref], false, current_user) + end + + redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.id) + end + + def show + @commit = @ci_commit.commit + @builds = @ci_commit.builds + @statuses = @ci_commit.statuses + + respond_to do |format| + format.html + end + end + + def retry + ci_commit.builds.latest.failed.select(&:retryable?).each(&:retry) + + redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) + end + + def cancel + ci_commit.builds.running_or_pending.each(&:cancel) + + redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) + end + + def retry_builds + end + private + + def ci_commit + @ci_commit ||= project.ci_commits.find_by!(id: params[:id]) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index c0bf6def7c5..ec5ac54c277 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -195,6 +195,7 @@ class Ability :admin_label, :read_commit_status, :read_build, + :read_pipeline, ] end @@ -206,6 +207,8 @@ class Ability :update_commit_status, :create_build, :update_build, + :create_pipeline, + :update_pipeline, :create_merge_request, :create_wiki, :push_code @@ -234,7 +237,8 @@ class Ability :admin_wiki, :admin_project, :admin_commit_status, - :admin_build + :admin_build, + :admin_pipeline ] end @@ -277,6 +281,7 @@ class Ability unless project.builds_enabled rules += named_abilities('build') + rules += named_abilities('pipeline') end rules diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 86b46e8c75e..fcce1b1dc98 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -39,6 +39,13 @@ Commits - if project_nav_tab? :builds + = nav_link(controller: %w(ci_commits)) do + = link_to project_ci_commits_path(@project), title: 'Pipelines', class: 'shortcuts-builds' do + = icon('ship fw') + %span + Pipelines + %span.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count(:all)) + = nav_link(controller: %w(builds)) do = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do = icon('cubes fw') diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index e69de29bb2d..29efcc9cfdd 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -0,0 +1,73 @@ +- status = commit.status +%tr.commit + %td.commit-link + = link_to namespace_project_commit_url(@project.namespace, @project, commit), class: "ci-status ci-#{status}" do + = ci_icon_for_status(status) + %strong ##{commit.id} + + %td + %div + - if commit.ref + = link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref) +   + - if commit.tag? + %span.label.label-primary tag + - if commit.branch? + %span.label.label-primary branch + - if commit.trigger_requests.any? + %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?) + %span.label.label-warning stuck + + - if commit_data = commit.commit_data + = render 'projects/branches/commit', commit: commit_data, project: @project + - else + %p + Cant find HEAD commit for this branch + + - stages.each do |stage| + %td + - status = commit.statuses.latest.where(stage: stage).status + %span.has-tooltip(title="#{status || "missing"}"){class: "ci-status-icon-#{status || "skipped"}"} + = ci_icon_for_status(status || "missing") + -#- if status + -# = ci_status_with_icon(status) + -#- else + -# = ci_status_with_icon('missing') + + %td + - if commit.started_at && commit.finished_at + %p + #{duration_in_words(commit.finished_at, commit.started_at)} + - if commit.finished_at + %p + #{time_ago_with_tooltip(commit.finished_at)} + + %td.content + .controls.hidden-xs.pull-right + - artifacts = commit.builds.latest.select { |status| status.artifacts? } + - if artifacts.present? + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + = icon('download') + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - artifacts.each do |build| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do + %i.fa.fa-download + %span #{build.name} +   + + - if can?(current_user, :update_pipeline, @project) + - if commit.retryable? + = link_to retry_namespace_project_ci_commit_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do + = icon("repeat") + +   + + - if commit.active? + = link_to cancel_namespace_project_ci_commit_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do + = icon("remove cred") diff --git a/app/views/projects/ci_commits/_header_title.html.haml b/app/views/projects/ci_commits/_header_title.html.haml index e69de29bb2d..27c125ca40f 100644 --- a/app/views/projects/ci_commits/_header_title.html.haml +++ b/app/views/projects/ci_commits/_header_title.html.haml @@ -0,0 +1 @@ +- header_title project_title(@project, "Pipelines", project_ci_commits_path(@project)) diff --git a/app/views/projects/ci_commits/index.html.haml b/app/views/projects/ci_commits/index.html.haml index e69de29bb2d..0347c220382 100644 --- a/app/views/projects/ci_commits/index.html.haml +++ b/app/views/projects/ci_commits/index.html.haml @@ -0,0 +1,65 @@ +- page_title "Pipelines" += render "header_title" + +.top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to project_ci_commits_path(@project) do + All + %span.badge.js-totalbuilds-count + = number_with_delimiter(@all_commits.count(:id)) + + %li{class: ('active' if @scope == 'branches')} + = link_to project_ci_commits_path(@project, scope: :branches) do + Branches + %span.badge.js-running-count + = number_with_delimiter(@all_commits.running_or_pending.count(:id)) + + %li{class: ('active' if @scope == 'tags')} + = link_to project_ci_commits_path(@project, scope: :tags) do + Tags + %span.badge.js-running-count + = number_with_delimiter(@all_commits.running_or_pending.count(:id)) + + %li{class: ('active' if @scope == 'running')} + = link_to project_ci_commits_path(@project, scope: :running) do + Failed + %span.badge.js-running-count + = number_with_delimiter(@all_commits.running_or_pending.count(:id)) + + .nav-controls + - if can? current_user, :create_pipeline, @project + = link_to new_namespace_project_ci_commit_path(@project.namespace, @project), class: 'btn btn-create' do + = icon('plus') + New + + - if can?(current_user, :update_build, @project) + - 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 + +.gray-content-block + Pipelines for #{(@scope || 'changes')} on this project + +%ul.content-list + - stages = @commits.stages + - if @commits.blank? + %li + .nothing-here-block No pipelines to show + - else + .table-holder + %table.table.builds + %tbody + %th Pipeline ID + %th Commit + - @commits.stages.each do |stage| + %th + = stage.titleize + %th + %th + = render @commits.includes(:statuses).includes(:builds), commit_sha: true, stage: true, allow_retry: true, stages: stages + + = paginate @commits, theme: 'gitlab' diff --git a/app/views/projects/ci_commits/new.html.haml b/app/views/projects/ci_commits/new.html.haml index e69de29bb2d..e9a22bbb157 100644 --- a/app/views/projects/ci_commits/new.html.haml +++ b/app/views/projects/ci_commits/new.html.haml @@ -0,0 +1,25 @@ +- page_title "New Pipeline" += render "header_title" + +- if @error + .alert.alert-danger + %button{ type: "button", class: "close", "data-dismiss" => "alert"} × + = @error +%h3.page-title + New Pipeline +%hr + += form_tag namespace_project_ci_commits_path, method: :post, id: "new-pipeline-form", class: "form-horizontal js-create-branch-form js-requires-input" do + .form-group + = label_tag :ref, 'Create for', class: 'control-label' + .col-sm-10 + = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' + .help-block Existing branch name, tag + .form-actions + = button_tag 'Create pipeline', class: 'btn btn-create', tabindex: 3 + = link_to 'Cancel', namespace_project_ci_commits_path(@project.namespace, @project), class: 'btn btn-cancel' + +:javascript + var availableRefs = #{@project.repository.ref_names.to_json}; + + new NewBranchForm($('.js-create-branch-form'), availableRefs) diff --git a/config/routes.rb b/config/routes.rb index 842fbb99843..841b3f26272 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -654,6 +654,13 @@ Rails.application.routes.draw do resource :variables, only: [:show, :update] resources :triggers, only: [:index, :create, :destroy] + resources :pipelines, only: [:index, :new, :create] do + member do + post :cancel + post :retry + end + end + resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all From 406a796f76824e18f4dca2d29c41dcc3d2e4d457 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 12 Apr 2016 19:57:22 +0200 Subject: [PATCH 0024/1306] Make Pipeline view work --- app/assets/stylesheets/framework/tables.scss | 19 +++++++++++++++++ .../projects/pipelines_controller.rb | 2 +- app/helpers/ci_status_helper.rb | 2 +- app/helpers/gitlab_routing_helper.rb | 2 +- app/models/ci/commit.rb | 4 ++++ app/views/layouts/nav/_project.html.haml | 2 +- .../projects/ci/commits/_commit.html.haml | 21 +++++++++---------- .../ci_commits/_header_title.html.haml | 1 - .../pipelines/_header_title.html.haml | 1 + .../{ci_commits => pipelines}/index.html.haml | 14 ++++++------- .../{ci_commits => pipelines}/new.html.haml | 4 ++-- spec/models/project_spec.rb | 2 +- 12 files changed, 48 insertions(+), 26 deletions(-) delete mode 100644 app/views/projects/ci_commits/_header_title.html.haml create mode 100644 app/views/projects/pipelines/_header_title.html.haml rename app/views/projects/{ci_commits => pipelines}/index.html.haml (76%) rename app/views/projects/{ci_commits => pipelines}/new.html.haml (72%) diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 75b770ae5a2..3a7f5bb932e 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -34,6 +34,25 @@ table { font-weight: normal; font-size: 15px; border-bottom: 1px solid $border-color; + + .rotate { + height: 140px; + white-space: nowrap; + } + + .rotate > div { + transform: + /* Magic Numbers */ + translate(25px, 51px) + /* 45 is really 360 - 45 */ + rotate(315deg); + width: 30px; + } + + .rotate > div > span { + border-bottom: 1px solid #ccc; + padding: 5px 10px; + } } td { diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 764c8cc9cca..a3e72fbdef1 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,4 +1,4 @@ -class Projects::PipelineController < Projects::ApplicationController +class Projects::PipelinesController < Projects::ApplicationController before_action :ci_commit, except: [:index, :new, :create] before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index effa7ce77e1..3f7282d0c6c 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -37,7 +37,7 @@ module CiStatusHelper return unless ci_commit.is_a?(Commit) || ci_commit.is_a?(Ci::Commit) link_to ci_icon_for_status(ci_commit.status), - project_ci_commit_path(ci_commit.project, ci_commit), + project_pipeline_path(ci_commit.project, ci_commit), class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", title: "Build #{ci_label_for_status(ci_commit.status)}", data: { toggle: 'tooltip', placement: tooltip_placement } diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f1af8e163cd..ed0db04e069 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -37,7 +37,7 @@ module GitlabRoutingHelper builds_namespace_project_commit_path(project.namespace, project, commit.id) end - def project_ci_commit_path(project, ci_commit) + def project_pipeline_path(project, ci_commit) builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 8865bd76bd2..687654d3c89 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -94,6 +94,10 @@ module Ci end end + def triggered? + trigger_requests.any? + end + def invalidate write_attribute(:status, nil) write_attribute(:started_at, nil) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index fcce1b1dc98..b58d8270230 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -40,7 +40,7 @@ - if project_nav_tab? :builds = nav_link(controller: %w(ci_commits)) do - = link_to project_ci_commits_path(@project), title: 'Pipelines', class: 'shortcuts-builds' do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-builds' do = icon('ship fw') %span Pipelines diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index 29efcc9cfdd..7c6ba216386 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -1,7 +1,7 @@ - status = commit.status %tr.commit %td.commit-link - = link_to namespace_project_commit_url(@project.namespace, @project, commit), class: "ci-status ci-#{status}" do + = link_to namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "ci-status ci-#{status}" do = ci_icon_for_status(status) %strong ##{commit.id} @@ -14,7 +14,7 @@ %span.label.label-primary tag - if commit.branch? %span.label.label-primary branch - - if commit.trigger_requests.any? + - if commit.triggered? %span.label.label-primary triggered - if commit.yaml_errors.present? %span.label.label-danger.has-tooltip(title="#{commit.yaml_errors}") yaml invalid @@ -27,22 +27,21 @@ %p Cant find HEAD commit for this branch + - stages_status = commit.statuses.stages_status - stages.each do |stage| %td - - status = commit.statuses.latest.where(stage: stage).status - %span.has-tooltip(title="#{status || "missing"}"){class: "ci-status-icon-#{status || "skipped"}"} - = ci_icon_for_status(status || "missing") - -#- if status - -# = ci_status_with_icon(status) - -#- else - -# = ci_status_with_icon('missing') + - if status = stages_status[stage] + %span.has-tooltip(title="#{status}"){class: "ci-status-icon-#{status}"} + = ci_icon_for_status(status) %td - if commit.started_at && commit.finished_at %p + %i.fa.fa-late-o #{duration_in_words(commit.finished_at, commit.started_at)} - if commit.finished_at %p + %i.fa.fa-date-o #{time_ago_with_tooltip(commit.finished_at)} %td.content @@ -63,11 +62,11 @@ - if can?(current_user, :update_pipeline, @project) - if commit.retryable? - = link_to retry_namespace_project_ci_commit_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do + = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do = icon("repeat")   - if commit.active? - = link_to cancel_namespace_project_ci_commit_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do + = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do = icon("remove cred") diff --git a/app/views/projects/ci_commits/_header_title.html.haml b/app/views/projects/ci_commits/_header_title.html.haml deleted file mode 100644 index 27c125ca40f..00000000000 --- a/app/views/projects/ci_commits/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Pipelines", project_ci_commits_path(@project)) diff --git a/app/views/projects/pipelines/_header_title.html.haml b/app/views/projects/pipelines/_header_title.html.haml new file mode 100644 index 00000000000..faf63d64a79 --- /dev/null +++ b/app/views/projects/pipelines/_header_title.html.haml @@ -0,0 +1 @@ +- header_title project_title(@project, "Pipelines", project_pipelines_path(@project)) diff --git a/app/views/projects/ci_commits/index.html.haml b/app/views/projects/pipelines/index.html.haml similarity index 76% rename from app/views/projects/ci_commits/index.html.haml rename to app/views/projects/pipelines/index.html.haml index 0347c220382..b9877cd37be 100644 --- a/app/views/projects/ci_commits/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -4,32 +4,32 @@ .top-area %ul.nav-links %li{class: ('active' if @scope.nil?)} - = link_to project_ci_commits_path(@project) do + = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count = number_with_delimiter(@all_commits.count(:id)) %li{class: ('active' if @scope == 'branches')} - = link_to project_ci_commits_path(@project, scope: :branches) do + = link_to project_pipelines_path(@project, scope: :branches) do Branches %span.badge.js-running-count = number_with_delimiter(@all_commits.running_or_pending.count(:id)) %li{class: ('active' if @scope == 'tags')} - = link_to project_ci_commits_path(@project, scope: :tags) do + = link_to project_pipelines_path(@project, scope: :tags) do Tags %span.badge.js-running-count = number_with_delimiter(@all_commits.running_or_pending.count(:id)) %li{class: ('active' if @scope == 'running')} - = link_to project_ci_commits_path(@project, scope: :running) do + = link_to project_pipelines_path(@project, scope: :running) do Failed %span.badge.js-running-count = number_with_delimiter(@all_commits.running_or_pending.count(:id)) .nav-controls - if can? current_user, :create_pipeline, @project - = link_to new_namespace_project_ci_commit_path(@project.namespace, @project), class: 'btn btn-create' do + = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do = icon('plus') New @@ -56,10 +56,10 @@ %th Pipeline ID %th Commit - @commits.stages.each do |stage| - %th + %th.rotate = stage.titleize %th %th - = render @commits.includes(:statuses).includes(:builds), commit_sha: true, stage: true, allow_retry: true, stages: stages + = render @commits, commit_sha: true, stage: true, allow_retry: true, stages: stages = paginate @commits, theme: 'gitlab' diff --git a/app/views/projects/ci_commits/new.html.haml b/app/views/projects/pipelines/new.html.haml similarity index 72% rename from app/views/projects/ci_commits/new.html.haml rename to app/views/projects/pipelines/new.html.haml index e9a22bbb157..39b1571b9cf 100644 --- a/app/views/projects/ci_commits/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -9,7 +9,7 @@ New Pipeline %hr -= form_tag namespace_project_ci_commits_path, method: :post, id: "new-pipeline-form", class: "form-horizontal js-create-branch-form js-requires-input" do += form_tag namespace_project_pipelines_path, method: :post, id: "new-pipeline-form", class: "form-horizontal js-create-branch-form js-requires-input" do .form-group = label_tag :ref, 'Create for', class: 'control-label' .col-sm-10 @@ -17,7 +17,7 @@ .help-block Existing branch name, tag .form-actions = button_tag 'Create pipeline', class: 'btn btn-create', tabindex: 3 - = link_to 'Cancel', namespace_project_ci_commits_path(@project.namespace, @project), class: 'btn btn-cancel' + = link_to 'Cancel', namespace_project_pipelines_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript var availableRefs = #{@project.repository.ref_names.to_json}; diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1688e91ca62..59df2c5cb87 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -62,7 +62,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) } From f5d24e60f842096f670593fb4dd0d29c3f5d4fcc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 13 Apr 2016 13:01:08 +0200 Subject: [PATCH 0025/1306] Pipeline view --- app/assets/stylesheets/framework/tables.scss | 3 - .../projects/pipelines_controller.rb | 56 +++++++------------ app/models/ci/commit.rb | 7 +++ app/views/projects/ci/builds/_build.html.haml | 4 +- .../projects/ci/commits/_commit.html.haml | 41 +++++++------- .../projects/commit/_ci_commit.html.haml | 30 +++------- app/views/projects/pipelines/index.html.haml | 33 +++++------ app/views/projects/pipelines/show.html.haml | 3 + config/routes.rb | 2 +- 9 files changed, 79 insertions(+), 100 deletions(-) create mode 100644 app/views/projects/pipelines/show.html.haml diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 3a7f5bb932e..9d6a6c5b237 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -38,9 +38,6 @@ table { .rotate { height: 140px; white-space: nowrap; - } - - .rotate > div { transform: /* Magic Numbers */ translate(25px, 51px) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index a3e72fbdef1..b2ee5573bfc 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,5 +1,5 @@ class Projects::PipelinesController < Projects::ApplicationController - before_action :ci_commit, except: [:index, :new, :create] + before_action :pipeline, except: [:index, :new, :create] before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] @@ -7,26 +7,24 @@ class Projects::PipelinesController < Projects::ApplicationController def index @scope = params[:scope] - @all_commits = project.ci_commits - @commits = @all_commits.order(id: :desc) - @commits = + @all_pipelines = project.ci_commits + @pipelines = @all_pipelines.order(id: :desc) + @pipelines = case @scope - when 'latest' - @commits when 'running' - @commits.running_or_pending + @pipelines.running_or_pending when 'branches' - refs = project.repository.branches.map(&:name) - ids = @all_commits.where(ref: refs).group(:ref).select('max(id)') - @commits.where(id: ids) + @branches = project.repository.branches.map(&:name) + @branches_ids = @all_pipelines.where(ref: @branches).group(:ref).select('max(id)') + @pipelines.where(id: @branches_ids) when 'tags' - refs = project.repository.tags.map(&:name) - ids = @all_commits.where(ref: refs).group(:ref).select('max(id)') - @commits.where(id: ids) + @tags = project.repository.tags.map(&:name) + @tags_ids = @all_pipelines.where(ref: @tags).group(:ref).select('max(id)') + @pipelines.where(id: @tags_ids) else - @commits + @pipelines end - @commits = @commits.page(params[:page]).per(30) + @pipelines = @pipelines.page(params[:page]).per(30) end def new @@ -47,56 +45,44 @@ class Projects::PipelinesController < Projects::ApplicationController return end - ci_commit = project.ci_commit(commit.id, params[:ref]) - if ci_commit - @error = 'Pipeline already created' - render action: 'new' - return - end + pipeline = project.ci_commits.new(sha: commit.id, ref: params[:ref], before_sha: Gitlab::Git::BLANK_SHA) # Skip creating ci_commit when no gitlab-ci.yml is found - commit = project.ci_commits.new(sha: commit.id, ref: params[:ref], before_sha: Gitlab::Git::BLANK_SHA) - unless commit.config_processor - @error = commit.yaml_errors || 'Missing .gitlab-ci.yml file' + unless pipeline.config_processor + @error = pipeline.yaml_errors || 'Missing .gitlab-ci.yml file' render action: 'new' return end Ci::Commit.transaction do commit.save! - commit.create_builds(params[:ref], false, current_user) + commit.create_builds(current_user) end redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.id) end def show - @commit = @ci_commit.commit - @builds = @ci_commit.builds - @statuses = @ci_commit.statuses - respond_to do |format| format.html end end def retry - ci_commit.builds.latest.failed.select(&:retryable?).each(&:retry) + pipeline.builds.latest.failed.select(&:retryable?).each(&:retry) redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) end def cancel - ci_commit.builds.running_or_pending.each(&:cancel) + pipeline.builds.running_or_pending.each(&:cancel) redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) end - def retry_builds - end private - def ci_commit - @ci_commit ||= project.ci_commits.find_by!(id: params[:id]) + def pipeline + @pipeline ||= project.ci_commits.find_by!(id: params[:id]) end end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 687654d3c89..7991b987e35 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -94,6 +94,13 @@ module Ci end end + def latest? + return false unless ref + commit = project.commit(ref) + return false unless commit + commit.sha == sha + end + def triggered? trigger_requests.any? end diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 218d396b898..7ded4828b2f 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -13,7 +13,9 @@ %strong ##{build.id} - if build.stuck? - %i.fa.fa-warning.text-warning + %i.fa.fa-warning.text-warning.has-tooltip(title="Build is stuck. Check runners.") + - if defined?(retried) && retried + %i.fa.fa-warning.has-tooltip(title="Build was retried") - if defined?(commit_sha) && commit_sha %td diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index 7c6ba216386..32f85cb8f8c 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -1,19 +1,21 @@ - status = commit.status %tr.commit %td.commit-link - = link_to namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "ci-status ci-#{status}" do + = link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: "ci-status ci-#{status}" do = ci_icon_for_status(status) %strong ##{commit.id} %td - %div + %div.branch-commit - if commit.ref - = link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref) + = link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref), class: "monospace" + · + = link_to commit.short_sha, namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "commit-id monospace"   + - if commit.latest? + %span.label.label-success latest - if commit.tag? %span.label.label-primary tag - - if commit.branch? - %span.label.label-primary branch - if commit.triggered? %span.label.label-primary triggered - if commit.yaml_errors.present? @@ -21,32 +23,36 @@ - if commit.builds.any?(&:stuck?) %span.label.label-warning stuck - - if commit_data = commit.commit_data - = render 'projects/branches/commit', commit: commit_data, project: @project - - else - %p - Cant find HEAD commit for this branch + %p + %span + - if commit_data = commit.commit_data + = link_to_gfm commit_data.title, 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.each do |stage| %td - if status = stages_status[stage] - %span.has-tooltip(title="#{status}"){class: "ci-status-icon-#{status}"} + %span.has-tooltip(title="#{stage.titleize}: #{status}"){class: "ci-status-icon-#{status}"} = ci_icon_for_status(status) %td - if commit.started_at && commit.finished_at %p - %i.fa.fa-late-o + %i.fa.fa-clock-o +   #{duration_in_words(commit.finished_at, commit.started_at)} - if commit.finished_at %p - %i.fa.fa-date-o + %i.fa.fa-calendar +   #{time_ago_with_tooltip(commit.finished_at)} - %td.content + %td .controls.hidden-xs.pull-right - - artifacts = commit.builds.latest.select { |status| status.artifacts? } + - artifacts = commit.builds.latest - if artifacts.present? .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} @@ -58,15 +64,12 @@ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do %i.fa.fa-download %span #{build.name} -   - if can?(current_user, :update_pipeline, @project) - - if commit.retryable? + - if commit.retryable? && commit.builds.failed.any? = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do = icon("repeat") -   - - if commit.active? = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do = icon("remove cred") diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 06520e40bd9..582ce61a64a 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -2,12 +2,15 @@ .pull-right - if can?(current_user, :update_build, @project) - if ci_commit.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: 'btn btn-grouped btn-primary', method: :post + = link_to "Retry failed", retry_namespace_project_pipeline_path(@project.namespace, @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_builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + = link_to "Cancel running", cancel_namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post .oneline + Pipeline + = link_to "##{ci_commit.id}", namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), class: "monospace" + with = pluralize ci_commit.statuses.count(:id), "build" - if ci_commit.ref for @@ -17,7 +20,7 @@ for commit = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" - if ci_commit.duration > 0 - in + took = time_interval_in_words ci_commit.duration - if ci_commit.yaml_errors.present? @@ -47,23 +50,4 @@ %th - builds = ci_commit.statuses.latest.ordered = render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true - -- if ci_commit.retried.any? - .gray-content-block.second-block - Retried builds - - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @project.build_coverage_enabled? - %th Coverage - %th - = render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false + = render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false, retried: true diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index b9877cd37be..838b2986d4f 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -7,25 +7,21 @@ = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count - = number_with_delimiter(@all_commits.count(:id)) + = number_with_delimiter(@all_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(@all_pipelines.running_or_pending.count) %li{class: ('active' if @scope == 'branches')} = link_to project_pipelines_path(@project, scope: :branches) do Branches - %span.badge.js-running-count - = number_with_delimiter(@all_commits.running_or_pending.count(:id)) %li{class: ('active' if @scope == 'tags')} = link_to project_pipelines_path(@project, scope: :tags) do Tags - %span.badge.js-running-count - = number_with_delimiter(@all_commits.running_or_pending.count(:id)) - - %li{class: ('active' if @scope == 'running')} - = link_to project_pipelines_path(@project, scope: :running) do - Failed - %span.badge.js-running-count - = number_with_delimiter(@all_commits.running_or_pending.count(:id)) .nav-controls - if can? current_user, :create_pipeline, @project @@ -45,8 +41,8 @@ Pipelines for #{(@scope || 'changes')} on this project %ul.content-list - - stages = @commits.stages - - if @commits.blank? + - stages = @pipelines.stages + - if @pipelines.blank? %li .nothing-here-block No pipelines to show - else @@ -55,11 +51,12 @@ %tbody %th Pipeline ID %th Commit - - @commits.stages.each do |stage| - %th.rotate - = stage.titleize + - @pipelines.stages.each do |stage| + %th + %span.has-tooltip(title="#{stage.titleize}") + = truncate(stage.titleize.pluralize, length: 8) %th %th - = render @commits, commit_sha: true, stage: true, allow_retry: true, stages: stages + = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages - = paginate @commits, theme: 'gitlab' + = paginate @pipelines, theme: 'gitlab' diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml new file mode 100644 index 00000000000..9f33e2ad624 --- /dev/null +++ b/app/views/projects/pipelines/show.html.haml @@ -0,0 +1,3 @@ +- page_title "Pipeline" += render "header_title" += render "projects/commit/ci_commit", ci_commit: @pipeline diff --git a/config/routes.rb b/config/routes.rb index 841b3f26272..6384757835a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -654,7 +654,7 @@ Rails.application.routes.draw do resource :variables, only: [:show, :update] resources :triggers, only: [:index, :create, :destroy] - resources :pipelines, only: [:index, :new, :create] do + resources :pipelines, only: [:index, :new, :create, :show] do member do post :cancel post :retry From 410f2b40f2579b2e6a77591157900ce07512ee36 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 13 Apr 2016 16:39:23 +0200 Subject: [PATCH 0026/1306] Remove unneeded changes --- app/assets/stylesheets/framework/tables.scss | 16 ---------------- app/views/layouts/nav/_project.html.haml | 2 +- app/views/projects/commit/_ci_commit.html.haml | 2 +- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 9d6a6c5b237..75b770ae5a2 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -34,22 +34,6 @@ table { font-weight: normal; font-size: 15px; border-bottom: 1px solid $border-color; - - .rotate { - height: 140px; - white-space: nowrap; - transform: - /* Magic Numbers */ - translate(25px, 51px) - /* 45 is really 360 - 45 */ - rotate(315deg); - width: 30px; - } - - .rotate > div > span { - border-bottom: 1px solid #ccc; - padding: 5px 10px; - } } td { diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index b58d8270230..f4797a85bb7 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -39,7 +39,7 @@ Commits - if project_nav_tab? :builds - = nav_link(controller: %w(ci_commits)) do + = nav_link(controller: %w(pipelines)) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-builds' do = icon('ship fw') %span diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 2ec3c809e1c..cf101acbb53 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -20,7 +20,7 @@ for commit = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" - if ci_commit.duration > 0 - took + in = time_interval_in_words ci_commit.duration - if ci_commit.yaml_errors.present? From c351b9f599aa1af693435738d2c897cc2a954fe7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 13 Apr 2016 16:51:52 +0200 Subject: [PATCH 0027/1306] Improve rendered CI statuses --- app/helpers/ci_status_helper.rb | 27 ++++++++++++------- app/views/projects/commits/_commit.html.haml | 2 +- .../projects/issues/_merge_requests.html.haml | 2 +- .../issues/_related_branches.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- app/views/shared/projects/_project.html.haml | 2 +- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 417050b4132..acc01b008bf 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -38,15 +38,24 @@ module CiStatusHelper icon(icon_name + ' fw') end - def render_ci_status(ci_commit, tooltip_placement: 'auto left') - # TODO: split this method into - # - render_commit_status - # - render_pipeline_status - link_to ci_icon_for_status(ci_commit.status), - ci_status_path(ci_commit), - class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", - title: "Build #{ci_label_for_status(ci_commit.status)}", - data: { toggle: 'tooltip', placement: tooltip_placement } + def render_commit_status(commit, tooltip_placement: 'auto left') + project = commit.project + path = builds_namespace_project_commit_path(project.namespace, project, commit) + render_status_with_link('commit', commit.status, path, tooltip_placement) + end + + def render_pipeline_status(pipeline, tooltip_placement: 'auto left') + project = pipeline.project + path = namespace_project_pipeline_path(project.namespace, project, pipeline) + render_status_with_link('pipeline', pipeline.status, path, tooltip_placement) + end + + def render_status_with_link(type, status, path, tooltip_placement) + link_to ci_icon_for_status(status), + path, + class: "ci-status-link ci-status-icon-#{status.dasherize}", + title: "#{type.titleize}: #{ci_label_for_status(status)}", + data: { toggle: 'tooltip', placement: tooltip_placement } end def no_runners_for_project?(project) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index f7c8647ac0e..b231b584eab 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -17,7 +17,7 @@ .pull-right - if commit.status - = render_ci_status(commit) + = render_commit_status(commit) = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index d6b38b327ff..e953353567e 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -7,7 +7,7 @@ %li %span.merge-request-ci-status - if merge_request.ci_commit - = render_ci_status(merge_request.ci_commit) + = render_pipeline_status(merge_request.ci_commit) - 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 bdfa0c7009e..5f9d2919982 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -8,7 +8,7 @@ - ci_commit = @project.ci_commit(sha, branch) if sha - if ci_commit %span.related-branch-ci-status - = render_ci_status(ci_commit) + = render_pipeline_status(ci_commit) %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 391193eed6c..7bfde8b1c57 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -13,7 +13,7 @@ - if merge_request.ci_commit %li - = render_ci_status(merge_request.ci_commit) + = render_pipeline_status(merge_request.ci_commit) - if merge_request.open? && merge_request.broken? %li diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index ab8b022411d..9ef021747a5 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -17,7 +17,7 @@ = project.main_language - if project.commit.try(:status) %span - = render_ci_status(project.commit) + = render_commit_status(project.commit) - if forks %span = icon('code-fork') From cb6f035141d2e7792d9594e5d664d1a305b728cf Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 13 Apr 2016 17:05:17 +0200 Subject: [PATCH 0028/1306] Improve pipeline view --- app/controllers/projects/pipelines_controller.rb | 5 +++++ app/views/projects/commit/_ci_commit.html.haml | 3 +-- app/views/projects/commit/_commit_box.html.haml | 11 +++++++++-- app/views/projects/pipelines/show.html.haml | 6 ++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index b2ee5573bfc..aba64e4a730 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,5 +1,6 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :pipeline, except: [:index, :new, :create] + before_action :commit, only: [:show] before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] @@ -85,4 +86,8 @@ class Projects::PipelinesController < Projects::ApplicationController def pipeline @pipeline ||= project.ci_commits.find_by!(id: params[:id]) end + + def commit + @commit ||= @pipeline.commit_data + end end diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index cf101acbb53..782ea341daf 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -14,8 +14,7 @@ = pluralize ci_commit.statuses.count(:id), "build" - if ci_commit.ref for - %span.label.label-info - = ci_commit.ref + = link_to ci_commit.ref, namespace_project_commits_path(@project.namespace, @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(@project.namespace, @project, ci_commit.sha), class: "monospace" diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 0908e830f83..9cb14b6a90f 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,6 +1,6 @@ .pull-right %div - - if @notes_count > 0 + - if defined?(@notes_count) && @notes_count > 0 %span.btn.disabled.btn-grouped %i.fa.fa-comment = @notes_count @@ -42,7 +42,14 @@ - @commit.parents.each do |parent| = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" -- if @commit.status +- if defined?(pipeline) && pipeline + .pull-right + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline), class: "ci-status ci-#{pipeline.status}" do + = ci_icon_for_status(pipeline.status) + pipeline: + = ci_label_for_status(pipeline.status) + +- elsif @commit.status .pull-right = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do = ci_icon_for_status(@commit.status) diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 9f33e2ad624..8a2e14d8d87 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -1,3 +1,9 @@ - page_title "Pipeline" + = render "header_title" +.prepend-top-default + - if @commit + = render "projects/commit/commit_box", pipeline: @pipeline + %div.block-connector + = render "projects/commit/ci_commit", ci_commit: @pipeline From 21136baa77369d5990ef5db4af26d688aedc8320 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 13 Apr 2016 20:51:03 +0200 Subject: [PATCH 0029/1306] Update handling of skipped status --- app/models/ci/build.rb | 2 +- app/models/ci/commit.rb | 23 ++++++++--------------- app/models/concerns/ci_status.rb | 10 +++++++++- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 085ecc6951c..c0b334d3600 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -116,7 +116,7 @@ module Ci end def retried? - !self.commit.latest.include?(self) + !self.commit.statuses.latest.include?(self) end def retry diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index e2bf4d62541..00a95dd05be 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -125,16 +125,12 @@ module Ci end end - def latest - statuses.latest - end - def retried @retried ||= (statuses.order(id: :desc) - statuses.latest) end def coverage - coverage_array = latest.map(&:coverage).compact + coverage_array = statuses.latest.map(&:coverage).compact if coverage_array.size >= 1 '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) end @@ -169,18 +165,15 @@ module Ci private def update_state - reload - self.status = if yaml_errors.present? - 'failed' + statuses.reload + self.status = if yaml_errors.blank? + statuses.latest.status || 'skipped' else - latest.status + 'failed' end - self.started_at = statuses.minimum(:started_at) - self.finished_at = statuses.maximum(:finished_at) - self.duration = begin - duration_array = latest.map(&:duration).compact - duration_array.reduce(:+).to_i - end + self.started_at = statuses.started_at + self.finished_at = statuses.finished_at + self.duration = statuses.latest.duration save end diff --git a/app/models/concerns/ci_status.rb b/app/models/concerns/ci_status.rb index fd86d2f7553..8190b2a20c6 100644 --- a/app/models/concerns/ci_status.rb +++ b/app/models/concerns/ci_status.rb @@ -15,7 +15,7 @@ module CiStatus skipped = all.skipped.select('count(*)').to_sql deduce_status = "(CASE - WHEN (#{builds})=0 THEN 'skipped' + WHEN (#{builds})=0 THEN NULL WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success' WHEN (#{builds})=(#{pending}) THEN 'pending' WHEN (#{builds})=(#{canceled}) THEN 'canceled' @@ -35,6 +35,14 @@ module CiStatus duration_array = all.map(&:duration).compact duration_array.reduce(:+).to_i end + + def started_at + all.minimum(:started_at) + end + + def finished_at + all.minimum(:finished_at) + end end included do From e7cea8cd75aa23ad4eb9705ddb0871775d65309b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 15 Apr 2016 11:22:08 +0200 Subject: [PATCH 0030/1306] 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 275e94d39ed..817727d7868 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 0031/1306] 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 11e17510cb9..13af17083bd 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 03e5873ae52f3c8c0efb7baa7d1a358a7c3e7974 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 19 Apr 2016 19:04:58 +0300 Subject: [PATCH 0032/1306] Fix broken links [ci skip] --- doc/ci/{ => examples}/deployment/README.md | 0 doc/ci/triggers/README.md | 2 +- doc/markdown/markdown.md | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) rename doc/ci/{ => examples}/deployment/README.md (100%) diff --git a/doc/ci/deployment/README.md b/doc/ci/examples/deployment/README.md similarity index 100% rename from doc/ci/deployment/README.md rename to doc/ci/examples/deployment/README.md diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 9f7c1bfe6a0..1848f6319d8 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -33,7 +33,7 @@ POST /projects/:id/trigger/builds The required parameters are the trigger's `token` and the Git `ref` on which the trigger will be performed. Valid refs are the branch, the tag or the commit -SHA. The `:id` of a project can be found by [querying the API](../api/projects.md) +SHA. The `:id` of a project can be found by [querying the API](../../api/projects.md) or by visiting the **Triggers** page which provides self-explanatory examples. When a rebuild is triggered, the information is exposed in GitLab's UI under diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 4f199b6af6f..3f44a1b4c6c 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -402,7 +402,7 @@ There are two ways to create links, inline-style and reference-style. [I'm a reference-style link][Arbitrary case-insensitive reference text] -[I'm a relative reference to a repository file](LICENSE) +[I'm a relative reference to a repository file](LICENSE)[^1] [You can use numbers for reference-style link definitions][1] @@ -594,3 +594,4 @@ By including colons in the header row, you can align the text within that column [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" +[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com From 077f9a4eeef3c64c5f3e9cc5df5442c8817ee1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Wed, 30 Mar 2016 23:12:34 -0300 Subject: [PATCH 0033/1306] Implementing special GitLab markdown reference for milestones Using the syntax proposed in #13829 [project_reference]%(milestone_id | milestone_name) to get a link to the referred milestone. --- app/models/milestone.rb | 42 +++++++++++++++---- .../filter/milestone_reference_filter.rb | 26 +++++++++++- spec/fixtures/markdown.md.erb | 9 ++-- spec/support/markdown_feature.rb | 6 ++- spec/support/matchers/markdown_matchers.rb | 2 +- 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 986184dd301..39dc8d89614 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -74,8 +74,22 @@ class Milestone < ActiveRecord::Base end end + def self.reference_prefix + '%' + end + def self.reference_pattern - nil + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)} + (?: + (?\d+) | # Integer-based milestone ID, or + (? + [A-Za-z0-9_-]+ | # String-based single-word milestone title, or + "[^"]+" # String-based multi-word milestone surrounded in quotes + ) + ) + }x end def self.link_reference_pattern @@ -86,13 +100,15 @@ class Milestone < ActiveRecord::Base self.where('due_date > ?', Time.now).reorder(due_date: :asc).first end - def to_reference(from_project = nil) - escaped_title = self.title.gsub("]", "\\]") + def to_reference(from_project = nil, format: :id) + format_reference = milestone_format_reference(format) + reference = "#{self.class.reference_prefix}#{format_reference}" - h = Gitlab::Routing.url_helpers - url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) - - "[#{escaped_title}](#{url})" + if cross_project_reference?(from_project) + project.to_reference + reference + else + reference + end end def reference_link_text(from_project = nil) @@ -160,4 +176,16 @@ class Milestone < ActiveRecord::Base issues.where(id: ids). update_all(["position = CASE #{conditions} ELSE position END", *pairs]) end + + private + + def milestone_format_reference(format = :id) + raise StandardError, 'Unknown format' unless [:id, :name].include?(format) + + if format == :name && !name.include?('"') + %("#{name}") + else + id + end + end end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 4cb82178024..2c90fd4d385 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -7,14 +7,36 @@ module Banzai end def find_object(project, id) - project.milestones.find_by(iid: id) + project.milestones.find(id) end - def url_for_object(issue, project) + def references_in(text, pattern = Milestone.reference_pattern) + text.gsub(pattern) do |match| + project = project_from_ref($~[:project]) + params = milestone_params($~[:milestone_id].to_i, $~[:milestone_name]) + milestone = project.milestones.find_by(params) + + if milestone + yield match, milestone.id, $~[:project], $~ + else + match + end + end + end + + def url_for_object(milestone, project) h = Gitlab::Routing.url_helpers h.namespace_project_milestone_url(project.namespace, project, milestone, only_path: context[:only_path]) end + + def milestone_params(id, name) + if name + { name: name.tr('"', '') } + else + { id: id } + end + end end end end diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 1772cc3f6a4..6d3bf810c2c 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -216,10 +216,13 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e #### MilestoneReferenceFilter -- Milestone: <%= milestone.to_reference %> +- Milestone by ID: <%= simple_milestone.to_reference %> +- Milestone by name: <%= Milestone.reference_prefix %><%= simple_milestone.name %> +- Milestone by name in quotes: <%= milestone.to_reference(format: :name) %> - Milestone in another project: <%= xmilestone.to_reference(project) %> -- Ignored in code: `<%= milestone.to_reference %>` -- Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>) +- Ignored in code: `<%= simple_milestone.to_reference %>` +- Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link) +- Link to milestone by URL: [Milestone](<%= milestone.to_reference %>) ### Task Lists diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index b87cd6bbca2..7fc6d6fcc5e 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -63,8 +63,12 @@ class MarkdownFeature @label ||= create(:label, name: 'awaiting feedback', project: project) end + def simple_milestone + @simple_milestone ||= create(:milestone, name: 'gfm-milestone', project: project) + end + def milestone - @milestone ||= create(:milestone, project: project) + @milestone ||= create(:milestone, name: 'next goal', project: project) end # Cross-references ----------------------------------------------------------- diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 43cb6ef43f2..492138716af 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -154,7 +154,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-milestone', count: 3) + expect(actual).to have_selector('a.gfm.gfm-milestone', count: 5) end end From 375e83bb57dc0143691cf6ef7277bec494f060f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 31 Mar 2016 21:54:00 -0300 Subject: [PATCH 0034/1306] Consistently using iid when treating milestones as referrables Also, addint a suffix to the reference text when the milestone is in another project --- app/models/milestone.rb | 25 ++- .../filter/milestone_reference_filter.rb | 19 ++- .../filter/milestone_reference_filter_spec.rb | 159 +++++++++++++++--- 3 files changed, 164 insertions(+), 39 deletions(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 39dc8d89614..50fa95d4d4b 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -83,10 +83,10 @@ class Milestone < ActiveRecord::Base (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: - (?\d+) | # Integer-based milestone ID, or + (?\d+) | # Integer-based milestone iid, or (? - [A-Za-z0-9_-]+ | # String-based single-word milestone title, or - "[^"]+" # String-based multi-word milestone surrounded in quotes + [A-Za-z0-9_-]+ | # String-based single-word milestone title, or + "[^"]+" # String-based multi-word milestone surrounded in quotes ) ) }x @@ -100,7 +100,18 @@ class Milestone < ActiveRecord::Base self.where('due_date > ?', Time.now).reorder(due_date: :asc).first end - def to_reference(from_project = nil, format: :id) + ## + # Returns the String necessary to reference this Milestone in Markdown + # + # format - Symbol format to use (default: :iid, optional: :name) + # + # Examples: + # + # Milestone.first.to_reference # => "%1" + # Milestone.first.to_reference(format: :name) # => "%\"goal\"" + # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1" + # + def to_reference(from_project = nil, format: :iid) format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" @@ -179,13 +190,13 @@ class Milestone < ActiveRecord::Base private - def milestone_format_reference(format = :id) - raise StandardError, 'Unknown format' unless [:id, :name].include?(format) + def milestone_format_reference(format = :iid) + raise StandardError, 'Unknown format' unless [:iid, :name].include?(format) if format == :name && !name.include?('"') %("#{name}") else - id + iid end end end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 2c90fd4d385..419532717f2 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -7,17 +7,17 @@ module Banzai end def find_object(project, id) - project.milestones.find(id) + project.milestones.find_by(iid: id) end def references_in(text, pattern = Milestone.reference_pattern) text.gsub(pattern) do |match| project = project_from_ref($~[:project]) - params = milestone_params($~[:milestone_id].to_i, $~[:milestone_name]) + params = milestone_params($~[:milestone_iid].to_i, $~[:milestone_name]) milestone = project.milestones.find_by(params) if milestone - yield match, milestone.id, $~[:project], $~ + yield match, milestone.iid, $~[:project], $~ else match end @@ -30,11 +30,20 @@ module Banzai only_path: context[:only_path]) end - def milestone_params(id, name) + def object_link_text(object, matches) + if context[:project] == object.project + super + else + "#{super} in #{escape_once(object.project.name_with_namespace)}". + html_safe + end + end + + def milestone_params(iid, name) if name { name: name.tr('"', '') } else - { id: id } + { iid: iid } end end end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index ebf3d7489b5..26f87286b2c 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe Banzai::Filter::MilestoneReferenceFilter, lib: true do include FilterSpecHelper - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let(:reference) { milestone.to_reference } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -17,10 +18,111 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end end - context 'internal reference' do - # Convert the Markdown link to only the URL, since these tests aren't run through the regular Markdown pipeline. - # Milestone reference behavior in the full Markdown pipeline is tested elsewhere. - let(:reference) { milestone.to_reference.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '\2') } + it 'includes default classes' do + doc = reference_filter("Milestone #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Milestone #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-milestone attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-milestone') + expect(link.attr('data-milestone')).to eq milestone.id.to_s + end + + it 'supports an :only_path context' do + doc = reference_filter("Milestone #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls. + namespace_project_milestone_path(project.namespace, project, milestone) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Milestone #{reference}") + expect(result[:references][:milestone]).to eq [milestone] + end + + context 'Integer-based references' do + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) + end + + it 'ignores invalid milestone IIDs' do + exp = act = "Milestone #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'String-based single-word references' do + let(:milestone) { create(:milestone, name: 'gfm', project: project) } + let(:reference) { "#{Milestone.reference_prefix}#{milestone.name}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + expect(doc.text).to eq 'See gfm' + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) + end + + it 'ignores invalid milestone names' do + exp = act = "Milestone #{Milestone.reference_prefix}#{milestone.name.reverse}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'String-based multi-word references in quotes' do + let(:milestone) { create(:milestone, name: 'gfm references', project: project) } + let(:reference) { milestone.to_reference(format: :name) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) + end + + it 'ignores invalid milestone names' do + exp = act = %(Milestone #{Milestone.reference_prefix}"#{milestone.name.reverse}") + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'referencing a milestone in a link href' do + let(:reference) { %Q{Milestone} } it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -30,29 +132,12 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end it 'links with adjacent text' do - doc = reference_filter("milestone (#{reference}.)") - expect(doc.to_html).to match(/\(#{Regexp.escape(milestone.title)}<\/a>\.\)/) - end - - it 'includes a title attribute' do - doc = reference_filter("milestone #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Milestone: #{milestone.title}" - end - - it 'escapes the title attribute' do - milestone.update_attribute(:title, %{">whateverMilestone\.\))) end it 'includes a data-project attribute' do - doc = reference_filter("milestone #{reference}") + doc = reference_filter("Milestone #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -68,8 +153,28 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end it 'adds to the results hash' do - result = reference_pipeline_result("milestone #{reference}") + result = reference_pipeline_result("Milestone #{reference}") expect(result[:references][:milestone]).to eq [milestone] end end + + describe 'cross project milestone references' do + let(:another_project) { create(:empty_project, :public) } + let(:project_name) { another_project.name_with_namespace } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { milestone.to_reference(project) } + + let!(:result) { reference_filter("See #{reference}") } + + it 'points to referenced project milestone page' do + expect(result.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(another_project.namespace, + another_project, + milestone) + end + + it 'contains cross project content' do + expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_name}" + end + end end From 6d9794d42a7bea1150374c76fd3ce5521a44e58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Mon, 4 Apr 2016 22:20:10 -0300 Subject: [PATCH 0035/1306] Transforming milestones link references to the short reference form --- lib/banzai/filter/milestone_reference_filter.rb | 5 +++++ spec/fixtures/markdown.md.erb | 1 + spec/support/matchers/markdown_matchers.rb | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 419532717f2..556087c4880 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -11,6 +11,11 @@ module Banzai end def references_in(text, pattern = Milestone.reference_pattern) + # We'll handle here the references that follow the `reference_pattern`. + # Other patterns (for example, the link pattern) are handled by the + # default implementation. + return super(text, pattern) if pattern != Milestone.reference_pattern + text.gsub(pattern) do |match| project = project_from_ref($~[:project]) params = milestone_params($~[:milestone_iid].to_i, $~[:milestone_name]) diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 6d3bf810c2c..3e777a5e92b 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -222,6 +222,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Milestone in another project: <%= xmilestone.to_reference(project) %> - Ignored in code: `<%= simple_milestone.to_reference %>` - Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link) +- Milestone by URL: <%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %> - Link to milestone by URL: [Milestone](<%= milestone.to_reference %>) ### Task Lists diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 492138716af..d921f9bb2bc 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -154,7 +154,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-milestone', count: 5) + expect(actual).to have_selector('a.gfm.gfm-milestone', count: 6) end end From 1ff896f2bf5d06d0d772fd0df98bf43edf107373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Mon, 4 Apr 2016 23:09:44 -0300 Subject: [PATCH 0036/1306] Escaping the `object_link_text` on cross project milestone references --- lib/banzai/filter/milestone_reference_filter.rb | 2 +- spec/lib/banzai/filter/milestone_reference_filter_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 556087c4880..aea1abf3b8e 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -39,7 +39,7 @@ module Banzai if context[:project] == object.project super else - "#{super} in #{escape_once(object.project.name_with_namespace)}". + "#{escape_once(super)} in #{escape_once(object.project.name_with_namespace)}". html_safe end end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 26f87286b2c..ac3e6e4e536 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -176,5 +176,11 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'contains cross project content' do expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_name}" end + + it 'escapes the name attribute' do + allow_any_instance_of(Milestone).to receive(:title).and_return(%{">whatever Date: Mon, 4 Apr 2016 23:37:50 -0300 Subject: [PATCH 0037/1306] Implementing autocomplete for GFM milestone references --- .../javascripts/gfm_auto_complete.js.coffee | 19 +++++++++++++++++++ app/controllers/projects_controller.rb | 1 + app/services/projects/autocomplete_service.rb | 4 ++++ 3 files changed, 24 insertions(+) diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 61e3f811e73..54d89ef69a1 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -18,6 +18,10 @@ GitLab.GfmAutoComplete = Issues: template: '
  • ${id} ${title}
  • ' + # Milestones + Milestones: + template: '
  • ${title}
  • ' + # Add GFM auto-completion to all input fields, that accept GFM input. setup: (wrap) -> @input = $('.js-gfm-input') @@ -81,6 +85,19 @@ GitLab.GfmAutoComplete = title: sanitize(i.title) search: "#{i.iid} #{i.title}" + @input.atwho + at: '%' + alias: 'milestones' + searchKey: 'search' + displayTpl: @Milestones.template + insertTpl: '${atwho-at}${id}' + callbacks: + beforeSave: (milestones) -> + $.map milestones, (m) -> + id: m.iid + title: sanitize(m.title) + search: "#{m.title}" + @input.atwho at: '!' alias: 'mergerequests' @@ -105,6 +122,8 @@ GitLab.GfmAutoComplete = @input.atwho 'load', '@', data.members # load issues @input.atwho 'load', 'issues', data.issues + # load milestones + @input.atwho 'load', 'milestones', data.milestones # load merge requests @input.atwho 'load', 'mergerequests', data.mergerequests # load emojis diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3768efe142a..8662de712a1 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -147,6 +147,7 @@ class ProjectsController < Projects::ApplicationController @suggestions = { emojis: AwardEmoji.urls, issues: autocomplete.issues, + milestones: autocomplete.milestones, mergerequests: autocomplete.merge_requests, members: participants } diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index ba50305dbd5..eec38c5c3d8 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -4,6 +4,10 @@ module Projects @project.issues.visible_to_user(current_user).opened.select([:iid, :title]) end + def milestones + @project.milestones.active.select([:iid, :title]) + end + def merge_requests @project.merge_requests.opened.select([:iid, :title]) end From 0ba116a58ed57d760b264c39f241798528f54b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Tue, 5 Apr 2016 21:35:43 -0300 Subject: [PATCH 0038/1306] Matching version-like expressions as `milestone_name`s instead of `milestone_iid`s The changes also account for %2.1. being matched as milestone_name = "2.1" without the word-separating dot. --- app/models/milestone.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 50fa95d4d4b..92c07fd20da 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -79,14 +79,19 @@ class Milestone < ActiveRecord::Base end def self.reference_pattern + # NOTE: The iid pattern only matches when all characters on the expression + # are digits, so it will match %2 but not %2.1 because that's probably a + # milestone name and we want it to be matched as such. %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: - (?\d+) | # Integer-based milestone iid, or + (? + \d+(?!\S\w)\b # Integer-based milestone iid, or + ) | (? - [A-Za-z0-9_-]+ | # String-based single-word milestone title, or - "[^"]+" # String-based multi-word milestone surrounded in quotes + [^"\s]+\b | # String-based single-word milestone title, or + "[^"]+" # String-based multi-word milestone surrounded in quotes ) ) }x From 4596190a2d1eb6d575b52ff889a20c49fbd8ca2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Tue, 5 Apr 2016 21:40:40 -0300 Subject: [PATCH 0039/1306] Inserting Milestone titles insted of IIDs with GFM auto complete --- 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 54d89ef69a1..0f2c5a6241c 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -90,7 +90,7 @@ GitLab.GfmAutoComplete = alias: 'milestones' searchKey: 'search' displayTpl: @Milestones.template - insertTpl: '${atwho-at}${id}' + insertTpl: '${atwho-at}${title}' callbacks: beforeSave: (milestones) -> $.map milestones, (m) -> From ec71edfeddc403df5dcff1300e3f4868554c5f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Tue, 5 Apr 2016 21:43:26 -0300 Subject: [PATCH 0040/1306] Sorting Milestones on the auto complete list by due date and title --- app/services/projects/autocomplete_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index eec38c5c3d8..eb73948006e 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -5,7 +5,7 @@ module Projects end def milestones - @project.milestones.active.select([:iid, :title]) + @project.milestones.active.reorder(due_date: :asc, title: :asc).select([:iid, :title]) end def merge_requests From 0f925714d04a4d2e86db3a752fc8c1fc45da2214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Wed, 6 Apr 2016 20:35:02 -0300 Subject: [PATCH 0041/1306] Inserting the Milestone title between quotes on GFM auto complete This is due to the fact that for multiple word titles it might be an invalid reference without the quotes --- 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 0f2c5a6241c..41dba342107 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -90,7 +90,7 @@ GitLab.GfmAutoComplete = alias: 'milestones' searchKey: 'search' displayTpl: @Milestones.template - insertTpl: '${atwho-at}${title}' + insertTpl: '${atwho-at}"${title}"' callbacks: beforeSave: (milestones) -> $.map milestones, (m) -> From 30d1d47d1da729319a3e71bd5599c473fc926565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Wed, 6 Apr 2016 21:37:56 -0300 Subject: [PATCH 0042/1306] Using project `path_with_namespace` in milestone's cross project references link text --- lib/banzai/filter/milestone_reference_filter.rb | 2 +- spec/lib/banzai/filter/milestone_reference_filter_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index aea1abf3b8e..746e768061c 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -39,7 +39,7 @@ module Banzai if context[:project] == object.project super else - "#{escape_once(super)} in #{escape_once(object.project.name_with_namespace)}". + "#{escape_once(super)} in #{escape_once(object.project.path_with_namespace)}". html_safe end end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index ac3e6e4e536..bdf48eabb0e 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -160,7 +160,7 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do describe 'cross project milestone references' do let(:another_project) { create(:empty_project, :public) } - let(:project_name) { another_project.name_with_namespace } + let(:project_path) { another_project.path_with_namespace } let(:milestone) { create(:milestone, project: another_project) } let(:reference) { milestone.to_reference(project) } @@ -174,13 +174,13 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_name}" + expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}" end it 'escapes the name attribute' do allow_any_instance_of(Milestone).to receive(:title).and_return(%{">
    whatever Date: Fri, 8 Apr 2016 23:03:23 -0300 Subject: [PATCH 0043/1306] Include Milestone reference syntax in Markdown documentation --- doc/markdown/markdown.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 4f199b6af6f..1afa1f14067 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -185,20 +185,23 @@ GFM will turn that reference into a link so you can navigate between them easily GFM will recognize the following: -| input | references | -|:-----------------------|:---------------------------| -| `@user_name` | specific user | -| `@group_name` | specific group | -| `@all` | entire team | -| `#123` | issue | -| `!123` | merge request | -| `$123` | snippet | -| `~123` | label by ID | -| `~bug` | one-word label by name | -| `~"feature request"` | multi-word label by name | -| `9ba12248` | specific commit | -| `9ba12248...b19a04f5` | commit range comparison | -| `[README](doc/README)` | repository file references | +| input | references | +|:-----------------------|:--------------------------- | +| `@user_name` | specific user | +| `@group_name` | specific group | +| `@all` | entire team | +| `#123` | issue | +| `!123` | merge request | +| `$123` | snippet | +| `~123` | label by ID | +| `~bug` | one-word label by name | +| `~"feature request"` | multi-word label by name | +| `%123` | milestone by ID | +| `%v1.23` | one-word milestone by name | +| `%"release candidate"` | multi-word milestone by name | +| `9ba12248` | specific commit | +| `9ba12248...b19a04f5` | commit range comparison | +| `[README](doc/README)` | repository file references | GFM also recognizes certain cross-project references: @@ -206,6 +209,7 @@ GFM also recognizes certain cross-project references: |:----------------------------------------|:------------------------| | `namespace/project#123` | issue | | `namespace/project!123` | merge request | +| `namespace/project%123` | milestone | | `namespace/project$123` | snippet | | `namespace/project@9ba12248` | specific commit | | `namespace/project@9ba12248...b19a04f5` | commit range comparison | From 7910853368970292eb243ee34072c7f527fa67f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Tue, 19 Apr 2016 22:20:43 -0300 Subject: [PATCH 0044/1306] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d4b8a509261..b35cd9585dc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -112,6 +112,7 @@ v 8.7.0 (unreleased) - Fix GitHub project's link in the import page when provider has a custom URL - Add RAW build trace output and button on build page - Add incremental build trace update into CI API + - Implement GFM references for milestones (Alejandro Rodríguez) v 8.6.7 - Fix persistent XSS vulnerability in `commit_person_link` helper From ff1e7474ed0f210df004c714e1b83c1c2eb0d91c Mon Sep 17 00:00:00 2001 From: Andrew Collett Date: Thu, 21 Apr 2016 10:30:27 +0000 Subject: [PATCH 0045/1306] Update cas.md to reflect the current syntax, and added that gitlab-ctl reconfigure should be run. --- doc/integration/cas.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/doc/integration/cas.md b/doc/integration/cas.md index e6b2071f193..e34e306f9ac 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -27,17 +27,18 @@ To enable the CAS OmniAuth provider you must register your application with your ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "cas3", - label: "cas", - args: { - url: 'CAS_SERVER', - login_url: '/CAS_PATH/login', - service_validate_url: '/CAS_PATH/p3/serviceValidate', - logout_url: '/CAS_PATH/logout'} } - } + "name"=> "cas3", + "label"=> "cas", + "args"=> { + "url"=> 'CAS_SERVER', + "login_url"=> '/CAS_PATH/login', + "service_validate_url"=> '/CAS_PATH/p3/serviceValidate', + "logout_url"=> '/CAS_PATH/logout' + } } ] ``` + For installations from source: @@ -57,6 +58,8 @@ To enable the CAS OmniAuth provider you must register your application with your 1. Save the configuration file. +1. Run `gitlab-ctl reconfigure` for the omnibus package. + 1. Restart GitLab for the changes to take effect. On the sign in page there should now be a CAS tab in the sign in form. From 9add3fbb3346460934d5990ede1b3216c03e62ee Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Apr 2016 13:24:53 +0200 Subject: [PATCH 0046/1306] =?UTF-8?q?Some=20changes=20after=20review=20fro?= =?UTF-8?q?m=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 13af17083bd..cd8dd610bcd 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 0047/1306] 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 817727d7868..74150ad606b 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 cd8dd610bcd..e38552218ec 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 0048/1306] 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 8b217684911..20c7357cba5 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 ab1734f9e1e3f07482185c8a4cb168be463fcff5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 23 Apr 2016 12:27:29 +0200 Subject: [PATCH 0049/1306] Move changelog item --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f81435805d3..b1df9145d93 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.0 (unreleased) + - Implement GFM references for milestones (Alejandro Rodríguez) v 8.7.1 (unreleased) - Use the `can?` helper instead of `current_user.can?` @@ -121,7 +122,6 @@ v 8.7.0 - Fix GitHub project's link in the import page when provider has a custom URL - Add RAW build trace output and button on build page - Add incremental build trace update into CI API - - Implement GFM references for milestones (Alejandro Rodríguez) v 8.6.7 - Fix persistent XSS vulnerability in `commit_person_link` helper From 715959e58190eca661ea377b949af3515d8da913 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 23 Apr 2016 12:34:09 +0200 Subject: [PATCH 0050/1306] Fix cross-project milestone ref with invalid project --- .../filter/milestone_reference_filter.rb | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 746e768061c..dad0768f51b 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -17,9 +17,7 @@ module Banzai return super(text, pattern) if pattern != Milestone.reference_pattern text.gsub(pattern) do |match| - project = project_from_ref($~[:project]) - params = milestone_params($~[:milestone_iid].to_i, $~[:milestone_name]) - milestone = project.milestones.find_by(params) + milestone = find_milestone($~[:project], $~[:milestone_iid], $~[:milestone_name]) if milestone yield match, milestone.iid, $~[:project], $~ @@ -29,6 +27,22 @@ module Banzai end end + def find_milestone(project_ref, milestone_id, milestone_name) + project = project_from_ref(project_ref) + return unless project + + milestone_params = milestone_params(milestone_id, milestone_name) + project.milestones.find_by(milestone_params) + end + + def milestone_params(iid, name) + if name + { name: name.tr('"', '') } + else + { iid: iid.to_i } + end + end + def url_for_object(milestone, project) h = Gitlab::Routing.url_helpers h.namespace_project_milestone_url(project.namespace, project, milestone, @@ -43,14 +57,6 @@ module Banzai html_safe end end - - def milestone_params(iid, name) - if name - { name: name.tr('"', '') } - else - { iid: iid } - end - end end 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 0051/1306] =?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 e38552218ec..fafd9e445b5 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 c6f19aed51736e5945283a611eae09f32a9b5aeb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 26 Apr 2016 16:01:00 +0200 Subject: [PATCH 0052/1306] Fix builds rendering bug --- app/views/projects/commit/_ci_commit.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 782ea341daf..21a30080868 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -18,7 +18,7 @@ - if defined?(link_to_commit) && link_to_commit for commit = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" - - if ci_commit.duration > 0 + - if ci_commit.duration in = time_interval_in_words ci_commit.duration From f41a3e24d20b26b53c5321571ef89f441c32aa4d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Apr 2016 08:13:16 -0400 Subject: [PATCH 0053/1306] Added authentication service for docker registry --- Gemfile | 1 + Gemfile.lock | 1 + app/models/ability.rb | 8 +- app/models/ci/build.rb | 1 + app/models/project.rb | 5 + config/initializers/1_settings.rb | 39 ++++ ...07120251_add_images_enabled_for_project.rb | 5 + db/schema.rb | 1 + lib/api/api.rb | 1 + lib/api/auth.rb | 166 ++++++++++++++++++ 10 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20160407120251_add_images_enabled_for_project.rb create mode 100644 lib/api/auth.rb diff --git a/Gemfile b/Gemfile index 7882e467f8d..512c6babd7e 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' gem 'rack-oauth2', '~> 1.2.1' +gem 'jwt' # Spam and anti-bot protection gem 'recaptcha', require: 'recaptcha/rails' diff --git a/Gemfile.lock b/Gemfile.lock index 1dcda0daff6..2b578429b3c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -957,6 +957,7 @@ DEPENDENCIES jquery-scrollto-rails (~> 1.4.3) jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 5.0.0) + jwt kaminari (~> 0.16.3) letter_opener_web (~> 1.3.0) licensee (~> 8.0.0) diff --git a/app/models/ability.rb b/app/models/ability.rb index 6103a2947e2..ba27b9a9b14 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -203,6 +203,7 @@ class Ability :admin_label, :read_commit_status, :read_build, + :read_image, ] end @@ -216,7 +217,9 @@ class Ability :update_build, :create_merge_request, :create_wiki, - :push_code + :push_code, + :create_image, + :update_image, ] end @@ -242,7 +245,8 @@ class Ability :admin_wiki, :admin_project, :admin_commit_status, - :admin_build + :admin_build, + :admin_image ] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 553cd447971..c2ddee527e5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -426,6 +426,7 @@ module Ci variables << { key: :CI_BUILD_NAME, value: name, public: true } variables << { key: :CI_BUILD_STAGE, value: stage, public: true } variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request + variables << { key: :CI_DOCKER_REGISTRY, value: project.registry_repository_url, public: true } if project.registry_repository_url variables end end diff --git a/app/models/project.rb b/app/models/project.rb index 5c6c36e6b31..76265a59ea7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -64,6 +64,7 @@ class Project < ActiveRecord::Base default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets + default_value_for :images_enabled, gitlab_config_features.images default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at @@ -369,6 +370,10 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end + def registry_repository_url + "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" if images_enabled? && Gitlab.config.registry.enabled + end + def commit(id = 'HEAD') repository.commit(id) end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8db2c05fe45..01ee8a0d525 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -27,6 +27,30 @@ class Settings < Settingslogic ].join('') end + def build_registry_api_url + if registry.port.to_i == (registry.https ? 443 : 80) + custom_port = nil + else + custom_port = ":#{registry.port}" + end + [ registry.protocol, + "://", + registry.internal_host, + custom_port + ].join('') + end + + def build_registry_host_with_port + if registry.port.to_i == (registry.https ? 443 : 80) + custom_port = nil + else + custom_port = ":#{registry.port}" + end + [ registry.host, + custom_port + ].join('') + end + def build_gitlab_shell_ssh_path_prefix user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" @@ -211,6 +235,7 @@ Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.g Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? +Settings.gitlab.default_projects_features['images'] = true if Settings.gitlab.default_projects_features['images'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] @@ -242,6 +267,20 @@ Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) Settings.artifacts['max_size'] ||= 100 # in megabytes +# +# Registry +# +Settings['registry'] ||= Settingslogic.new({}) +Settings.registry['registry'] = false if Settings.registry['enabled'].nil? +Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) +Settings.registry['host'] ||= "example.com" +Settings.registry['internal_host']||= "localhost" +Settings.registry['https'] = false if Settings.registry['https'].nil? +Settings.registry['port'] ||= Settings.registry.https ? 443 : 80 +Settings.registry['protocol'] ||= Settings.registry.https ? "https" : "http" +Settings.registry['api_url'] ||= Settings.send(:build_registry_api_url) +Settings.registry['host_port'] ||= Settings.send(:build_registry_host_with_port) + # # Git LFS # diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb new file mode 100644 index 00000000000..6a221a7fb03 --- /dev/null +++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb @@ -0,0 +1,5 @@ +class AddImagesEnabledForProject < ActiveRecord::Migration + def change + add_column :projects, :images_enabled, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 42457d92353..bf46028d23f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -760,6 +760,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" + t.boolean "images_enabled" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree diff --git a/lib/api/api.rb b/lib/api/api.rb index cc1004f8005..6ddfe11d98e 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -58,5 +58,6 @@ module API mount Variables mount Runners mount Licenses + mount Auth end end diff --git a/lib/api/auth.rb b/lib/api/auth.rb new file mode 100644 index 00000000000..b992e497307 --- /dev/null +++ b/lib/api/auth.rb @@ -0,0 +1,166 @@ +module API + # Projects builds API + class Auth < Grape::API + namespace 'auth' do + get 'token' do + required_attributes! [:scope, :service] + keys = attributes_for_keys [:scope, :service] + + case keys[:service] + when 'docker' + docker_token_auth(keys[:scope]) + else + not_found! + end + end + end + + helpers do + def docker_token_auth(scope) + @type, @path, actions = scope.split(':', 3) + bad_request!("invalid type: #{type}") unless type == 'repository' + + @actions = actions.split(',') + bad_request!('missing actions') if @actions.empty? + + @project = Project.find_with_namespace(path) + not_found!('Project') unless @project + + auth! + + authorize_actions!(@actions) + + { token: encode(docker_payload) } + end + + def auth! + auth = BasicRequest.new(request.env) + return unless auth.provided? + + return bad_request unless auth.basic? + + # Authentication with username and password + login, password = auth.credentials + + if ci_request?(login, password) + @ci = true + return + end + + @user = authenticate_user(login, password) + + if @user + request.env['REMOTE_USER'] = @auth.username + end + end + + def ci_request?(login, password) + matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) + + if @project && matched_login.present? + underscored_service = matched_login['s'].underscore + + if underscored_service == 'gitlab_ci' + return @project.valid_build_token?(password) + end + end + + false + end + + def authenticate_user(login, password) + user = Gitlab::Auth.new.find(login, password) + + unless user + user = oauth_access_token_check(login, password) + end + + # If the user authenticated successfully, we reset the auth failure count + # from Rack::Attack for that IP. A client may attempt to authenticate + # with a username and blank password first, and only after it receives + # a 401 error does it present a password. Resetting the count prevents + # false positives from occurring. + # + # Otherwise, we let Rack::Attack know there was a failed authentication + # attempt from this IP. This information is stored in the Rails cache + # (Redis) and will be used by the Rack::Attack middleware to decide + # whether to block requests from this IP. + config = Gitlab.config.rack_attack.git_basic_auth + + if config.enabled + if user + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(@request.ip, config) + else + banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(@request.ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{@request.ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end + end + + user + end + + def docker_payload + { + access: [ + type: @type, + name: @path, + actions: @actions + ], + exp: Time.now.to_i + 3600 + } + end + + def private_key + @private_key ||= OpenSSL::PKey::RSA.new File.read 'config/registry.key' + end + + def encode(payload) + JWT.encode(payload, private_key, 'RS256') + end + + def authorize_actions!(actions) + actions.each do |action| + forbidden! unless can_access?(action) + end + end + + def can_access?(action) + case action + when 'pull' + @ci || can?(@user, :download_code, @project) + when 'push' + @ci || can?(@user, :push_code, @project) + else + false + end + end + + class BasicRequest < Rack::Auth::AbstractRequest + def basic? + "basic" == scheme + end + + def credentials + @credentials ||= params.unpack("m*").first.split(/:/, 2) + end + + def username + credentials.first + end + end + end + end +end From 03b3fe13f6af67f8117cf4322b605630f55f3136 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Apr 2016 08:23:17 -0400 Subject: [PATCH 0054/1306] Make images_enabled configurable --- app/controllers/projects_controller.rb | 3 ++- app/views/projects/edit.html.haml | 10 ++++++++++ lib/api/entities.rb | 3 ++- lib/api/projects.rb | 5 +++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3768efe142a..52f7b993343 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -235,7 +235,8 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, + :issues_enabled, :merge_requests_enabled, :snippets_enabled, :images_enabled, + :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :public_builds, diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 76a4f41193c..5c7960031e0 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -84,6 +84,16 @@ %br %span.descr Share code pastes with others out of git repository + - if Gitlab.config.registry.enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :images_enabled do + = f.check_box :images_enabled + %strong Images + %br + %span.descr Use Docker Registry for this repository + = render 'builds_settings', f: f %fieldset.features diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 716ca6f7ed9..95c3597b03c 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -66,7 +66,8 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :images_enabled + expose :created_at, :last_activity_at expose :shared_runners_enabled expose :creator_id expose :namespace diff --git a/lib/api/projects.rb b/lib/api/projects.rb index cc2c7a0c503..6f85bc4b1b9 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -94,6 +94,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # images_enabled (optional) # shared_runners_enabled (optional) # namespace_id (optional) - defaults to user namespace # public (optional) - if true same as setting visibility_level = 20 @@ -112,6 +113,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :images_enabled, :shared_runners_enabled, :namespace_id, :public, @@ -143,6 +145,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # images_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) @@ -206,6 +209,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # images_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project @@ -222,6 +226,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :images_enabled, :shared_runners_enabled, :public, :visibility_level, From 0ca8db25f008cd3bc4f2df0f58efd739718323d0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Apr 2016 10:55:10 -0400 Subject: [PATCH 0055/1306] Try to fix auth service --- lib/api/auth.rb | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/api/auth.rb b/lib/api/auth.rb index b992e497307..ec944b1dc8c 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -3,12 +3,12 @@ module API class Auth < Grape::API namespace 'auth' do get 'token' do - required_attributes! [:scope, :service] - keys = attributes_for_keys [:scope, :service] + required_attributes! [:service] + keys = attributes_for_keys [:offline_token, :scope, :service] case keys[:service] when 'docker' - docker_token_auth(keys[:scope]) + docker_token_auth(keys[:scope], keys[:offline_token]) else not_found! end @@ -16,19 +16,23 @@ module API end helpers do - def docker_token_auth(scope) - @type, @path, actions = scope.split(':', 3) - bad_request!("invalid type: #{type}") unless type == 'repository' - - @actions = actions.split(',') - bad_request!('missing actions') if @actions.empty? - - @project = Project.find_with_namespace(path) - not_found!('Project') unless @project - + def docker_token_auth(scope, offline_token) auth! - authorize_actions!(@actions) + if offline_token + forbidden! unless @user + elsif scope + @type, @path, actions = scope.split(':', 3) + bad_request!("invalid type: #{@type}") unless @type == 'repository' + + @actions = actions.split(',') + bad_request!('missing actions') if @actions.empty? + + @project = Project.find_with_namespace(@path) + not_found!('Project') unless @project + + authorize_actions!(@actions) + end { token: encode(docker_payload) } end @@ -50,7 +54,7 @@ module API @user = authenticate_user(login, password) if @user - request.env['REMOTE_USER'] = @auth.username + request.env['REMOTE_USER'] = @user.username end end @@ -71,10 +75,6 @@ module API def authenticate_user(login, password) user = Gitlab::Auth.new.find(login, password) - unless user - user = oauth_access_token_check(login, password) - end - # If the user authenticated successfully, we reset the auth failure count # from Rack::Attack for that IP. A client may attempt to authenticate # with a username and blank password first, and only after it receives From 72611f9cfa9014653c0894115af6223687c2eab4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Apr 2016 13:57:35 -0400 Subject: [PATCH 0056/1306] Auth token --- config/gitlab.yml.example | 10 ++++++++++ config/initializers/1_settings.rb | 1 + lib/api/auth.rb | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 07ce4b6d715..e55ca6f9c6b 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -176,6 +176,16 @@ production: &base repository_archive_cache_worker: cron: "0 * * * *" + registry: + # enabled: true + # host: localhost + # port: 5000 + # https: false + # internal_host: localhost + # key: config/registry.key + # issuer: omnibus-certificate + # path: shared/registry + # # 2. GitLab CI settings # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 01ee8a0d525..b94f3f2f901 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -275,6 +275,7 @@ Settings.registry['registry'] = false if Settings.registry['enabled'].nil? Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) Settings.registry['host'] ||= "example.com" Settings.registry['internal_host']||= "localhost" +Settings.registry['key'] ||= nil Settings.registry['https'] = false if Settings.registry['https'].nil? Settings.registry['port'] ||= Settings.registry.https ? 443 : 80 Settings.registry['protocol'] ||= Settings.registry.https ? "https" : "http" diff --git a/lib/api/auth.rb b/lib/api/auth.rb index ec944b1dc8c..d769c692754 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -119,12 +119,13 @@ module API name: @path, actions: @actions ], + iss: Gitlab.config.registry.issuer, exp: Time.now.to_i + 3600 } end def private_key - @private_key ||= OpenSSL::PKey::RSA.new File.read 'config/registry.key' + @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key end def encode(payload) From 8aac802eaf417a4f484f099089410934cdfdb0b7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Apr 2016 14:16:17 -0400 Subject: [PATCH 0057/1306] Audience --- lib/api/auth.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/api/auth.rb b/lib/api/auth.rb index d769c692754..e4ce9bf122d 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -120,6 +120,7 @@ module API actions: @actions ], iss: Gitlab.config.registry.issuer, + aud: "docker", exp: Time.now.to_i + 3600 } end From 5fc310b440a7bb3ead91760ac2b7cbb1cee72f2a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 21 Apr 2016 10:02:24 +0200 Subject: [PATCH 0058/1306] Missing parameters of docker payload --- lib/api/auth.rb | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/api/auth.rb b/lib/api/auth.rb index e4ce9bf122d..dab04bca818 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -113,6 +113,7 @@ module API end def docker_payload + issued_at = Time.now { access: [ type: @type, @@ -121,8 +122,14 @@ module API ], iss: Gitlab.config.registry.issuer, aud: "docker", + sub: @user.try(:username), + aud: @service, + iat: issued_at, + nbf: issued_at - 5.seconds, + exp: issued_at + 60.minutes, + jti: SecureRandom.uuid, exp: Time.now.to_i + 3600 - } + }.compact end def private_key @@ -130,7 +137,10 @@ module API end def encode(payload) - JWT.encode(payload, private_key, 'RS256') + headers = { + kid: kid(private_key) + } + JWT.encode(payload, private_key, 'RS256', headers) end def authorize_actions!(actions) @@ -150,6 +160,15 @@ module API end end + def kid(private_key) + sha256 = Digest::SHA256.new + sha256.update(private_key.public_key.to_der) + payload = StringIO.new(sha256.digest).read(30) + Base32.encode(payload).split("").each_slice(4).each_with_object([]) do |slice, mem| + mem << slice.join + end.join(":") + end + class BasicRequest < Rack::Auth::AbstractRequest def basic? "basic" == scheme From 0a280158efeb7f681589ae7af24f0ed9052de809 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 15 Apr 2016 19:23:33 +0530 Subject: [PATCH 0059/1306] Eager load `lib/api` - So that the server doesn't have to be restarted for every change in dev. --- config/application.rb | 2 + config/routes.rb | 1 - lib/api/api.rb | 4 +- lib/api/api_guard.rb | 316 +++++++++++++++++++++--------------------- lib/ci/api/api.rb | 2 +- 5 files changed, 163 insertions(+), 162 deletions(-) diff --git a/config/application.rb b/config/application.rb index 2e2ed48db07..abe22691ad1 100644 --- a/config/application.rb +++ b/config/application.rb @@ -79,6 +79,8 @@ module Gitlab # This is needed for gitlab-shell ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH'] + config.eager_load_paths += ["#{Rails.root}/lib"] + config.generators do |g| g.factory_girl false end diff --git a/config/routes.rb b/config/routes.rb index 5ce1f49ec6a..adf4bb18b3c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ require 'sidekiq/web' require 'sidekiq/cron/web' -require 'api/api' Rails.application.routes.draw do if Gitlab::Sherlock.enabled? diff --git a/lib/api/api.rb b/lib/api/api.rb index 6ddfe11d98e..d41b4b71865 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,8 +1,6 @@ -Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} - module API class API < Grape::API - include APIGuard + include ::API::APIGuard version 'v3', using: :path rescue_from ActiveRecord::RecordNotFound do diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index b9994fcefda..6dfd6e4396b 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -2,171 +2,173 @@ require 'rack/oauth2' -module APIGuard - extend ActiveSupport::Concern +module API + module APIGuard + extend ActiveSupport::Concern - included do |base| - # OAuth2 Resource Server Authentication - use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| - # The authenticator only fetches the raw token string + included do |base| + # OAuth2 Resource Server Authentication + use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| + # The authenticator only fetches the raw token string - # Must yield access token to store it in the env - request.access_token - end - - helpers HelperMethods - - install_error_responders(base) - end - - # Helper Methods for Grape Endpoint - module HelperMethods - # Invokes the doorkeeper guard. - # - # If token is presented and valid, then it sets @current_user. - # - # If the token does not have sufficient scopes to cover the requred scopes, - # then it raises InsufficientScopeError. - # - # If the token is expired, then it raises ExpiredError. - # - # If the token is revoked, then it raises RevokedError. - # - # If the token is not found (nil), then it raises TokenNotFoundError. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def doorkeeper_guard!(scopes: []) - if (access_token = find_access_token).nil? - raise TokenNotFoundError - - else - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end + # Must yield access token to store it in the env + request.access_token end + + helpers HelperMethods + + install_error_responders(base) end - def doorkeeper_guard(scopes: []) - if access_token = find_access_token - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) + # Helper Methods for Grape Endpoint + module HelperMethods + # Invokes the doorkeeper guard. + # + # If token is presented and valid, then it sets @current_user. + # + # If the token does not have sufficient scopes to cover the requred scopes, + # then it raises InsufficientScopeError. + # + # If the token is expired, then it raises ExpiredError. + # + # If the token is revoked, then it raises RevokedError. + # + # If the token is not found (nil), then it raises TokenNotFoundError. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def doorkeeper_guard!(scopes: []) + if (access_token = find_access_token).nil? + raise TokenNotFoundError - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end - end - end - - def current_user - @current_user - end - - private - def find_access_token - @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) - end - - def doorkeeper_request - @doorkeeper_request ||= ActionDispatch::Request.new(env) - end - - def validate_access_token(access_token, scopes) - Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) - end - end - - module ClassMethods - # Installs the doorkeeper guard on the whole Grape API endpoint. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def guard_all!(scopes: []) - before do - guard! scopes: scopes - end - end - - private - def install_error_responders(base) - error_classes = [ MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] - - base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler - end - - def oauth2_bearer_token_error_handler - Proc.new do |e| - response = - case e - when MissingTokenError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new - - when TokenNotFoundError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Bad Access Token.") - - when ExpiredError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token is expired. You can either do re-authorization or token refresh.") - - when RevokedError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token was revoked. You have to re-authorize from the user.") - - when InsufficientScopeError - # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) - # does not include WWW-Authenticate header, which breaks the standard. - Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( - :insufficient_scope, - Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], - { scope: e.scopes }) + else + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) end + end + end - response.finish + def doorkeeper_guard(scopes: []) + if access_token = find_access_token + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end + + def current_user + @current_user + end + + private + def find_access_token + @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + end + + def doorkeeper_request + @doorkeeper_request ||= ActionDispatch::Request.new(env) + end + + def validate_access_token(access_token, scopes) + Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) + end + end + + module ClassMethods + # Installs the doorkeeper guard on the whole Grape API endpoint. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def guard_all!(scopes: []) + before do + guard! scopes: scopes + end + end + + private + def install_error_responders(base) + error_classes = [ MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] + + base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + end + + def oauth2_bearer_token_error_handler + Proc.new do |e| + response = + case e + when MissingTokenError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new + + when TokenNotFoundError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Bad Access Token.") + + when ExpiredError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token is expired. You can either do re-authorization or token refresh.") + + when RevokedError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token was revoked. You have to re-authorize from the user.") + + when InsufficientScopeError + # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) + # does not include WWW-Authenticate header, which breaks the standard. + Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( + :insufficient_scope, + Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], + { scope: e.scopes }) + end + + response.finish + end + end + end + + # + # Exceptions + # + + class MissingTokenError < StandardError; end + + class TokenNotFoundError < StandardError; end + + class ExpiredError < StandardError; end + + class RevokedError < StandardError; end + + class InsufficientScopeError < StandardError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes end end end - - # - # Exceptions - # - - class MissingTokenError < StandardError; end - - class TokenNotFoundError < StandardError; end - - class ExpiredError < StandardError; end - - class RevokedError < StandardError; end - - class InsufficientScopeError < StandardError - attr_reader :scopes - def initialize(scopes) - @scopes = scopes - end - end -end +end \ No newline at end of file diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 353c4ddebf8..7cd8b6fbae2 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -3,7 +3,7 @@ Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file} module Ci module API class API < Grape::API - include APIGuard + include ::API::APIGuard version 'v1', using: :path rescue_from ActiveRecord::RecordNotFound do From 9ef50db6279d722caed1ab1e4576275428e6a94f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 29 Apr 2016 18:56:53 +0200 Subject: [PATCH 0060/1306] 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 20c7357cba5..14d126480a3 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 0061/1306] 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 fafd9e445b5..16a85d6f62b 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 66ac88e9f4a..0c694e0d37a 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 cc646895914..e308ccc3004 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 30509528b8b..32e903905ad 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 e2363b91265..b263a27d4d3 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 aad291c03cd..2c2f7ed0665 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 c0f02aad4a1a178109a235d34bd70218c0aec86c Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Mon, 2 May 2016 16:37:12 +0700 Subject: [PATCH 0062/1306] Add snippet tab under user profile --- app/assets/javascripts/user_tabs.js.coffee | 9 ++++++++- app/controllers/users_controller.rb | 22 ++++++++++++++++++++++ app/views/users/show.html.haml | 6 ++++++ config/routes.rb | 5 +++-- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee index 09b7eec9104..aa798b96ede 100644 --- a/app/assets/javascripts/user_tabs.js.coffee +++ b/app/assets/javascripts/user_tabs.js.coffee @@ -26,6 +26,10 @@ # Personal projects # # +#
  • +# +# +#
  • # # #
    @@ -41,6 +45,9 @@ #
    # Projects content #
    +#
    +# Snippets content +#
    #
    # #
    @@ -100,7 +107,7 @@ class @UserTabs if action is 'activity' @loadActivities(source) - if action in ['groups', 'contributed', 'projects'] + if action in ['groups', 'contributed', 'projects', 'snippets'] @loadTab(source, action) loadTab: (source, action) -> diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2ae180c8a12..799421c185b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -58,6 +58,19 @@ class UsersController < ApplicationController end end + def snippets + load_snippets + + respond_to do |format| + format.html { render 'show' } + format.json do + render json: { + html: view_to_html_string("snippets/_snippets", collection: @snippets) + } + end + end + end + def calendar calendar = contributions_calendar @timestamps = calendar.timestamps @@ -116,6 +129,15 @@ class UsersController < ApplicationController @groups = JoinedGroupsFinder.new(user).execute(current_user) end + def load_snippets + @snippets = SnippetsFinder.new.execute( + current_user, + filter: :by_user, + user: user, + scope: params[:scope] + ).page(params[:page]) + end + def projects_for_current_user ProjectsFinder.new.execute(current_user) end diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 3028491e5b6..a453a7fedb7 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -81,6 +81,9 @@ %li.projects-tab = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects + %li.snippets-tab + = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + Snippets %div{ class: container_class } .tab-content @@ -104,6 +107,9 @@ #projects.tab-pane - # This tab is always loaded via AJAX + #snippets.tab-pane + - # This tab is always loaded via AJAX + .loading-status = spinner diff --git a/config/routes.rb b/config/routes.rb index 2f820aafed1..f6a41331ecf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -89,8 +89,6 @@ Rails.application.routes.draw do end end - get '/s/:username' => 'snippets#index', as: :user_snippets, constraints: { username: /.*/ } - # # Invites # @@ -355,6 +353,9 @@ Rails.application.routes.draw do get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects, constraints: { username: /.*/ } + get 'u/:username/snippets' => 'users#snippets', as: :user_snippets, + constraints: { username: /.*/ } + get '/u/:username' => 'users#show', as: :user, constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Mon, 2 May 2016 13:19:39 +0200 Subject: [PATCH 0063/1306] 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 e308ccc3004..1156aab8cc2 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 b263a27d4d3..3462c2dcfbc 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 0064/1306] 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 5b2982e4994..f9ceee142d7 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 2c2f7ed0665..16083f90bb4 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 105017c3084c60e45f4bac85a76da78f39e6433f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 2 May 2016 13:29:17 +0200 Subject: [PATCH 0065/1306] Added JWT controller --- Gemfile | 1 + Gemfile.lock | 2 + app/controllers/jwt_controller.rb | 173 ++++++++++++++++++++++++++++++ config/routes.rb | 3 + 4 files changed, 179 insertions(+) create mode 100644 app/controllers/jwt_controller.rb diff --git a/Gemfile b/Gemfile index 512c6babd7e..0301f6fe062 100644 --- a/Gemfile +++ b/Gemfile @@ -225,6 +225,7 @@ gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' +gem 'base32', '~> 0.3.0' # Sentry integration gem 'sentry-raven', '~> 0.15' diff --git a/Gemfile.lock b/Gemfile.lock index 2b578429b3c..2b1cfdc9bb2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,7 @@ GEM ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) + base32 (0.3.2) bcrypt (3.1.10) benchmark-ips (2.3.0) better_errors (1.0.1) @@ -897,6 +898,7 @@ DEPENDENCIES attr_encrypted (~> 1.3.4) awesome_print (~> 1.2.0) babosa (~> 1.0.2) + base32 (~> 0.3.0) benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb new file mode 100644 index 00000000000..7e70c70c89c --- /dev/null +++ b/app/controllers/jwt_controller.rb @@ -0,0 +1,173 @@ +class JwtController < ApplicationController + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + + def auth + @authenticated = authenticate_with_http_basic do |login, password| + @ci_project = ci_project(login, password) + @user = authenticate_user(login, password) unless @ci_project + end + + unless @authenticated + return render_403 if has_basic_credentials? + end + + case params[:service] + when 'docker' + docker_token_auth(params[:scope], params[:offline_token]) + else + return render_404 + end + end + + private + + def render_400 + head :invalid_request + end + + def render_404 + head :not_found + end + + def render_403 + head :forbidden + end + + def docker_token_auth(scope, offline_token) + payload = { + aud: params[:service], + sub: @user.try(:username) + } + + if offline_token + return render_403 unless @user + elsif scope + access = process_access(scope) + return render_404 unless access + payload[:access] = [access] + end + + render json: { token: encode(payload) } + end + + def ci_project(login, password) + matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) + + if matched_login.present? + underscored_service = matched_login['s'].underscore + + if underscored_service == 'gitlab_ci' + Project.find_by(builds_enabled: true, runners_token: password) + end + end + end + + def authenticate_user(login, password) + user = Gitlab::Auth.new.find(login, password) + + # If the user authenticated successfully, we reset the auth failure count + # from Rack::Attack for that IP. A client may attempt to authenticate + # with a username and blank password first, and only after it receives + # a 401 error does it present a password. Resetting the count prevents + # false positives from occurring. + # + # Otherwise, we let Rack::Attack know there was a failed authentication + # attempt from this IP. This information is stored in the Rails cache + # (Redis) and will be used by the Rack::Attack middleware to decide + # whether to block requests from this IP. + config = Gitlab.config.rack_attack.git_basic_auth + + if config.enabled + if user + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(request.ip, config) + else + banned = Rack::Attack::Allow2Ban.filter(request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(request.ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{request.ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end + end + + user + end + + def process_access(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + project = Project.find_with_namespace(name) + return unless project + + actions = actions.select do |action| + can_access?(project, action) + end + + { type: 'repository', name: name, actions: actions } if actions + end + + def default_payload + { + aud: 'docker', + sub: @user.try(:username), + aud: params[:service], + } + end + + def private_key + @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key + end + + def encode(payload) + issued_at = Time.now + payload = payload.merge( + iss: Gitlab.config.registry.issuer, + iat: issued_at.to_i, + nbf: issued_at.to_i - 5.seconds.to_i, + exp: issued_at.to_i + 60.minutes.to_i, + jti: SecureRandom.uuid, + ) + headers = { + kid: kid(private_key) + } + JWT.encode(payload, private_key, 'RS256', headers) + end + + def can_access?(project, action) + case action + when 'pull' + project == @ci_project || can?(@user, :download_code, project) + when 'push' + project == @ci_project || can?(@user, :push_code, project) + else + false + end + end + + def kid(private_key) + sha256 = Digest::SHA256.new + sha256.update(private_key.public_key.to_der) + payload = StringIO.new(sha256.digest).read(30) + Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| + mem << slice.join + end.join(':') + end +end diff --git a/config/routes.rb b/config/routes.rb index adf4bb18b3c..5b48819dd9d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -63,6 +63,9 @@ Rails.application.routes.draw do get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete + # JSON Web Token + get 'jwt/auth' => 'jwt#auth' + # API API::API.logger Rails.logger mount API::API => '/api' From 011a905a821e2ff0cd2d9885ef93764018eb8346 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 2 May 2016 14:32:16 +0200 Subject: [PATCH 0066/1306] Split docker authentication service --- app/controllers/jwt_controller.rb | 116 +++--------------- .../jwt/docker_authentication_service.rb | 65 ++++++++++ lib/jwt/rsa_token.rb | 36 ++++++ lib/jwt/token.rb | 48 ++++++++ 4 files changed, 163 insertions(+), 102 deletions(-) create mode 100644 app/services/jwt/docker_authentication_service.rb create mode 100644 lib/jwt/rsa_token.rb create mode 100644 lib/jwt/token.rb diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 7e70c70c89c..2a92627cb1b 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -2,6 +2,10 @@ class JwtController < ApplicationController skip_before_action :authenticate_user! skip_before_action :verify_authenticity_token + SERVICES = { + 'docker' => Jwt::DockerAuthenticationService, + } + def auth @authenticated = authenticate_with_http_basic do |login, password| @ci_project = ci_project(login, password) @@ -9,46 +13,22 @@ class JwtController < ApplicationController end unless @authenticated - return render_403 if has_basic_credentials? + head :forbidden if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) end - case params[:service] - when 'docker' - docker_token_auth(params[:scope], params[:offline_token]) - else - return render_404 - end + service = SERVICES[params[:service]] + head :not_found unless service + + result = service.new(@ci_project, @user, auth_params).execute + return head result[:http_status] if result[:http_status] + + render json: result end private - def render_400 - head :invalid_request - end - - def render_404 - head :not_found - end - - def render_403 - head :forbidden - end - - def docker_token_auth(scope, offline_token) - payload = { - aud: params[:service], - sub: @user.try(:username) - } - - if offline_token - return render_403 unless @user - elsif scope - access = process_access(scope) - return render_404 unless access - payload[:access] = [access] - end - - render json: { token: encode(payload) } + def auth_params + params.permit(:service, :scope, :offline_token, :account, :client_id) end def ci_project(login, password) @@ -102,72 +82,4 @@ class JwtController < ApplicationController user end - - def process_access(scope) - type, name, actions = scope.split(':', 3) - actions = actions.split(',') - - case type - when 'repository' - process_repository_access(type, name, actions) - end - end - - def process_repository_access(type, name, actions) - project = Project.find_with_namespace(name) - return unless project - - actions = actions.select do |action| - can_access?(project, action) - end - - { type: 'repository', name: name, actions: actions } if actions - end - - def default_payload - { - aud: 'docker', - sub: @user.try(:username), - aud: params[:service], - } - end - - def private_key - @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key - end - - def encode(payload) - issued_at = Time.now - payload = payload.merge( - iss: Gitlab.config.registry.issuer, - iat: issued_at.to_i, - nbf: issued_at.to_i - 5.seconds.to_i, - exp: issued_at.to_i + 60.minutes.to_i, - jti: SecureRandom.uuid, - ) - headers = { - kid: kid(private_key) - } - JWT.encode(payload, private_key, 'RS256', headers) - end - - def can_access?(project, action) - case action - when 'pull' - project == @ci_project || can?(@user, :download_code, project) - when 'push' - project == @ci_project || can?(@user, :push_code, project) - else - false - end - end - - def kid(private_key) - sha256 = Digest::SHA256.new - sha256.update(private_key.public_key.to_der) - payload = StringIO.new(sha256.digest).read(30) - Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| - mem << slice.join - end.join(':') - end end diff --git a/app/services/jwt/docker_authentication_service.rb b/app/services/jwt/docker_authentication_service.rb new file mode 100644 index 00000000000..ce28085e5d6 --- /dev/null +++ b/app/services/jwt/docker_authentication_service.rb @@ -0,0 +1,65 @@ +module Jwt + class DockerAuthenticationService < BaseService + def execute + if params[:offline_token] + return error('forbidden', 403) unless current_user + end + + { token: token.encoded } + end + + private + + def token + token = ::Jwt::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token[:access] = access + token + end + + def access + return unless params[:scope] + + scope = process_scope(params[:scope]) + [scope].compact + end + + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + current_project = Project.find_with_namespace(name) + return unless current_project + + actions = actions.select do |action| + can_access?(current_project, action) + end + + { type: type, name: name, actions: actions } if actions + end + + def can_access?(current_project, action) + case action + when 'pull' + current_project == project || can?(current_user, :download_code, current_project) + when 'push' + current_project == project || can?(current_user, :push_code, current_project) + else + false + end + end + + def registry + Gitlab.config.registry + end + end +end diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb new file mode 100644 index 00000000000..cc265e3b31a --- /dev/null +++ b/lib/jwt/rsa_token.rb @@ -0,0 +1,36 @@ +module Jwt + class RSAToken < Token + attr_reader :key_file + + def initialize(key_file) + super() + @key_file = key_file + end + + def encoded + headers = { + kid: kid + } + JWT.encode(payload, key, 'RS256', headers) + end + + private + + def key_data + @key_data ||= File.read(key_file) + end + + def key + @key ||= OpenSSL::PKey::RSA.new(key_data) + end + + def kid + sha256 = Digest::SHA256.new + sha256.update(key.public_key.to_der) + payload = StringIO.new(sha256.digest).read(30) + Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| + mem << slice.join + end.join(':') + end + end +end diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb new file mode 100644 index 00000000000..38cbc8004e7 --- /dev/null +++ b/lib/jwt/token.rb @@ -0,0 +1,48 @@ +module Jwt + class Token + attr_accessor :issuer, :subject, :audience, :id + attr_accessor :issued_at, :not_before, :expire_time + + def initialize + @payload = {} + @id = SecureRandom.uuid + @issued_at = Time.now + @not_before = issued_at - 5.seconds + @expire_time = issued_at + 1.minute + end + + def [](key) + @payload[key] + end + + def []=(key, value) + @payload[key] = value + end + + def encoded + raise NotImplementedError + end + + def payload + @payload.merge(default_payload) + end + + def to_json + payload.to_json + end + + private + + def default_payload + { + jti: id, + aud: audience, + sub: subject, + iss: issuer, + iat: issued_at.to_i, + nbf: not_before.to_i, + exp: expire_time.to_i + }.compact + end + end +end \ No newline at end of file From 3dc276b367fe88c3c1026371d275d6078611f625 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 3 May 2016 11:46:14 +0200 Subject: [PATCH 0067/1306] 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 32e903905ad..0479006f993 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 f4e0c56279007fd6cec3d8e6bd684f0483b0e0ff Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 3 May 2016 13:03:10 +0200 Subject: [PATCH 0068/1306] Improve documentation and web test for web hooks I wanted to share what I learned trying to debug web hooks using netcat. --- app/controllers/projects/hooks_controller.rb | 2 +- app/models/hooks/web_hook.rb | 2 +- doc/web_hooks/web_hooks.md | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index dfa9bd259e8..366373b0f0a 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -28,7 +28,7 @@ class Projects::HooksController < Projects::ApplicationController status, message = TestHookService.new.execute(hook, current_user) if status - flash[:notice] = 'Hook successfully executed.' + flash[:notice] = "Hook successfully executed, HTTP #{status} #{message}" else flash[:alert] = "Hook execution failed: #{message}" end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 1e3b4815596..818abbf4cc4 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -59,7 +59,7 @@ class WebHook < ActiveRecord::Base basic_auth: auth) end - [(response.code >= 200 && response.code < 300), ActionView::Base.full_sanitizer.sanitize(response.to_s)] + [response.code, ActionView::Base.full_sanitizer.sanitize(response.to_s)] rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e logger.error("WebHook Error => #{e}") [false, e.to_s] diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index c1c51302e79..6ffdb18339e 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -13,6 +13,19 @@ You can configure webhooks to listen for specific events like pushes, issues or Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. +## Webhook endpoint tips + +If you are writing your own endpoint (web server) that will receive +GitLab web hooks keep in mind the following things: + +- Your endpoint should send its HTTP response as fast as possible. If + you wait too long, GitLab may decide the hook failed and retry it. +- Your endpoint should ALWAYS return a valid HTTP response. If you do + not do this then GitLab will think the hook failed and retry it. + Most HTTP libraries take care of this for you automatically but if + you are writing a low-level hook this is important to remember. +- GitLab ignores the HTTP status code returned by your endpoint. + ## SSL Verification By default, the SSL certificate of the webhook endpoint is verified based on From 23a3e3756a4f44aa8bd69310a2e584c1d4f7af1d Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 3 May 2016 13:40:59 +0200 Subject: [PATCH 0069/1306] Inform user about questionable hook success --- app/controllers/projects/hooks_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 366373b0f0a..9869d90831c 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -27,8 +27,10 @@ class Projects::HooksController < Projects::ApplicationController if !@project.empty_repo? status, message = TestHookService.new.execute(hook, current_user) - if status - flash[:notice] = "Hook successfully executed, HTTP #{status} #{message}" + if status && status >= 200 && status < 400 + flash[:notice] = "Hook executed successfully" + elsif status + flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}" else flash[:alert] = "Hook execution failed: #{message}" end From 6957fb7c4f0993ba1e0cbd9949a7e96c2c2def32 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 3 May 2016 13:42:56 +0200 Subject: [PATCH 0070/1306] Always mention HTTP status --- app/controllers/projects/hooks_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 9869d90831c..47524b1cf0b 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -28,7 +28,7 @@ class Projects::HooksController < Projects::ApplicationController status, message = TestHookService.new.execute(hook, current_user) if status && status >= 200 && status < 400 - flash[:notice] = "Hook executed successfully" + flash[:notice] = "Hook executed successfully: HTTP #{status}" elsif status flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}" else From faaab2aef8e7f6b06f8f04dc68596e1229d507db Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 3 May 2016 11:58:43 -0500 Subject: [PATCH 0071/1306] Add to label :id to response --- app/controllers/dashboard/labels_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb index 23a4ef21ea2..2a88350a4ca 100644 --- a/app/controllers/dashboard/labels_controller.rb +++ b/app/controllers/dashboard/labels_controller.rb @@ -1,6 +1,6 @@ class Dashboard::LabelsController < Dashboard::ApplicationController def index - labels = Label.where(project_id: projects).select(:title, :color).uniq(:title) + labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title) respond_to do |format| format.json { render json: labels } From b0ddbaa07cd780b0ed86aa4e3c24744c6426b1e1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Apr 2016 08:14:40 -0400 Subject: [PATCH 0072/1306] Added docker registry view --- app/controllers/projects/images_controller.rb | 26 ++++++++++ app/helpers/gitlab_routing_helper.rb | 4 ++ app/helpers/projects_helper.rb | 4 ++ app/models/project.rb | 4 ++ app/models/registry.rb | 42 ++++++++++++++++ app/views/layouts/nav/_project.html.haml | 7 +++ .../projects/images/_header_title.html.haml | 1 + app/views/projects/images/index.html.haml | 48 +++++++++++++++++++ config/initializers/mime_types.rb | 7 +++ config/routes.rb | 2 + lib/gitlab/regex.rb | 4 ++ lib/registry_client.rb | 38 +++++++++++++++ 12 files changed, 187 insertions(+) create mode 100644 app/controllers/projects/images_controller.rb create mode 100644 app/models/registry.rb create mode 100644 app/views/projects/images/_header_title.html.haml create mode 100644 app/views/projects/images/index.html.haml create mode 100644 lib/registry_client.rb diff --git a/app/controllers/projects/images_controller.rb b/app/controllers/projects/images_controller.rb new file mode 100644 index 00000000000..5b10746aa0d --- /dev/null +++ b/app/controllers/projects/images_controller.rb @@ -0,0 +1,26 @@ +class Projects::ImagesController < Projects::ApplicationController + before_action :authorize_read_image! + before_action :authorize_update_image!, only: [:destroy] + before_action :tag, except: [:index] + layout 'project' + + def index + @tags = registry.tags + end + + def destroy + # registry.destroy_tag(tag['fsLayers'].first['blobSum']) + registry.destroy_tag(registry.tag_digest(params[:id])) + redirect_to namespace_project_images_path(project.namespace, project) + end + + private + + def registry + @registry ||= project.registry + end + + def tag + @tag ||= registry.tag(params[:id]) + end +end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f07eff3fb57..66cb41cc496 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -33,6 +33,10 @@ module GitlabRoutingHelper namespace_project_builds_path(project.namespace, project, *args) end + def project_images_path(project, *args) + namespace_project_images_path(project.namespace, project, *args) + end + def activity_project_path(project, *args) activity_namespace_project_path(project.namespace, project, *args) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 3d5e61d2c18..6d1e630a097 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -152,6 +152,10 @@ module ProjectsHelper nav_tabs << :builds end + if can?(current_user, :read_image, project) + nav_tabs << :images + end + if can?(current_user, :admin_project, project) nav_tabs << :settings end diff --git a/app/models/project.rb b/app/models/project.rb index 76265a59ea7..496f9f3e347 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -370,6 +370,10 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end + def registry + @registry ||= Registry.new(path_with_namespace, self) + end + def registry_repository_url "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" if images_enabled? && Gitlab.config.registry.enabled end diff --git a/app/models/registry.rb b/app/models/registry.rb new file mode 100644 index 00000000000..b4ef60a016f --- /dev/null +++ b/app/models/registry.rb @@ -0,0 +1,42 @@ +require 'net/http' + +class Registry + attr_accessor :path_with_namespace, :project + + def initialize(path_with_namespace, project) + @path_with_namespace = path_with_namespace + @project = project + end + + def tags + @tags ||= client.tags(path_with_namespace) + end + + def tag(reference) + return @tag[reference] if defined?(@tag[reference]) + @tag ||= {} + @tag[reference] ||= client.tag(path_with_namespace, reference) + end + + def tag_digest(reference) + return @tag_digest[reference] if defined?(@tag_digest[reference]) + @tag_digest ||= {} + @tag_digest[reference] ||= client.tag_digest(path_with_namespace, reference) + end + + def destroy_tag(reference) + client.delete_tag(path_with_namespace, reference) + end + + def blob_size(blob) + return @blob_size[blob] if defined?(@blob_size[blob]) + @blob_size ||= {} + @blob_size[blob] ||= client.blob_size(path_with_namespace, blob) + end + + private + + def client + @client ||= RegistryClient.new(Gitlab.config.registry.api_url) + end +end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 479bde33719..2577afefa95 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -46,6 +46,13 @@ Builds %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) + - if project_nav_tab? :images + = nav_link(controller: %w(images)) do + = link_to project_images_path(@project), title: 'Images', class: 'shortcuts-images' do + = icon('image fw') + %span + Images + - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do diff --git a/app/views/projects/images/_header_title.html.haml b/app/views/projects/images/_header_title.html.haml new file mode 100644 index 00000000000..648aeeef2dc --- /dev/null +++ b/app/views/projects/images/_header_title.html.haml @@ -0,0 +1 @@ +- header_title project_title(@project, "Images", project_images_path(@project)) diff --git a/app/views/projects/images/index.html.haml b/app/views/projects/images/index.html.haml new file mode 100644 index 00000000000..338f3e5662c --- /dev/null +++ b/app/views/projects/images/index.html.haml @@ -0,0 +1,48 @@ +- page_title "Images" += render "header_title" + +.top-area + .nav-controls + +.gray-content-block + A list of Docker Images for this project + +%ul.content-list + - if @tags.blank? + %li + .nothing-here-block No images to show + - else + .table-holder + %table.table.builds + %thead + %tr + %th Name + %th Layers + %th Size + %th Created + %th Docker + %th + + - @tags.sort.each do |tag| + - details = @registry.tag(tag) + - layer = details['history'].first + - if layer && layer['v1Compatibility'] + - layer_data = JSON.parse(layer['v1Compatibility']) + %tr + %td + = link_to namespace_project_image_path(@project.namespace, @project, tag) do + #{details['name']}:#{details['tag']} + %td + = details['fsLayers'].length + %td + = number_to_human_size(details['fsLayers'].inject(0) { |sum, d| sum + @registry.blob_size(d['blobSum']) }.bytes) + %td + - if layer_data + = time_ago_in_words(DateTime.rfc3339(layer_data['created'])) + %td + - if layer_data + = layer_data['docker_version'] + %td.content + .controls.hidden-xs.pull-right + = link_to namespace_project_image_path(@project.namespace, @project, tag), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = icon("trash cred") diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index ca58ae92d1b..71e3c9d7db4 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -8,3 +8,10 @@ Mime::Type.register_alias "text/plain", :diff Mime::Type.register_alias "text/plain", :patch Mime::Type.register_alias 'text/html', :markdown Mime::Type.register_alias 'text/html', :md +#Mime::Type.unregister :json +Mime::Type.register_alias 'application/vnd.docker.distribution.manifest.v1+prettyjws', :json +#Mime::Type.register 'application/json', :json, %w( text/plain text/x-json application/jsonrequest ) + +ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime::Type.lookup('application/vnd.docker.distribution.manifest.v1+prettyjws')]=lambda do |body| + JSON.parse(body) +end diff --git a/config/routes.rb b/config/routes.rb index 5b48819dd9d..0280898accd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -690,6 +690,8 @@ Rails.application.routes.draw do end end + resources :images, only: [:index, :destroy], constraints: { id: Gitlab::Regex.image_reference_regex } + resources :milestones, constraints: { id: /\d+/ } do member do put :sort_issues diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index ace906a6f59..9b8f416ddfa 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -96,5 +96,9 @@ module Gitlab (? Date: Wed, 4 May 2016 14:22:54 +0200 Subject: [PATCH 0073/1306] Implement Container Registry API client --- app/controllers/projects/images_controller.rb | 16 +++-- app/models/project.rb | 5 +- app/models/registry.rb | 42 ------------ app/views/projects/images/index.html.haml | 28 ++++---- lib/image_registry/blob.rb | 47 ++++++++++++++ lib/image_registry/client.rb | 64 +++++++++++++++++++ lib/image_registry/config.rb | 15 +++++ lib/image_registry/registry.rb | 14 ++++ lib/image_registry/repository.rb | 38 +++++++++++ lib/image_registry/tag.rb | 62 ++++++++++++++++++ lib/registry_client.rb | 38 ----------- 11 files changed, 263 insertions(+), 106 deletions(-) delete mode 100644 app/models/registry.rb create mode 100644 lib/image_registry/blob.rb create mode 100644 lib/image_registry/client.rb create mode 100644 lib/image_registry/config.rb create mode 100644 lib/image_registry/registry.rb create mode 100644 lib/image_registry/repository.rb create mode 100644 lib/image_registry/tag.rb delete mode 100644 lib/registry_client.rb diff --git a/app/controllers/projects/images_controller.rb b/app/controllers/projects/images_controller.rb index 5b10746aa0d..cf3bdd42cf4 100644 --- a/app/controllers/projects/images_controller.rb +++ b/app/controllers/projects/images_controller.rb @@ -5,22 +5,24 @@ class Projects::ImagesController < Projects::ApplicationController layout 'project' def index - @tags = registry.tags + @tags = image_repository.tags end def destroy - # registry.destroy_tag(tag['fsLayers'].first['blobSum']) - registry.destroy_tag(registry.tag_digest(params[:id])) - redirect_to namespace_project_images_path(project.namespace, project) + if tag.delete + redirect_to namespace_project_images_path(project.namespace, project) + else + redirect_to namespace_project_images_path(project.namespace, project), alert: 'Failed to remove tag' + end end private - def registry - @registry ||= project.registry + def image_repository + @image_repository ||= project.image_repository end def tag - @tag ||= registry.tag(params[:id]) + @tag ||= image_repository[params[:id]] end end diff --git a/app/models/project.rb b/app/models/project.rb index 496f9f3e347..b905ebbfcaa 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -370,8 +370,9 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end - def registry - @registry ||= Registry.new(path_with_namespace, self) + def image_repository + @registry ||= ImageRegistry::Registry.new(Gitlab.config.registry.api_url) + @image_repository ||= ImageRegistry::Repository.new(@registry, path_with_namespace) end def registry_repository_url diff --git a/app/models/registry.rb b/app/models/registry.rb deleted file mode 100644 index b4ef60a016f..00000000000 --- a/app/models/registry.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'net/http' - -class Registry - attr_accessor :path_with_namespace, :project - - def initialize(path_with_namespace, project) - @path_with_namespace = path_with_namespace - @project = project - end - - def tags - @tags ||= client.tags(path_with_namespace) - end - - def tag(reference) - return @tag[reference] if defined?(@tag[reference]) - @tag ||= {} - @tag[reference] ||= client.tag(path_with_namespace, reference) - end - - def tag_digest(reference) - return @tag_digest[reference] if defined?(@tag_digest[reference]) - @tag_digest ||= {} - @tag_digest[reference] ||= client.tag_digest(path_with_namespace, reference) - end - - def destroy_tag(reference) - client.delete_tag(path_with_namespace, reference) - end - - def blob_size(blob) - return @blob_size[blob] if defined?(@blob_size[blob]) - @blob_size ||= {} - @blob_size[blob] ||= client.blob_size(path_with_namespace, blob) - end - - private - - def client - @client ||= RegistryClient.new(Gitlab.config.registry.api_url) - end -end diff --git a/app/views/projects/images/index.html.haml b/app/views/projects/images/index.html.haml index 338f3e5662c..0987c7a39eb 100644 --- a/app/views/projects/images/index.html.haml +++ b/app/views/projects/images/index.html.haml @@ -17,32 +17,26 @@ %thead %tr %th Name - %th Layers + %th Revision %th Size %th Created - %th Docker %th - - @tags.sort.each do |tag| - - details = @registry.tag(tag) - - layer = details['history'].first - - if layer && layer['v1Compatibility'] - - layer_data = JSON.parse(layer['v1Compatibility']) + - @tags.each do |tag| %tr %td - = link_to namespace_project_image_path(@project.namespace, @project, tag) do - #{details['name']}:#{details['tag']} + = link_to namespace_project_image_path(@project.namespace, @project, tag.name) do + #{tag.repository.name}:#{tag.name} %td - = details['fsLayers'].length + - if layer = tag.layers.first + \##{layer.short_revision} %td - = number_to_human_size(details['fsLayers'].inject(0) { |sum, d| sum + @registry.blob_size(d['blobSum']) }.bytes) + = pluralize(tag.layers.size, "layer") +   + = number_to_human_size(tag.total_size) %td - - if layer_data - = time_ago_in_words(DateTime.rfc3339(layer_data['created'])) - %td - - if layer_data - = layer_data['docker_version'] + = time_ago_in_words(tag.created_at) %td.content .controls.hidden-xs.pull-right - = link_to namespace_project_image_path(@project.namespace, @project, tag), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_image_path(@project.namespace, @project, tag.name), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/lib/image_registry/blob.rb b/lib/image_registry/blob.rb new file mode 100644 index 00000000000..1aeeba7a686 --- /dev/null +++ b/lib/image_registry/blob.rb @@ -0,0 +1,47 @@ +module ImageRegistry + class Blob + attr_reader :repository, :config + + def initialize(repository, config) + @repository = repository + @config = config || {} + end + + def valid? + digest.present? + end + + def digest + config['digest'] + end + + def type + config['mediaType'] + end + + def size + config['size'] + end + + def revision + digest.split(':')[1] + end + + def short_revision + revision[0..8] + end + + def client + @client ||= repository.client + end + + def delete + client.delete_blob(repository.name, digest) + end + + def data + return @data if defined?(@data) + @data ||= client.blob(repository.name, digest, type) + end + end +end diff --git a/lib/image_registry/client.rb b/lib/image_registry/client.rb new file mode 100644 index 00000000000..b2e43ce4aeb --- /dev/null +++ b/lib/image_registry/client.rb @@ -0,0 +1,64 @@ +require 'faraday' +require 'faraday_middleware' + +module ImageRegistry + class Client + attr_accessor :uri + + MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json' + + def initialize(base_uri, options = {}) + @base_uri = base_uri + @faraday = Faraday.new(@base_uri) do |builder| + builder.request :json + builder.headers['Accept'] = MANIFEST_VERSION + + builder.response :json, :content_type => /\bjson$/ + builder.response :json, :content_type => 'application/vnd.docker.distribution.manifest.v1+prettyjws' + + if options[:user] && options[:password] + builder.request(:basic_auth, options[:user].to_s, options[:password].to_s) + elsif options[:token] + builder.request(:authentication, :Bearer, options[:token].to_s) + end + + builder.adapter :net_http + end + end + + def repository_tags(name) + @faraday.get("/v2/#{name}/tags/list").body + end + + def repository_manifest(name, reference) + @faraday.get("/v2/#{name}/manifests/#{reference}").body + end + + def put_repository_manifest(name, reference, manifest) + @faraday.put("/v2/#{name}/manifests/#{reference}", manifest, { "Content-Type" => MANIFEST_VERSION }).success? + end + + def repository_mount_blob(name, digest, from) + @faraday.post("/v2/#{name}/blobls/uploads/?mount=#{digest}&from=#{from}").status == 201 + end + + def repository_tag_digest(name, reference) + response = @faraday.head("/v2/#{name}/manifests/#{reference}") + response.headers['docker-content-digest'] if response.success? + end + + def delete_repository_tag(name, reference) + @faraday.delete("/v2/#{name}/manifests/#{reference}").success? + end + + def blob(name, digest, type = nil) + headers = {} + headers['Accept'] = type if type + @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers).body + end + + def delete_blob(name, digest) + @faraday.delete("/v2/#{name}/blobs/#{digest}").success? + end + end +end diff --git a/lib/image_registry/config.rb b/lib/image_registry/config.rb new file mode 100644 index 00000000000..1c2abec1bfa --- /dev/null +++ b/lib/image_registry/config.rb @@ -0,0 +1,15 @@ +module ImageRegistry + class Config + attr_reader :tag, :blob, :data + + def initialize(tag, blob) + @tag, @blob = tag, blob + @data = JSON.parse(blob.data) + end + + def [](key) + return unless data + data[key] + end + end +end diff --git a/lib/image_registry/registry.rb b/lib/image_registry/registry.rb new file mode 100644 index 00000000000..d8de8e392e9 --- /dev/null +++ b/lib/image_registry/registry.rb @@ -0,0 +1,14 @@ +module ImageRegistry + class Registry + attr_reader :uri, :client + + def initialize(uri, options = {}) + @uri = URI.parse(uri) + @client = ImageRegistry::Client.new(uri, options) + end + + def [](name) + ImageRegistry::Repository.new(self, name) + end + end +end diff --git a/lib/image_registry/repository.rb b/lib/image_registry/repository.rb new file mode 100644 index 00000000000..f4f4ba65afc --- /dev/null +++ b/lib/image_registry/repository.rb @@ -0,0 +1,38 @@ +module ImageRegistry + class Repository + attr_reader :registry, :name + + def initialize(registry, name) + @registry, @name = registry, name + end + + def client + @client ||= registry.client + end + + def [](tag) + ImageRegistry::Tag.new(self, tag) + end + + def manifest + return @manifest if defined?(@manifest) + @manifest = client.repository_tags(name) + end + + def valid? + manifest.present? + end + + def tags + return @tags if defined?(@tags) + return unless manifest && manifest['tags'] + @tags = manifest['tags'].map do |tag| + ImageRegistry::Tag.new(self, tag) + end + end + + def delete + tags.each(:delete) + end + end +end diff --git a/lib/image_registry/tag.rb b/lib/image_registry/tag.rb new file mode 100644 index 00000000000..2bf0b8e345f --- /dev/null +++ b/lib/image_registry/tag.rb @@ -0,0 +1,62 @@ +module ImageRegistry + class Tag + attr_reader :repository, :name + + def initialize(repository, name) + @repository, @name = repository, name + end + + def valid? + manifest.present? + end + + def manifest + return @manifest if defined?(@manifest) + @manifest = client.repository_manifest(repository.name, name) + end + + def [](key) + return unless manifest + manifest[key] + end + + def digest + return @digest if defined?(@digest) + @digest = client.repository_tag_digest(repository.name, name) + end + + def config + return @config if defined?(@config) + return unless manifest && manifest['config'] + blob = ImageRegistry::Blob.new(repository, manifest['config']) + @config = ImageRegistry::Config.new(self, blob) + end + + def created_at + return unless config + @created_at ||= DateTime.rfc3339(config['created']) + end + + def layers + return @layers if defined?(@layers) + return unless manifest + @layers = manifest['layers'].map do |layer| + ImageRegistry::Blob.new(repository, layer) + end + end + + def total_size + return unless layers + layers.map(&:size).sum + end + + def delete + return unless digest + client.delete_repository_tag(repository.name, digest) + end + + def client + @client ||= repository.client + end + end +end diff --git a/lib/registry_client.rb b/lib/registry_client.rb deleted file mode 100644 index 87518a7b39c..00000000000 --- a/lib/registry_client.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'HTTParty' - -class RegistryClient - attr_accessor :uri - - def initialize(uri) - @uri = uri - end - - def tags(name) - response = HTTParty.get("#{uri}/v2/#{name}/tags/list") - response.parsed_response['tags'] - end - - def tag(name, reference) - response = HTTParty.get("#{uri}/v2/#{name}/manifests/#{reference}") - JSON.parse(response) - end - - def tag_digest(name, reference) - response = HTTParty.head("#{uri}/v2/#{name}/manifests/#{reference}") - response.headers['docker-content-digest'].split(':') - end - - def delete_tag(name, reference) - response = HTTParty.delete("#{uri}/v2/#{name}/manifests/#{reference}") - response.parsed_response - end - - def blob_size(name, digest) - response = HTTParty.head("#{uri}/v2/#{name}/blobs/#{digest}") - response.headers.content_length - end - - def delete_blob(name, digest) - HTTParty.delete("#{uri}/v2/#{name}/blobs/#{digest}") - end -end From d85b88962b603d46672ed6ebd01955ca7560fcc6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 4 May 2016 14:23:43 +0200 Subject: [PATCH 0074/1306] Remove unused mime_types --- config/initializers/mime_types.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 71e3c9d7db4..ca58ae92d1b 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -8,10 +8,3 @@ Mime::Type.register_alias "text/plain", :diff Mime::Type.register_alias "text/plain", :patch Mime::Type.register_alias 'text/html', :markdown Mime::Type.register_alias 'text/html', :md -#Mime::Type.unregister :json -Mime::Type.register_alias 'application/vnd.docker.distribution.manifest.v1+prettyjws', :json -#Mime::Type.register 'application/json', :json, %w( text/plain text/x-json application/jsonrequest ) - -ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime::Type.lookup('application/vnd.docker.distribution.manifest.v1+prettyjws')]=lambda do |body| - JSON.parse(body) -end From 7168493e8a25836dc7eedf25ec3241afd0d501b8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 4 May 2016 14:35:18 +0200 Subject: [PATCH 0075/1306] Remove container registry on project removal --- app/services/projects/destroy_service.rb | 8 ++++++++ lib/image_registry/repository.rb | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index df5054f08d7..70bfc1fd533 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -28,6 +28,10 @@ module Projects Project.transaction do project.destroy! + unless remove_registry_tags + raise_error('Failed to remove project image registry. Please try again or contact administrator') + end + unless remove_repository(repo_path) raise_error('Failed to remove project repository. Please try again or contact administrator') end @@ -61,6 +65,10 @@ module Projects end end + def remove_registry_tags + project.image_registry.delete_tags + end + def raise_error(message) raise DestroyError.new(message) end diff --git a/lib/image_registry/repository.rb b/lib/image_registry/repository.rb index f4f4ba65afc..c45fa2911e7 100644 --- a/lib/image_registry/repository.rb +++ b/lib/image_registry/repository.rb @@ -31,7 +31,8 @@ module ImageRegistry end end - def delete + def delete_tags + return unless tags tags.each(:delete) end end From e1c8663a3e7ad1f77a8476888331c376cc35eda5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 4 May 2016 15:15:16 +0200 Subject: [PATCH 0076/1306] Allow to copy all manifests from one container repository to another --- lib/image_registry/blob.rb | 4 ++++ lib/image_registry/client.rb | 2 +- lib/image_registry/repository.rb | 16 ++++++++++++++++ lib/image_registry/tag.rb | 21 +++++++++++++++++---- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/lib/image_registry/blob.rb b/lib/image_registry/blob.rb index 1aeeba7a686..43665149e23 100644 --- a/lib/image_registry/blob.rb +++ b/lib/image_registry/blob.rb @@ -43,5 +43,9 @@ module ImageRegistry return @data if defined?(@data) @data ||= client.blob(repository.name, digest, type) end + + def mount_to(to_repository) + client.repository_mount_blob(to_repository.name, digest, repository.name) + end end end diff --git a/lib/image_registry/client.rb b/lib/image_registry/client.rb index b2e43ce4aeb..84375ce8029 100644 --- a/lib/image_registry/client.rb +++ b/lib/image_registry/client.rb @@ -39,7 +39,7 @@ module ImageRegistry end def repository_mount_blob(name, digest, from) - @faraday.post("/v2/#{name}/blobls/uploads/?mount=#{digest}&from=#{from}").status == 201 + @faraday.post("/v2/#{name}/blobs/uploads/?mount=#{digest}&from=#{from}").status == 201 end def repository_tag_digest(name, reference) diff --git a/lib/image_registry/repository.rb b/lib/image_registry/repository.rb index c45fa2911e7..763d8669555 100644 --- a/lib/image_registry/repository.rb +++ b/lib/image_registry/repository.rb @@ -29,11 +29,27 @@ module ImageRegistry @tags = manifest['tags'].map do |tag| ImageRegistry::Tag.new(self, tag) end + @tags ||= [] end def delete_tags return unless tags tags.each(:delete) end + + def mount_blob(blob) + return unless blob + client.repository_mount_blob(name, blob.digest, blob.repository.name) + end + + def mount_manifest(tag, manifest) + client.put_repository_manifest(name, tag, manifest) + end + + def copy_to(other_repository) + tags.all? do |tag| + tag.copy_to(other_repository) + end + end end end diff --git a/lib/image_registry/tag.rb b/lib/image_registry/tag.rb index 2bf0b8e345f..76946a6ce5b 100644 --- a/lib/image_registry/tag.rb +++ b/lib/image_registry/tag.rb @@ -25,11 +25,15 @@ module ImageRegistry @digest = client.repository_tag_digest(repository.name, name) end - def config - return @config if defined?(@config) + def config_blob + return @config_blob if defined?(@config_blob) return unless manifest && manifest['config'] - blob = ImageRegistry::Blob.new(repository, manifest['config']) - @config = ImageRegistry::Config.new(self, blob) + @config_blob = ImageRegistry::Blob.new(repository, manifest['config']) + end + + def config + return unless config_blob + @config ||= ImageRegistry::Config.new(self, config_blob) end def created_at @@ -55,6 +59,15 @@ module ImageRegistry client.delete_repository_tag(repository.name, digest) end + def copy_to(repository) + return unless manifest + layers.each do |blob| + repository.mount_blob(blob) + end + repository.mount_blob(config_blob) + repository.mount_manifest(name, manifest.to_json) + end + def client @client ||= repository.client end From de008127eb9a7a14b06b2e4a3d3d1822ad6a54d7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 4 May 2016 16:16:54 +0200 Subject: [PATCH 0077/1306] Fix bearer token support --- lib/image_registry/client.rb | 2 +- lib/image_registry/repository.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/image_registry/client.rb b/lib/image_registry/client.rb index 84375ce8029..a4432059097 100644 --- a/lib/image_registry/client.rb +++ b/lib/image_registry/client.rb @@ -19,7 +19,7 @@ module ImageRegistry if options[:user] && options[:password] builder.request(:basic_auth, options[:user].to_s, options[:password].to_s) elsif options[:token] - builder.request(:authentication, :Bearer, options[:token].to_s) + builder.request(:authorization, :bearer, options[:token].to_s) end builder.adapter :net_http diff --git a/lib/image_registry/repository.rb b/lib/image_registry/repository.rb index 763d8669555..43e8e7720db 100644 --- a/lib/image_registry/repository.rb +++ b/lib/image_registry/repository.rb @@ -25,7 +25,7 @@ module ImageRegistry def tags return @tags if defined?(@tags) - return unless manifest && manifest['tags'] + return [] unless manifest && manifest['tags'] @tags = manifest['tags'].map do |tag| ImageRegistry::Tag.new(self, tag) end From 7731bb59c8d43cfa7e47c945d7aed05e5e3932c1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 4 May 2016 16:17:08 +0200 Subject: [PATCH 0078/1306] Use bearer token to access registry --- app/models/project.rb | 3 ++- app/services/jwt/docker_authentication_service.rb | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index b905ebbfcaa..c50ea45d3eb 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -371,7 +371,8 @@ class Project < ActiveRecord::Base end def image_repository - @registry ||= ImageRegistry::Registry.new(Gitlab.config.registry.api_url) + @registry_token ||= Jwt::DockerAuthenticationService.full_access_token(path_with_namespace) + @registry ||= ImageRegistry::Registry.new(Gitlab.config.registry.api_url, token: @registry_token) @image_repository ||= ImageRegistry::Repository.new(@registry, path_with_namespace) end diff --git a/app/services/jwt/docker_authentication_service.rb b/app/services/jwt/docker_authentication_service.rb index ce28085e5d6..16d77193a1e 100644 --- a/app/services/jwt/docker_authentication_service.rb +++ b/app/services/jwt/docker_authentication_service.rb @@ -8,6 +8,17 @@ module Jwt { token: token.encoded } end + def self.full_access_token(*names) + registry = Gitlab.config.registry + token = ::Jwt::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = 'docker' + token[:access] = names.map do |name| + { type: 'repository', name: name, actions: %w(pull push) } + end + token.encoded + end + private def token From 2afae7eac97d24d51eb949b9faa676314f06cdd6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 4 May 2016 16:17:35 +0200 Subject: [PATCH 0079/1306] Use Container Images instead of Images --- .../projects/images/_header_title.html.haml | 2 +- app/views/projects/images/index.html.haml | 30 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/views/projects/images/_header_title.html.haml b/app/views/projects/images/_header_title.html.haml index 648aeeef2dc..f583e7fcfef 100644 --- a/app/views/projects/images/_header_title.html.haml +++ b/app/views/projects/images/_header_title.html.haml @@ -1 +1 @@ -- header_title project_title(@project, "Images", project_images_path(@project)) +- header_title project_title(@project, "Container Images", project_images_path(@project)) diff --git a/app/views/projects/images/index.html.haml b/app/views/projects/images/index.html.haml index 0987c7a39eb..3732698c088 100644 --- a/app/views/projects/images/index.html.haml +++ b/app/views/projects/images/index.html.haml @@ -1,11 +1,23 @@ -- page_title "Images" +- page_title "Container Images" = render "header_title" -.top-area - .nav-controls +.light.prepend-top-default + %p + A 'container image' is a snapshot of a container. + You can host your 'container images' with GitLab. + %br + To start using container images hosted on GitLab you first need to login: + %pre + %code + docker login #{Gitlab.config.registry.host_port} + %br + Then you are free to create and upload a container images with build and push commands: + %pre + docker build -t #{Gitlab.config.registry.host_port}/#{@project.path_with_namespace} . + %br + docker push #{Gitlab.config.registry.host_port}/#{@project.path_with_namespace} -.gray-content-block - A list of Docker Images for this project +%hr %ul.content-list - if @tags.blank? @@ -25,15 +37,15 @@ - @tags.each do |tag| %tr %td - = link_to namespace_project_image_path(@project.namespace, @project, tag.name) do - #{tag.repository.name}:#{tag.name} + #{tag.repository.name}:#{tag.name} + = clipboard_button(clipboard_text: "docker pull #{Gitlab.config.registry.host_port}/#{tag.repository.name}:#{tag.name}") %td - if layer = tag.layers.first \##{layer.short_revision} %td - = pluralize(tag.layers.size, "layer") -   = number_to_human_size(tag.total_size) + · + = pluralize(tag.layers.size, "layer") %td = time_ago_in_words(tag.created_at) %td.content From 9e619d3813764566e5f4c0208e5f2c7365351808 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 4 May 2016 16:28:01 +0200 Subject: [PATCH 0080/1306] Use Container Images --- app/views/layouts/nav/_project.html.haml | 4 ++-- app/views/projects/images/index.html.haml | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2577afefa95..bef350adf34 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -49,9 +49,9 @@ - if project_nav_tab? :images = nav_link(controller: %w(images)) do = link_to project_images_path(@project), title: 'Images', class: 'shortcuts-images' do - = icon('image fw') + = icon('hdd-o fw') %span - Images + Container Images - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do diff --git a/app/views/projects/images/index.html.haml b/app/views/projects/images/index.html.haml index 3732698c088..08f67345b4a 100644 --- a/app/views/projects/images/index.html.haml +++ b/app/views/projects/images/index.html.haml @@ -29,7 +29,7 @@ %thead %tr %th Name - %th Revision + %th Digest %th Size %th Created %th @@ -41,7 +41,10 @@ = clipboard_button(clipboard_text: "docker pull #{Gitlab.config.registry.host_port}/#{tag.repository.name}:#{tag.name}") %td - if layer = tag.layers.first - \##{layer.short_revision} + %span.has-tooltip(title="#{layer.revision}") + = layer.short_revision + - else + \- %td = number_to_human_size(tag.total_size) · From 17d23283a119fcfc2f6ac070bb5df8d913abf2e7 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 4 May 2016 12:46:53 -0500 Subject: [PATCH 0081/1306] Restore "r" shortcut --- app/assets/javascripts/shortcuts_issuable.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index ad9b3c1c6bf..ccb42ab2168 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -6,6 +6,10 @@ class @ShortcutsIssuable extends ShortcutsNavigation super() Mousetrap.bind('a', @openSidebarDropdown.bind(@, 'assignee')) Mousetrap.bind('m', @openSidebarDropdown.bind(@, 'milestone')) + Mousetrap.bind('r', => + @replyWithSelectedText() + return false + ) Mousetrap.bind('j', => @prevIssue() return false From 8b813277b0cbf57b2a07ad2e1b4cb87dadfb66c5 Mon Sep 17 00:00:00 2001 From: Yatish Mehta Date: Wed, 4 May 2016 14:04:13 -0700 Subject: [PATCH 0082/1306] Fixed typo in zen.scss and corresponding views --- app/assets/stylesheets/framework/zen.scss | 2 +- app/views/projects/_md_preview.html.haml | 2 +- app/views/projects/_zen.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index f870ea0d87f..ff02ebdd34c 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -32,7 +32,7 @@ } } -.zen-cotrol { +.zen-control { padding: 0; color: #555; background: none; diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 8de44a6c914..81afea2c60a 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -8,7 +8,7 @@ %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview %li.pull-right - %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } + %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } Go full screen .md-write-holder diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index e1e35013968..413477a2d3a 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -4,5 +4,5 @@ = f.text_area attr, class: classes, placeholder: placeholder - else = text_area_tag attr, nil, class: classes, placeholder: placeholder - %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } + %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') From f18ec70743375024aa7ec7bb86c437ca9198e729 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 4 May 2016 17:05:16 -0400 Subject: [PATCH 0083/1306] Backport changes from gitlab-org/gitlab-ee!372 Mostly replaces several Spinach tests with RSpec Feature tests. --- app/helpers/projects_helper.rb | 3 +- features/project/create.feature | 16 +---- features/steps/project/create.rb | 26 +------- spec/factories/projects.rb | 10 ++- ...r_views_empty_project_instructions_spec.rb | 63 +++++++++++++++++++ spec/helpers/projects_helper_spec.rb | 10 +-- 6 files changed, 80 insertions(+), 48 deletions(-) create mode 100644 spec/features/projects/developer_views_empty_project_instructions_spec.rb diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 3d5e61d2c18..62e8c03cc81 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -209,7 +209,8 @@ module ProjectsHelper end def default_url_to_repo(project = @project) - if default_clone_protocol == "ssh" + case default_clone_protocol + when 'ssh' project.ssh_url_to_repo else project.http_url_to_repo diff --git a/features/project/create.feature b/features/project/create.feature index 27136798e36..67336d73bf7 100644 --- a/features/project/create.feature +++ b/features/project/create.feature @@ -7,20 +7,8 @@ Feature: Project Create @javascript Scenario: User create a project Given I sign in as a user - When I visit new project page And I have an ssh key + When I visit new project page And fill project form with valid data Then I should see project page - And I should see empty project instuctions - - @javascript - Scenario: Empty project instructions - Given I sign in as a user - And I have an ssh key - When I visit new project page - And fill project form with valid data - Then I see empty project instuctions - And I click on HTTP - Then Remote url should update to http link - And If I click on SSH - Then Remote url should update to ssh link + And I should see empty project instructions diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index 422b151eaa2..5f5f806df36 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -13,33 +13,9 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps expect(current_path).to eq namespace_project_path(Project.last.namespace, Project.last) end - step 'I should see empty project instuctions' do + step 'I should see empty project instructions' do expect(page).to have_content "git init" expect(page).to have_content "git remote" expect(page).to have_content Project.last.url_to_repo end - - step 'I see empty project instuctions' do - expect(page).to have_content "git init" - expect(page).to have_content "git remote" - expect(page).to have_content Project.last.url_to_repo - end - - step 'I click on HTTP' do - find('#clone-dropdown').click - find('.http-selector').click - end - - step 'Remote url should update to http link' do - expect(page).to have_content "git remote add origin #{Project.last.http_url_to_repo}" - end - - step 'If I click on SSH' do - find('#clone-dropdown').click - find('.ssh-selector').click - end - - step 'Remote url should update to ssh link' do - expect(page).to have_content "git remote add origin #{Project.last.url_to_repo}" - end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index c14b99606ba..5338c88dd84 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -61,6 +61,12 @@ FactoryGirl.define do trait :private do visibility_level Gitlab::VisibilityLevel::PRIVATE end + + trait :empty_repo do + after(:create) do |project| + project.create_repository + end + end end # Project with empty repository @@ -68,9 +74,7 @@ FactoryGirl.define do # This is a case when you just created a project # but not pushed any code there yet factory :project_empty_repo, parent: :empty_project do - after :create do |project| - project.create_repository - end + empty_repo end # Project with test repository diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb new file mode 100644 index 00000000000..0c51fe72ca4 --- /dev/null +++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +feature 'Developer views empty project instructions', feature: true do + let(:project) { create(:empty_project, :empty_repo) } + let(:developer) { create(:user) } + + background do + project.team << [developer, :developer] + + login_as(developer) + end + + context 'without an SSH key' do + scenario 'defaults to HTTP' do + visit_project + + expect_instructions_for('http') + end + + scenario 'switches to SSH', js: true do + visit_project + + select_protocol('SSH') + + expect_instructions_for('ssh') + end + end + + context 'with an SSH key' do + background do + create(:personal_key, user: developer) + end + + scenario 'defaults to SSH' do + visit_project + + expect_instructions_for('ssh') + end + + scenario 'switches to HTTP', js: true do + visit_project + + select_protocol('HTTP') + + expect_instructions_for('http') + end + end + + def visit_project + visit namespace_project_path(project.namespace, project) + end + + def select_protocol(protocol) + find('#clone-dropdown').click + find(".#{protocol.downcase}-selector").click + end + + def expect_instructions_for(protocol) + msg = :"#{protocol.downcase}_url_to_repo" + + expect(page).to have_content("git clone #{project.send(msg)}") + end +end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 62389188d2c..6f7e2ae78fd 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -88,18 +88,18 @@ describe ProjectsHelper do end describe 'default_clone_protocol' do - describe 'using HTTP' do + context 'when user is not logged in and gitlab protocol is HTTP' do it 'returns HTTP' do - expect(helper).to receive(:current_user).and_return(nil) + allow(helper).to receive(:current_user).and_return(nil) expect(helper.send(:default_clone_protocol)).to eq('http') end end - describe 'using HTTPS' do + context 'when user is not logged in and gitlab protocol is HTTPS' do it 'returns HTTPS' do - allow(Gitlab.config.gitlab).to receive(:protocol).and_return('https') - expect(helper).to receive(:current_user).and_return(nil) + stub_config_setting(protocol: 'https') + allow(helper).to receive(:current_user).and_return(nil) expect(helper.send(:default_clone_protocol)).to eq('https') end From e4c64855e8531a9375de1d64a95f2e593b80c2bd Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Wed, 27 Apr 2016 23:37:07 +0200 Subject: [PATCH 0084/1306] Don't read otp_secret_encryption_key from hardcoded path in models/user Variable `Gitlab::Application.config.secret_key_base` is set in config/initializers/secret_token.rb. It's very bad practice to use hard-coded paths inside an application and really unnecessary in this case. --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index ab48f8f1960..a468b6ea075 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -85,7 +85,7 @@ class User < ActiveRecord::Base default_value_for :theme_id, gitlab_config.default_theme devise :two_factor_authenticatable, - otp_secret_encryption_key: File.read(Rails.root.join('.secret')).chomp + 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 From 8dc19494c3fdae366daa8849b5e2a3f58f98878c Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Thu, 5 May 2016 13:26:36 +0700 Subject: [PATCH 0085/1306] Remove unused code, update spec, and update changelog --- CHANGELOG | 1 + app/controllers/snippets_controller.rb | 20 +------------------- spec/routing/routing_spec.rb | 8 -------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 558897ad892..6c044192d06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.0 (unreleased) + - Snippets tab under user profile. !4001 (Long Nguyen) - Remove future dates from contribution calendar graph. - Fix error when visiting commit builds page before build was updated - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2daceed039b..f0bd842ca56 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -10,29 +10,11 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :user_index, :show, :raw] + skip_before_action :authenticate_user!, only: [:show, :raw] layout 'snippets' respond_to :html - def index - if params[:username].present? - @user = User.find_by(username: params[:username]) - - render_404 and return unless @user - - @snippets = SnippetsFinder.new.execute(current_user, { - filter: :by_user, - user: @user, - scope: params[:scope] }). - page(params[:page]) - - render 'index' - else - redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path) - end - end - def new @snippet = PersonalSnippet.new end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1527eddfa48..9deffd0a1e3 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -27,18 +27,10 @@ end # PUT /snippets/:id(.:format) snippets#update # DELETE /snippets/:id(.:format) snippets#destroy describe SnippetsController, "routing" do - it "to #user_index" do - expect(get("/s/User")).to route_to('snippets#index', username: 'User') - end - it "to #raw" do expect(get("/snippets/1/raw")).to route_to('snippets#raw', id: '1') end - it "to #index" do - expect(get("/snippets")).to route_to('snippets#index') - end - it "to #create" do expect(post("/snippets")).to route_to('snippets#create') end From ae29ec31e4f71d722e975bfce945aaed7e0d0bd1 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Thu, 5 May 2016 14:57:34 +0700 Subject: [PATCH 0086/1306] Remove unused view and update redirect when destroy snippet --- app/controllers/snippets_controller.rb | 2 +- app/views/snippets/index.html.haml | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 app/views/snippets/index.html.haml diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index f0bd842ca56..2c038bdfda5 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -43,7 +43,7 @@ class SnippetsController < ApplicationController @snippet.destroy - redirect_to snippets_path + redirect_to dashboard_snippets_path end def raw diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml deleted file mode 100644 index 7e4918a6085..00000000000 --- a/app/views/snippets/index.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- page_title "By #{@user.name}", "Snippets" - -%ol.breadcrumb - %li - = link_to snippets_path do - Snippets - %li - = @user.name - .pull-right.hidden-xs - = link_to user_path(@user) do - #{@user.name} profile page - -= render 'snippets' From e449a6c05c734ff9e38e13013591d07d03c0fff6 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 4 May 2016 13:14:11 +0200 Subject: [PATCH 0087/1306] Added documentation on how to instrument methods [ci skip] --- doc/development/instrumentation.md | 129 +++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 6 deletions(-) diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md index c1cf2e77c26..9168c70945a 100644 --- a/doc/development/instrumentation.md +++ b/doc/development/instrumentation.md @@ -1,12 +1,125 @@ # Instrumenting Ruby Code -GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby -code. This can be used to measure the time spent in a specific part of a larger -chunk of code. The resulting data is stored as a field in the transaction that -executed the block. +GitLab Performance Monitoring allows instrumenting of both methods and custom +blocks of Ruby code. Method instrumentation is the primary form of +instrumentation with block-based instrumentation only being used when we want to +drill down to specific regions of code within a method. -To start measuring a block of Ruby code you should use `Gitlab::Metrics.measure` -and give it a name: +## Instrumenting Methods + +Instrumenting methods is done by using the `Gitlab::Metrics::Instrumentation` +module. This module offers a few different methods that can be used to +instrument code: + +* `instrument_method`: instruments a single class method. +* `instrument_instance_method`: instruments a single instance method. +* `instrument_class_hierarchy`: given a Class this method will recursively + instrument all sub-classes (both class and instance methods). +* `instrument_methods`: instruments all public class methods of a Module. +* `instrument_instance_methods`: instruments all public instance methods of a + Module. + +To remove the need for typing the full `Gitlab::Metrics::Instrumentation` +namespace you can use the `configure` class method. This method simply yields +the supplied block while passing `Gitlab::Metrics::Instrumentation` as its +argument. An example: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_method(Foo, :bar) + conf.instrument_method(Foo, :baz) +end +``` + +Using this method is in general preferred over directly calling the various +instrumentation methods. + +Method instrumentation should be added in the initializer +`config/initializers/metrics.rb`. + +### Examples + +Instrumenting a single method: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_method(User, :find_by) +end +``` + +Instrumenting an entire class hierarchy: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_class_hierarchy(ActiveRecord::Base) +end +``` + +Instrumenting all public class methods: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_methods(User) +end +``` + +### Checking Instrumented Methods + +The easiest way to check if a method has been instrumented is to check its +source location. For example: + +``` +method = Rugged::TagCollection.instance_method(:[]) + +method.source_location +``` + +If the source location points to `lib/gitlab/metrics/instrumentation.rb` you +know the method has been instrumented. + +If you're using Pry you can use the `$` command to display the source code of a +method (along with its source location), this is easier than running the above +Ruby code. In case of the above snippet you'd run the following: + +``` +$ Rugged::TagCollection#[] +``` + +This will print out something along the lines of: + +``` +From: /path/to/your/gitlab/lib/gitlab/metrics/instrumentation.rb @ line 148: +Owner: # +Visibility: public +Number of lines: 21 + +def #{name}(#{args_signature}) + trans = Gitlab::Metrics::Instrumentation.transaction + + if trans + start = Time.now + retval = super + duration = (Time.now - start) * 1000.0 + + if duration >= Gitlab::Metrics.method_call_threshold + trans.increment(:method_duration, duration) + + trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, + { duration: duration }, + method: #{label.inspect}) + end + + retval + else + super + end +end +``` + +## Instrumenting Ruby Blocks + +Measuring blocks of Ruby code is done by calling `Gitlab::Metrics.measure` and +passing it a block. For example: ```ruby Gitlab::Metrics.measure(:foo) do @@ -14,6 +127,10 @@ Gitlab::Metrics.measure(:foo) do end ``` +The block is executed and the execution time is stored as a set of fields in the +currently running transaction. If no transaction is present the block is yielded +without measuring anything. + 3 values are measured for a block: 1. The real time elapsed, stored in NAME_real_time. From d028863eda8b97f6e4db129ef57f0d3a2130c9b3 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 4 May 2016 18:21:57 -0300 Subject: [PATCH 0088/1306] Sanitize milestones and label titles --- app/models/label.rb | 5 +++++ app/models/milestone.rb | 5 +++++ spec/lib/banzai/filter/milestone_reference_filter_spec.rb | 2 +- spec/models/label_spec.rb | 8 ++++++++ spec/models/milestone_spec.rb | 8 ++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/models/label.rb b/app/models/label.rb index 60bdce32952..0b34911a4e9 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -117,6 +117,11 @@ class Label < ActiveRecord::Base LabelsHelper::text_color_for_bg(self.color) end + def title= value + value = Sanitize.clean(value.to_s) if value + write_attribute(:title, Sanitize.clean(value)) + end + private def label_format_reference(format = :id) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 986184dd301..ed81791c69c 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -129,6 +129,11 @@ class Milestone < ActiveRecord::Base nil end + def title= value + value = Sanitize.clean(value.to_s) if value + write_attribute(:title, value) + end + # Sorts the issues for the given IDs. # # This method runs a single SQL query using a CASE statement to update the diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index ebf3d7489b5..5beb61dac5c 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -43,7 +43,7 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do milestone.update_attribute(:title, %{">whateverwhatever" end it 'includes default classes' do diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 0614ca1e7c9..b61c55a3f6d 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -55,6 +55,14 @@ describe Label, models: true do end end + describe "#title" do + let(:label) { create(:label, title: "test") } + + it "sanitizes title" do + expect(label.title).to eq("test") + end + end + describe '#to_reference' do context 'using id' do it 'returns a String reference to the object' do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 72a4ea70228..e2c89a4b3e6 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -34,6 +34,14 @@ describe Milestone, models: true do let(:issue) { create(:issue) } let(:user) { create(:user) } + describe "#title" do + let(:milestone) { create(:milestone, title: "test") } + + it "sanitizes title" do + expect(milestone.title).to eq("test") + end + end + describe "unique milestone title per project" do it "shouldn't accept the same title in a project twice" do new_milestone = Milestone.new(project: milestone.project, title: milestone.title) 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 0089/1306] 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 00000000000..9cd522d1c30 --- /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 38214f04793..86ba40facc5 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 3e0cfc6aa65..9117f9242cd 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 707a0d0e5c6..9000e0adf63 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 3768efe142a..85a987c2cb2 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 4cb8adcebad..38de0b442ca 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 00000000000..44a9b55a8a6 --- /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 00000000000..b4e3e9eb3dd --- /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 afa2ca039ae..6af76c97cd3 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 e410febdfff..2cb3e8b0176 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 87ced65c650..b992b2e76f0 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 031315debd7..52f2904f450 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 2bb312bb252..c5be21ba897 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 e818f58d13c..c1bf46bdfb3 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 42ec1ac9e1a..703636658b7 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 42c5bca90fd..da1b77c0f9e 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 00000000000..b77b4e79bf2 --- /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 00000000000..63c953195fe --- /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 3443a8e2307..97401a2e618 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 7a8009f6da4..4aa92d0b39e 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 5fe5ddc0819..c4cdd4b3d43 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 e740fe8c84d..391193eed6c 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 2c34f9c454b..e8cda51e759 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 dc249155b92..8692c1cccee 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 9e8b0131f8f..3d1a41a4652 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 46a25262844..ecde83d8547 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 00000000000..3177b86a133 --- /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 00000000000..76f4a3aa6ae --- /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 00000000000..da16372a297 --- /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 42c261003bb..354d7390a5b 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 60b9f5e0ece..b3769ba9c2d 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 5f8ff01b0a9..00000000000 --- 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 00000000000..0ae220a86bc --- /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 465531b2b36..82b25702172 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 00000000000..a1173834b29 --- /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 e5dcb159014..2bfc5effd78 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 543593cf389..2d4d9c18c9d 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 88c22912950..4e6c04a11b9 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 00000000000..fd3712b7d43 --- /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 b16ccc6e305..d5435916ea1 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 6b18936edb1..bb591e9cb53 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 0090/1306] 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 af4462ece38..4c0a274b793 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 = $("