From e3f8ed5cefcae3df9179f7b1198bf8026918653e Mon Sep 17 00:00:00 2001 From: Felix Ruess Date: Thu, 4 Feb 2016 17:51:12 +0000 Subject: [PATCH 001/449] 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 b59e92cb31..596478bf3c 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 002/449] 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 cc6c6b7cb2..ddfa298f79 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 003/449] Change deprecated usage of rendering without response body `render nothing: true` has been deprecated. For more information see [pr](https://github.com/rails/rails/pull/20336) --- app/controllers/admin/abuse_reports_controller.rb | 2 +- app/controllers/admin/broadcast_messages_controller.rb | 2 +- app/controllers/admin/keys_controller.rb | 2 +- app/controllers/admin/spam_logs_controller.rb | 2 +- app/controllers/admin/users_controller.rb | 2 +- app/controllers/concerns/toggle_subscription_action.rb | 2 +- app/controllers/dashboard/todos_controller.rb | 4 ++-- app/controllers/groups/group_members_controller.rb | 2 +- app/controllers/profiles/emails_controller.rb | 2 +- app/controllers/profiles/keys_controller.rb | 2 +- app/controllers/projects/milestones_controller.rb | 2 +- app/controllers/projects/notes_controller.rb | 4 ++-- app/controllers/projects/project_members_controller.rb | 4 ++-- app/controllers/projects/protected_branches_controller.rb | 2 +- spec/controllers/projects/raw_controller_spec.rb | 2 +- 15 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index 2463cfa87b..76fc10bcc1 100644 --- a/app/controllers/admin/abuse_reports_controller.rb +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -9,6 +9,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController abuse_report.remove_user if params[:remove_user] abuse_report.destroy - render nothing: true + head :ok end end diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index fc34292498..82055006ac 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -32,7 +32,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_default(default: { action: 'index' }) } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb index cb33fdd976..054bb52b69 100644 --- a/app/controllers/admin/keys_controller.rb +++ b/app/controllers/admin/keys_controller.rb @@ -6,7 +6,7 @@ class Admin::KeysController < Admin::ApplicationController respond_to do |format| format.html - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index 377e9741e5..3a2f018531 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -11,7 +11,7 @@ class Admin::SpamLogsController < Admin::ApplicationController redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed." else spam_log.destroy - render nothing: true + head :ok end end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 87f4fb455b..39c0c22f9b 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -135,7 +135,7 @@ class Admin::UsersController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb index 8a43c0b93c..9e3b9be2ff 100644 --- a/app/controllers/concerns/toggle_subscription_action.rb +++ b/app/controllers/concerns/toggle_subscription_action.rb @@ -6,7 +6,7 @@ module ToggleSubscriptionAction subscribable_resource.toggle_subscription(current_user) - render nothing: true + head :ok end private diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 43cf8fa71a..d8ba51294c 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -10,7 +10,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' } - format.js { render nothing: true } + format.js { head :ok } end end @@ -19,7 +19,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 0e902c4bb4..68f7012089 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -43,7 +43,7 @@ class Groups::GroupMembersController < Groups::ApplicationController respond_to do |format| format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 0ede9b8e21..1c24c4db99 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -24,7 +24,7 @@ class Profiles::EmailsController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_emails_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index b88c080352..9906493666 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -27,7 +27,7 @@ class Profiles::KeysController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_keys_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index da46731d94..6579f4f8c8 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -68,7 +68,7 @@ class Projects::MilestonesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_milestones_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 1b9dd56804..d91ab1cee1 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -44,7 +44,7 @@ class Projects::NotesController < Projects::ApplicationController end respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end @@ -53,7 +53,7 @@ class Projects::NotesController < Projects::ApplicationController note.update_attribute(:attachment, nil) respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e7bddc4a6f..b150e9ef02 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -55,7 +55,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController format.html do redirect_to namespace_project_project_members_path(@project.namespace, @project) end - format.js { render nothing: true } + format.js { head :ok } end end @@ -81,7 +81,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController respond_to do |format| format.html { redirect_to dashboard_projects_path, notice: "You left the project." } - format.js { render nothing: true } + format.js { head :ok } end else if current_user == @project.owner diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index e49259c34b..efa7bf14d0 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -39,7 +39,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_protected_branches_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 1caa476d37..fb29274c68 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -42,7 +42,7 @@ describe Projects::RawController do before do public_project.lfs_objects << lfs_object allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) - allow(controller).to receive(:send_file) { controller.render nothing: true } + allow(controller).to receive(:send_file) { controller.head :ok } end it 'serves the file' do From 1824fb0603c798ec467ea3529570031e7dbb2986 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 5 Apr 2016 15:20:58 +0000 Subject: [PATCH 004/449] Fix broken link in CI quickstart docs The space between the [label] and the (link) caused it to be interpreted literally. --- doc/ci/quick_start/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 9aba4326e1..aae9ccae1d 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -212,8 +212,8 @@ If you want to receive e-mail notifications about the result status of the builds, you should explicitly enable the **Builds Emails** service under your project's settings. -For more information read the [Builds emails service documentation] -(../../project_services/builds_emails.md). +For more information read the +[Builds emails service documentation](../../project_services/builds_emails.md). ## Builds badge From e4d9d4e55b67ab04e46ee3f72c5496f91687a205 Mon Sep 17 00:00:00 2001 From: "P.S.V.R" Date: Mon, 11 Apr 2016 16:45:00 +0800 Subject: [PATCH 005/449] 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 2b0bee2099..531dc9a5ae 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 05a1852cdb..fa602cbe55 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 006/449] 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 e69de29bb2..764c8cc9cc 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 c0bf6def7c..ec5ac54c27 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 86b46e8c75..fcce1b1dc9 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 e69de29bb2..29efcc9cfd 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 e69de29bb2..27c125ca40 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 e69de29bb2..0347c22038 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 e69de29bb2..e9a22bbb15 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 842fbb9984..841b3f2627 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 007/449] 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 75b770ae5a..3a7f5bb932 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 764c8cc9cc..a3e72fbdef 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 effa7ce77e..3f7282d0c6 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 f1af8e163c..ed0db04e06 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 8865bd76bd..687654d3c8 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 fcce1b1dc9..b58d827023 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 29efcc9cfd..7c6ba21638 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 27c125ca40..0000000000 --- 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 0000000000..faf63d64a7 --- /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 0347c22038..b9877cd37b 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 e9a22bbb15..39b1571b9c 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 1688e91ca6..59df2c5cb8 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 008/449] 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 3a7f5bb932..9d6a6c5b23 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 a3e72fbdef..b2ee5573bf 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 687654d3c8..7991b987e3 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 218d396b89..7ded4828b2 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 7c6ba21638..32f85cb8f8 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 06520e40bd..582ce61a64 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 b9877cd37b..838b2986d4 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 0000000000..9f33e2ad62 --- /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 841b3f2627..6384757835 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 009/449] 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 9d6a6c5b23..75b770ae5a 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 b58d827023..f4797a85bb 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 2ec3c809e1..cf101acbb5 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 010/449] 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 417050b413..acc01b008b 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 f7c8647ac0..b231b584ea 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 d6b38b327f..e953353567 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 bdfa0c7009..5f9d291998 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 391193eed6..7bfde8b1c5 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 ab8b022411..9ef021747a 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 011/449] 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 b2ee5573bf..aba64e4a73 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 cf101acbb5..782ea341da 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 0908e830f8..9cb14b6a90 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 9f33e2ad62..8a2e14d8d8 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 012/449] 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 085ecc6951..c0b334d360 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 e2bf4d6254..00a95dd05b 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 fd86d2f755..8190b2a20c 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 03e5873ae52f3c8c0efb7baa7d1a358a7c3e7974 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 19 Apr 2016 19:04:58 +0300 Subject: [PATCH 013/449] 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 9f7c1bfe6a..1848f6319d 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 4f199b6af6..3f44a1b4c6 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 014/449] 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 986184dd30..39dc8d8961 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 4cb8217802..2c90fd4d38 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 1772cc3f6a..6d3bf810c2 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 b87cd6bbca..7fc6d6fcc5 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 43cb6ef43f..492138716a 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 015/449] 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 39dc8d8961..50fa95d4d4 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 2c90fd4d38..419532717f 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 ebf3d7489b..26f87286b2 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 016/449] 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 419532717f..556087c488 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 6d3bf810c2..3e777a5e92 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 492138716a..d921f9bb2b 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 017/449] 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 556087c488..aea1abf3b8 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 26f87286b2..ac3e6e4e53 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 018/449] 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 61e3f811e7..54d89ef69a 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 3768efe142..8662de712a 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 ba50305dbd..eec38c5c3d 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 019/449] 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 50fa95d4d4..92c07fd20d 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 020/449] 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 54d89ef69a..0f2c5a6241 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 021/449] 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 eec38c5c3d..eb73948006 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 022/449] 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 0f2c5a6241..41dba34210 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 023/449] 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 aea1abf3b8..746e768061 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 ac3e6e4e53..bdf48eabb0 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 024/449] 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 4f199b6af6..1afa1f1406 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 025/449] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d4b8a50926..b35cd9585d 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 026/449] Update cas.md to reflect the current syntax, and added that gitlab-ctl reconfigure should be run. --- doc/integration/cas.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/doc/integration/cas.md b/doc/integration/cas.md index e6b2071f19..e34e306f9a 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -27,17 +27,18 @@ To enable the CAS OmniAuth provider you must register your application with your ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "cas3", - label: "cas", - args: { - url: 'CAS_SERVER', - login_url: '/CAS_PATH/login', - service_validate_url: '/CAS_PATH/p3/serviceValidate', - logout_url: '/CAS_PATH/logout'} } - } + "name"=> "cas3", + "label"=> "cas", + "args"=> { + "url"=> 'CAS_SERVER', + "login_url"=> '/CAS_PATH/login', + "service_validate_url"=> '/CAS_PATH/p3/serviceValidate', + "logout_url"=> '/CAS_PATH/logout' + } } ] ``` + For installations from source: @@ -57,6 +58,8 @@ To enable the CAS OmniAuth provider you must register your application with your 1. Save the configuration file. +1. Run `gitlab-ctl reconfigure` for the omnibus package. + 1. Restart GitLab for the changes to take effect. On the sign in page there should now be a CAS tab in the sign in form. From ab1734f9e1e3f07482185c8a4cb168be463fcff5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 23 Apr 2016 12:27:29 +0200 Subject: [PATCH 027/449] Move changelog item --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f81435805d..b1df9145d9 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 028/449] 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 746e768061..dad0768f51 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 c6f19aed51736e5945283a611eae09f32a9b5aeb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 26 Apr 2016 16:01:00 +0200 Subject: [PATCH 029/449] 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 782ea341da..21a3008086 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 030/449] Added authentication service for docker registry --- Gemfile | 1 + Gemfile.lock | 1 + app/models/ability.rb | 8 +- app/models/ci/build.rb | 1 + app/models/project.rb | 5 + config/initializers/1_settings.rb | 39 ++++ ...07120251_add_images_enabled_for_project.rb | 5 + db/schema.rb | 1 + lib/api/api.rb | 1 + lib/api/auth.rb | 166 ++++++++++++++++++ 10 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20160407120251_add_images_enabled_for_project.rb create mode 100644 lib/api/auth.rb diff --git a/Gemfile b/Gemfile index 7882e467f8..512c6babd7 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' gem 'rack-oauth2', '~> 1.2.1' +gem 'jwt' # Spam and anti-bot protection gem 'recaptcha', require: 'recaptcha/rails' diff --git a/Gemfile.lock b/Gemfile.lock index 1dcda0daff..2b578429b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -957,6 +957,7 @@ DEPENDENCIES jquery-scrollto-rails (~> 1.4.3) jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 5.0.0) + jwt kaminari (~> 0.16.3) letter_opener_web (~> 1.3.0) licensee (~> 8.0.0) diff --git a/app/models/ability.rb b/app/models/ability.rb index 6103a2947e..ba27b9a9b1 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -203,6 +203,7 @@ class Ability :admin_label, :read_commit_status, :read_build, + :read_image, ] end @@ -216,7 +217,9 @@ class Ability :update_build, :create_merge_request, :create_wiki, - :push_code + :push_code, + :create_image, + :update_image, ] end @@ -242,7 +245,8 @@ class Ability :admin_wiki, :admin_project, :admin_commit_status, - :admin_build + :admin_build, + :admin_image ] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 553cd44797..c2ddee527e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -426,6 +426,7 @@ module Ci variables << { key: :CI_BUILD_NAME, value: name, public: true } variables << { key: :CI_BUILD_STAGE, value: stage, public: true } variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request + variables << { key: :CI_DOCKER_REGISTRY, value: project.registry_repository_url, public: true } if project.registry_repository_url variables end end diff --git a/app/models/project.rb b/app/models/project.rb index 5c6c36e6b3..76265a59ea 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -64,6 +64,7 @@ class Project < ActiveRecord::Base default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets + default_value_for :images_enabled, gitlab_config_features.images default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at @@ -369,6 +370,10 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end + def registry_repository_url + "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" if images_enabled? && Gitlab.config.registry.enabled + end + def commit(id = 'HEAD') repository.commit(id) end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8db2c05fe4..01ee8a0d52 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -27,6 +27,30 @@ class Settings < Settingslogic ].join('') end + def build_registry_api_url + if registry.port.to_i == (registry.https ? 443 : 80) + custom_port = nil + else + custom_port = ":#{registry.port}" + end + [ registry.protocol, + "://", + registry.internal_host, + custom_port + ].join('') + end + + def build_registry_host_with_port + if registry.port.to_i == (registry.https ? 443 : 80) + custom_port = nil + else + custom_port = ":#{registry.port}" + end + [ registry.host, + custom_port + ].join('') + end + def build_gitlab_shell_ssh_path_prefix user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" @@ -211,6 +235,7 @@ Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.g Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? +Settings.gitlab.default_projects_features['images'] = true if Settings.gitlab.default_projects_features['images'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] @@ -242,6 +267,20 @@ Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) Settings.artifacts['max_size'] ||= 100 # in megabytes +# +# Registry +# +Settings['registry'] ||= Settingslogic.new({}) +Settings.registry['registry'] = false if Settings.registry['enabled'].nil? +Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) +Settings.registry['host'] ||= "example.com" +Settings.registry['internal_host']||= "localhost" +Settings.registry['https'] = false if Settings.registry['https'].nil? +Settings.registry['port'] ||= Settings.registry.https ? 443 : 80 +Settings.registry['protocol'] ||= Settings.registry.https ? "https" : "http" +Settings.registry['api_url'] ||= Settings.send(:build_registry_api_url) +Settings.registry['host_port'] ||= Settings.send(:build_registry_host_with_port) + # # Git LFS # diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb new file mode 100644 index 0000000000..6a221a7fb0 --- /dev/null +++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb @@ -0,0 +1,5 @@ +class AddImagesEnabledForProject < ActiveRecord::Migration + def change + add_column :projects, :images_enabled, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 42457d9235..bf46028d23 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -760,6 +760,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" + t.boolean "images_enabled" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree diff --git a/lib/api/api.rb b/lib/api/api.rb index cc1004f800..6ddfe11d98 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -58,5 +58,6 @@ module API mount Variables mount Runners mount Licenses + mount Auth end end diff --git a/lib/api/auth.rb b/lib/api/auth.rb new file mode 100644 index 0000000000..b992e49730 --- /dev/null +++ b/lib/api/auth.rb @@ -0,0 +1,166 @@ +module API + # Projects builds API + class Auth < Grape::API + namespace 'auth' do + get 'token' do + required_attributes! [:scope, :service] + keys = attributes_for_keys [:scope, :service] + + case keys[:service] + when 'docker' + docker_token_auth(keys[:scope]) + else + not_found! + end + end + end + + helpers do + def docker_token_auth(scope) + @type, @path, actions = scope.split(':', 3) + bad_request!("invalid type: #{type}") unless type == 'repository' + + @actions = actions.split(',') + bad_request!('missing actions') if @actions.empty? + + @project = Project.find_with_namespace(path) + not_found!('Project') unless @project + + auth! + + authorize_actions!(@actions) + + { token: encode(docker_payload) } + end + + def auth! + auth = BasicRequest.new(request.env) + return unless auth.provided? + + return bad_request unless auth.basic? + + # Authentication with username and password + login, password = auth.credentials + + if ci_request?(login, password) + @ci = true + return + end + + @user = authenticate_user(login, password) + + if @user + request.env['REMOTE_USER'] = @auth.username + end + end + + def ci_request?(login, password) + matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) + + if @project && matched_login.present? + underscored_service = matched_login['s'].underscore + + if underscored_service == 'gitlab_ci' + return @project.valid_build_token?(password) + end + end + + false + end + + def authenticate_user(login, password) + user = Gitlab::Auth.new.find(login, password) + + unless user + user = oauth_access_token_check(login, password) + end + + # If the user authenticated successfully, we reset the auth failure count + # from Rack::Attack for that IP. A client may attempt to authenticate + # with a username and blank password first, and only after it receives + # a 401 error does it present a password. Resetting the count prevents + # false positives from occurring. + # + # Otherwise, we let Rack::Attack know there was a failed authentication + # attempt from this IP. This information is stored in the Rails cache + # (Redis) and will be used by the Rack::Attack middleware to decide + # whether to block requests from this IP. + config = Gitlab.config.rack_attack.git_basic_auth + + if config.enabled + if user + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(@request.ip, config) + else + banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(@request.ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{@request.ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end + end + + user + end + + def docker_payload + { + access: [ + type: @type, + name: @path, + actions: @actions + ], + exp: Time.now.to_i + 3600 + } + end + + def private_key + @private_key ||= OpenSSL::PKey::RSA.new File.read 'config/registry.key' + end + + def encode(payload) + JWT.encode(payload, private_key, 'RS256') + end + + def authorize_actions!(actions) + actions.each do |action| + forbidden! unless can_access?(action) + end + end + + def can_access?(action) + case action + when 'pull' + @ci || can?(@user, :download_code, @project) + when 'push' + @ci || can?(@user, :push_code, @project) + else + false + end + end + + class BasicRequest < Rack::Auth::AbstractRequest + def basic? + "basic" == scheme + end + + def credentials + @credentials ||= params.unpack("m*").first.split(/:/, 2) + end + + def username + credentials.first + end + end + end + end +end From 03b3fe13f6af67f8117cf4322b605630f55f3136 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Apr 2016 08:23:17 -0400 Subject: [PATCH 031/449] Make images_enabled configurable --- app/controllers/projects_controller.rb | 3 ++- app/views/projects/edit.html.haml | 10 ++++++++++ lib/api/entities.rb | 3 ++- lib/api/projects.rb | 5 +++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3768efe142..52f7b99334 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -235,7 +235,8 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, + :issues_enabled, :merge_requests_enabled, :snippets_enabled, :images_enabled, + :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :public_builds, diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 76a4f41193..5c7960031e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -84,6 +84,16 @@ %br %span.descr Share code pastes with others out of git repository + - if Gitlab.config.registry.enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :images_enabled do + = f.check_box :images_enabled + %strong Images + %br + %span.descr Use Docker Registry for this repository + = render 'builds_settings', f: f %fieldset.features diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 716ca6f7ed..95c3597b03 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -66,7 +66,8 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :images_enabled + expose :created_at, :last_activity_at expose :shared_runners_enabled expose :creator_id expose :namespace diff --git a/lib/api/projects.rb b/lib/api/projects.rb index cc2c7a0c50..6f85bc4b1b 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -94,6 +94,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # images_enabled (optional) # shared_runners_enabled (optional) # namespace_id (optional) - defaults to user namespace # public (optional) - if true same as setting visibility_level = 20 @@ -112,6 +113,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :images_enabled, :shared_runners_enabled, :namespace_id, :public, @@ -143,6 +145,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # images_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) @@ -206,6 +209,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # images_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project @@ -222,6 +226,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :images_enabled, :shared_runners_enabled, :public, :visibility_level, From 0ca8db25f008cd3bc4f2df0f58efd739718323d0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Apr 2016 10:55:10 -0400 Subject: [PATCH 032/449] Try to fix auth service --- lib/api/auth.rb | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/api/auth.rb b/lib/api/auth.rb index b992e49730..ec944b1dc8 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -3,12 +3,12 @@ module API class Auth < Grape::API namespace 'auth' do get 'token' do - required_attributes! [:scope, :service] - keys = attributes_for_keys [:scope, :service] + required_attributes! [:service] + keys = attributes_for_keys [:offline_token, :scope, :service] case keys[:service] when 'docker' - docker_token_auth(keys[:scope]) + docker_token_auth(keys[:scope], keys[:offline_token]) else not_found! end @@ -16,19 +16,23 @@ module API end helpers do - def docker_token_auth(scope) - @type, @path, actions = scope.split(':', 3) - bad_request!("invalid type: #{type}") unless type == 'repository' - - @actions = actions.split(',') - bad_request!('missing actions') if @actions.empty? - - @project = Project.find_with_namespace(path) - not_found!('Project') unless @project - + def docker_token_auth(scope, offline_token) auth! - authorize_actions!(@actions) + if offline_token + forbidden! unless @user + elsif scope + @type, @path, actions = scope.split(':', 3) + bad_request!("invalid type: #{@type}") unless @type == 'repository' + + @actions = actions.split(',') + bad_request!('missing actions') if @actions.empty? + + @project = Project.find_with_namespace(@path) + not_found!('Project') unless @project + + authorize_actions!(@actions) + end { token: encode(docker_payload) } end @@ -50,7 +54,7 @@ module API @user = authenticate_user(login, password) if @user - request.env['REMOTE_USER'] = @auth.username + request.env['REMOTE_USER'] = @user.username end end @@ -71,10 +75,6 @@ module API def authenticate_user(login, password) user = Gitlab::Auth.new.find(login, password) - unless user - user = oauth_access_token_check(login, password) - end - # If the user authenticated successfully, we reset the auth failure count # from Rack::Attack for that IP. A client may attempt to authenticate # with a username and blank password first, and only after it receives From 72611f9cfa9014653c0894115af6223687c2eab4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Apr 2016 13:57:35 -0400 Subject: [PATCH 033/449] Auth token --- config/gitlab.yml.example | 10 ++++++++++ config/initializers/1_settings.rb | 1 + lib/api/auth.rb | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 07ce4b6d71..e55ca6f9c6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -176,6 +176,16 @@ production: &base repository_archive_cache_worker: cron: "0 * * * *" + registry: + # enabled: true + # host: localhost + # port: 5000 + # https: false + # internal_host: localhost + # key: config/registry.key + # issuer: omnibus-certificate + # path: shared/registry + # # 2. GitLab CI settings # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 01ee8a0d52..b94f3f2f90 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -275,6 +275,7 @@ Settings.registry['registry'] = false if Settings.registry['enabled'].nil? Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) Settings.registry['host'] ||= "example.com" Settings.registry['internal_host']||= "localhost" +Settings.registry['key'] ||= nil Settings.registry['https'] = false if Settings.registry['https'].nil? Settings.registry['port'] ||= Settings.registry.https ? 443 : 80 Settings.registry['protocol'] ||= Settings.registry.https ? "https" : "http" diff --git a/lib/api/auth.rb b/lib/api/auth.rb index ec944b1dc8..d769c69275 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -119,12 +119,13 @@ module API name: @path, actions: @actions ], + iss: Gitlab.config.registry.issuer, exp: Time.now.to_i + 3600 } end def private_key - @private_key ||= OpenSSL::PKey::RSA.new File.read 'config/registry.key' + @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key end def encode(payload) From 8aac802eaf417a4f484f099089410934cdfdb0b7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Apr 2016 14:16:17 -0400 Subject: [PATCH 034/449] Audience --- lib/api/auth.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/api/auth.rb b/lib/api/auth.rb index d769c69275..e4ce9bf122 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -120,6 +120,7 @@ module API actions: @actions ], iss: Gitlab.config.registry.issuer, + aud: "docker", exp: Time.now.to_i + 3600 } end From 5fc310b440a7bb3ead91760ac2b7cbb1cee72f2a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 21 Apr 2016 10:02:24 +0200 Subject: [PATCH 035/449] Missing parameters of docker payload --- lib/api/auth.rb | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/api/auth.rb b/lib/api/auth.rb index e4ce9bf122..dab04bca81 100644 --- a/lib/api/auth.rb +++ b/lib/api/auth.rb @@ -113,6 +113,7 @@ module API end def docker_payload + issued_at = Time.now { access: [ type: @type, @@ -121,8 +122,14 @@ module API ], iss: Gitlab.config.registry.issuer, aud: "docker", + sub: @user.try(:username), + aud: @service, + iat: issued_at, + nbf: issued_at - 5.seconds, + exp: issued_at + 60.minutes, + jti: SecureRandom.uuid, exp: Time.now.to_i + 3600 - } + }.compact end def private_key @@ -130,7 +137,10 @@ module API end def encode(payload) - JWT.encode(payload, private_key, 'RS256') + headers = { + kid: kid(private_key) + } + JWT.encode(payload, private_key, 'RS256', headers) end def authorize_actions!(actions) @@ -150,6 +160,15 @@ module API end end + def kid(private_key) + sha256 = Digest::SHA256.new + sha256.update(private_key.public_key.to_der) + payload = StringIO.new(sha256.digest).read(30) + Base32.encode(payload).split("").each_slice(4).each_with_object([]) do |slice, mem| + mem << slice.join + end.join(":") + end + class BasicRequest < Rack::Auth::AbstractRequest def basic? "basic" == scheme From 0a280158efeb7f681589ae7af24f0ed9052de809 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 15 Apr 2016 19:23:33 +0530 Subject: [PATCH 036/449] Eager load `lib/api` - So that the server doesn't have to be restarted for every change in dev. --- config/application.rb | 2 + config/routes.rb | 1 - lib/api/api.rb | 4 +- lib/api/api_guard.rb | 316 +++++++++++++++++++++--------------------- lib/ci/api/api.rb | 2 +- 5 files changed, 163 insertions(+), 162 deletions(-) diff --git a/config/application.rb b/config/application.rb index 2e2ed48db0..abe22691ad 100644 --- a/config/application.rb +++ b/config/application.rb @@ -79,6 +79,8 @@ module Gitlab # This is needed for gitlab-shell ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH'] + config.eager_load_paths += ["#{Rails.root}/lib"] + config.generators do |g| g.factory_girl false end diff --git a/config/routes.rb b/config/routes.rb index 5ce1f49ec6..adf4bb18b3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ require 'sidekiq/web' require 'sidekiq/cron/web' -require 'api/api' Rails.application.routes.draw do if Gitlab::Sherlock.enabled? diff --git a/lib/api/api.rb b/lib/api/api.rb index 6ddfe11d98..d41b4b7186 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,8 +1,6 @@ -Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} - module API class API < Grape::API - include APIGuard + include ::API::APIGuard version 'v3', using: :path rescue_from ActiveRecord::RecordNotFound do diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index b9994fcefd..6dfd6e4396 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -2,171 +2,173 @@ require 'rack/oauth2' -module APIGuard - extend ActiveSupport::Concern +module API + module APIGuard + extend ActiveSupport::Concern - included do |base| - # OAuth2 Resource Server Authentication - use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| - # The authenticator only fetches the raw token string + included do |base| + # OAuth2 Resource Server Authentication + use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| + # The authenticator only fetches the raw token string - # Must yield access token to store it in the env - request.access_token - end - - helpers HelperMethods - - install_error_responders(base) - end - - # Helper Methods for Grape Endpoint - module HelperMethods - # Invokes the doorkeeper guard. - # - # If token is presented and valid, then it sets @current_user. - # - # If the token does not have sufficient scopes to cover the requred scopes, - # then it raises InsufficientScopeError. - # - # If the token is expired, then it raises ExpiredError. - # - # If the token is revoked, then it raises RevokedError. - # - # If the token is not found (nil), then it raises TokenNotFoundError. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def doorkeeper_guard!(scopes: []) - if (access_token = find_access_token).nil? - raise TokenNotFoundError - - else - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end + # Must yield access token to store it in the env + request.access_token end + + helpers HelperMethods + + install_error_responders(base) end - def doorkeeper_guard(scopes: []) - if access_token = find_access_token - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) + # Helper Methods for Grape Endpoint + module HelperMethods + # Invokes the doorkeeper guard. + # + # If token is presented and valid, then it sets @current_user. + # + # If the token does not have sufficient scopes to cover the requred scopes, + # then it raises InsufficientScopeError. + # + # If the token is expired, then it raises ExpiredError. + # + # If the token is revoked, then it raises RevokedError. + # + # If the token is not found (nil), then it raises TokenNotFoundError. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def doorkeeper_guard!(scopes: []) + if (access_token = find_access_token).nil? + raise TokenNotFoundError - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end - end - end - - def current_user - @current_user - end - - private - def find_access_token - @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) - end - - def doorkeeper_request - @doorkeeper_request ||= ActionDispatch::Request.new(env) - end - - def validate_access_token(access_token, scopes) - Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) - end - end - - module ClassMethods - # Installs the doorkeeper guard on the whole Grape API endpoint. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def guard_all!(scopes: []) - before do - guard! scopes: scopes - end - end - - private - def install_error_responders(base) - error_classes = [ MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] - - base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler - end - - def oauth2_bearer_token_error_handler - Proc.new do |e| - response = - case e - when MissingTokenError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new - - when TokenNotFoundError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Bad Access Token.") - - when ExpiredError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token is expired. You can either do re-authorization or token refresh.") - - when RevokedError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token was revoked. You have to re-authorize from the user.") - - when InsufficientScopeError - # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) - # does not include WWW-Authenticate header, which breaks the standard. - Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( - :insufficient_scope, - Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], - { scope: e.scopes }) + else + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) end + end + end - response.finish + def doorkeeper_guard(scopes: []) + if access_token = find_access_token + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end + + def current_user + @current_user + end + + private + def find_access_token + @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + end + + def doorkeeper_request + @doorkeeper_request ||= ActionDispatch::Request.new(env) + end + + def validate_access_token(access_token, scopes) + Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) + end + end + + module ClassMethods + # Installs the doorkeeper guard on the whole Grape API endpoint. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def guard_all!(scopes: []) + before do + guard! scopes: scopes + end + end + + private + def install_error_responders(base) + error_classes = [ MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] + + base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + end + + def oauth2_bearer_token_error_handler + Proc.new do |e| + response = + case e + when MissingTokenError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new + + when TokenNotFoundError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Bad Access Token.") + + when ExpiredError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token is expired. You can either do re-authorization or token refresh.") + + when RevokedError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token was revoked. You have to re-authorize from the user.") + + when InsufficientScopeError + # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) + # does not include WWW-Authenticate header, which breaks the standard. + Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( + :insufficient_scope, + Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], + { scope: e.scopes }) + end + + response.finish + end + end + end + + # + # Exceptions + # + + class MissingTokenError < StandardError; end + + class TokenNotFoundError < StandardError; end + + class ExpiredError < StandardError; end + + class RevokedError < StandardError; end + + class InsufficientScopeError < StandardError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes end end end - - # - # Exceptions - # - - class MissingTokenError < StandardError; end - - class TokenNotFoundError < StandardError; end - - class ExpiredError < StandardError; end - - class RevokedError < StandardError; end - - class InsufficientScopeError < StandardError - attr_reader :scopes - def initialize(scopes) - @scopes = scopes - end - end -end +end \ No newline at end of file diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 353c4ddebf..7cd8b6fbae 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -3,7 +3,7 @@ Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file} module Ci module API class API < Grape::API - include APIGuard + include ::API::APIGuard version 'v1', using: :path rescue_from ActiveRecord::RecordNotFound do From c0f02aad4a1a178109a235d34bd70218c0aec86c Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Mon, 2 May 2016 16:37:12 +0700 Subject: [PATCH 037/449] Add snippet tab under user profile --- app/assets/javascripts/user_tabs.js.coffee | 9 ++++++++- app/controllers/users_controller.rb | 22 ++++++++++++++++++++++ app/views/users/show.html.haml | 6 ++++++ config/routes.rb | 5 +++-- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee index 09b7eec910..aa798b96ed 100644 --- a/app/assets/javascripts/user_tabs.js.coffee +++ b/app/assets/javascripts/user_tabs.js.coffee @@ -26,6 +26,10 @@ # Personal projects # # +#
  • +# +# +#
  • # # #
    @@ -41,6 +45,9 @@ #
    # Projects content #
    +#
    +# Snippets content +#
    #
    # #
    @@ -100,7 +107,7 @@ class @UserTabs if action is 'activity' @loadActivities(source) - if action in ['groups', 'contributed', 'projects'] + if action in ['groups', 'contributed', 'projects', 'snippets'] @loadTab(source, action) loadTab: (source, action) -> diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2ae180c8a1..799421c185 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -58,6 +58,19 @@ class UsersController < ApplicationController end end + def snippets + load_snippets + + respond_to do |format| + format.html { render 'show' } + format.json do + render json: { + html: view_to_html_string("snippets/_snippets", collection: @snippets) + } + end + end + end + def calendar calendar = contributions_calendar @timestamps = calendar.timestamps @@ -116,6 +129,15 @@ class UsersController < ApplicationController @groups = JoinedGroupsFinder.new(user).execute(current_user) end + def load_snippets + @snippets = SnippetsFinder.new.execute( + current_user, + filter: :by_user, + user: user, + scope: params[:scope] + ).page(params[:page]) + end + def projects_for_current_user ProjectsFinder.new.execute(current_user) end diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 3028491e5b..a453a7fedb 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -81,6 +81,9 @@ %li.projects-tab = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects + %li.snippets-tab + = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + Snippets %div{ class: container_class } .tab-content @@ -104,6 +107,9 @@ #projects.tab-pane - # This tab is always loaded via AJAX + #snippets.tab-pane + - # This tab is always loaded via AJAX + .loading-status = spinner diff --git a/config/routes.rb b/config/routes.rb index 2f820aafed..f6a41331ec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -89,8 +89,6 @@ Rails.application.routes.draw do end end - get '/s/:username' => 'snippets#index', as: :user_snippets, constraints: { username: /.*/ } - # # Invites # @@ -355,6 +353,9 @@ Rails.application.routes.draw do get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects, constraints: { username: /.*/ } + get 'u/:username/snippets' => 'users#snippets', as: :user_snippets, + constraints: { username: /.*/ } + get '/u/:username' => 'users#show', as: :user, constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Mon, 2 May 2016 13:29:17 +0200 Subject: [PATCH 038/449] Added JWT controller --- Gemfile | 1 + Gemfile.lock | 2 + app/controllers/jwt_controller.rb | 173 ++++++++++++++++++++++++++++++ config/routes.rb | 3 + 4 files changed, 179 insertions(+) create mode 100644 app/controllers/jwt_controller.rb diff --git a/Gemfile b/Gemfile index 512c6babd7..0301f6fe06 100644 --- a/Gemfile +++ b/Gemfile @@ -225,6 +225,7 @@ gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' +gem 'base32', '~> 0.3.0' # Sentry integration gem 'sentry-raven', '~> 0.15' diff --git a/Gemfile.lock b/Gemfile.lock index 2b578429b3..2b1cfdc9bb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,7 @@ GEM ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) + base32 (0.3.2) bcrypt (3.1.10) benchmark-ips (2.3.0) better_errors (1.0.1) @@ -897,6 +898,7 @@ DEPENDENCIES attr_encrypted (~> 1.3.4) awesome_print (~> 1.2.0) babosa (~> 1.0.2) + base32 (~> 0.3.0) benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb new file mode 100644 index 0000000000..7e70c70c89 --- /dev/null +++ b/app/controllers/jwt_controller.rb @@ -0,0 +1,173 @@ +class JwtController < ApplicationController + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + + def auth + @authenticated = authenticate_with_http_basic do |login, password| + @ci_project = ci_project(login, password) + @user = authenticate_user(login, password) unless @ci_project + end + + unless @authenticated + return render_403 if has_basic_credentials? + end + + case params[:service] + when 'docker' + docker_token_auth(params[:scope], params[:offline_token]) + else + return render_404 + end + end + + private + + def render_400 + head :invalid_request + end + + def render_404 + head :not_found + end + + def render_403 + head :forbidden + end + + def docker_token_auth(scope, offline_token) + payload = { + aud: params[:service], + sub: @user.try(:username) + } + + if offline_token + return render_403 unless @user + elsif scope + access = process_access(scope) + return render_404 unless access + payload[:access] = [access] + end + + render json: { token: encode(payload) } + end + + def ci_project(login, password) + matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) + + if matched_login.present? + underscored_service = matched_login['s'].underscore + + if underscored_service == 'gitlab_ci' + Project.find_by(builds_enabled: true, runners_token: password) + end + end + end + + def authenticate_user(login, password) + user = Gitlab::Auth.new.find(login, password) + + # If the user authenticated successfully, we reset the auth failure count + # from Rack::Attack for that IP. A client may attempt to authenticate + # with a username and blank password first, and only after it receives + # a 401 error does it present a password. Resetting the count prevents + # false positives from occurring. + # + # Otherwise, we let Rack::Attack know there was a failed authentication + # attempt from this IP. This information is stored in the Rails cache + # (Redis) and will be used by the Rack::Attack middleware to decide + # whether to block requests from this IP. + config = Gitlab.config.rack_attack.git_basic_auth + + if config.enabled + if user + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(request.ip, config) + else + banned = Rack::Attack::Allow2Ban.filter(request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(request.ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{request.ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end + end + + user + end + + def process_access(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + project = Project.find_with_namespace(name) + return unless project + + actions = actions.select do |action| + can_access?(project, action) + end + + { type: 'repository', name: name, actions: actions } if actions + end + + def default_payload + { + aud: 'docker', + sub: @user.try(:username), + aud: params[:service], + } + end + + def private_key + @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key + end + + def encode(payload) + issued_at = Time.now + payload = payload.merge( + iss: Gitlab.config.registry.issuer, + iat: issued_at.to_i, + nbf: issued_at.to_i - 5.seconds.to_i, + exp: issued_at.to_i + 60.minutes.to_i, + jti: SecureRandom.uuid, + ) + headers = { + kid: kid(private_key) + } + JWT.encode(payload, private_key, 'RS256', headers) + end + + def can_access?(project, action) + case action + when 'pull' + project == @ci_project || can?(@user, :download_code, project) + when 'push' + project == @ci_project || can?(@user, :push_code, project) + else + false + end + end + + def kid(private_key) + sha256 = Digest::SHA256.new + sha256.update(private_key.public_key.to_der) + payload = StringIO.new(sha256.digest).read(30) + Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| + mem << slice.join + end.join(':') + end +end diff --git a/config/routes.rb b/config/routes.rb index adf4bb18b3..5b48819dd9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -63,6 +63,9 @@ Rails.application.routes.draw do get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete + # JSON Web Token + get 'jwt/auth' => 'jwt#auth' + # API API::API.logger Rails.logger mount API::API => '/api' From 011a905a821e2ff0cd2d9885ef93764018eb8346 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 2 May 2016 14:32:16 +0200 Subject: [PATCH 039/449] Split docker authentication service --- app/controllers/jwt_controller.rb | 116 +++--------------- .../jwt/docker_authentication_service.rb | 65 ++++++++++ lib/jwt/rsa_token.rb | 36 ++++++ lib/jwt/token.rb | 48 ++++++++ 4 files changed, 163 insertions(+), 102 deletions(-) create mode 100644 app/services/jwt/docker_authentication_service.rb create mode 100644 lib/jwt/rsa_token.rb create mode 100644 lib/jwt/token.rb diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 7e70c70c89..2a92627cb1 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -2,6 +2,10 @@ class JwtController < ApplicationController skip_before_action :authenticate_user! skip_before_action :verify_authenticity_token + SERVICES = { + 'docker' => Jwt::DockerAuthenticationService, + } + def auth @authenticated = authenticate_with_http_basic do |login, password| @ci_project = ci_project(login, password) @@ -9,46 +13,22 @@ class JwtController < ApplicationController end unless @authenticated - return render_403 if has_basic_credentials? + head :forbidden if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) end - case params[:service] - when 'docker' - docker_token_auth(params[:scope], params[:offline_token]) - else - return render_404 - end + service = SERVICES[params[:service]] + head :not_found unless service + + result = service.new(@ci_project, @user, auth_params).execute + return head result[:http_status] if result[:http_status] + + render json: result end private - def render_400 - head :invalid_request - end - - def render_404 - head :not_found - end - - def render_403 - head :forbidden - end - - def docker_token_auth(scope, offline_token) - payload = { - aud: params[:service], - sub: @user.try(:username) - } - - if offline_token - return render_403 unless @user - elsif scope - access = process_access(scope) - return render_404 unless access - payload[:access] = [access] - end - - render json: { token: encode(payload) } + def auth_params + params.permit(:service, :scope, :offline_token, :account, :client_id) end def ci_project(login, password) @@ -102,72 +82,4 @@ class JwtController < ApplicationController user end - - def process_access(scope) - type, name, actions = scope.split(':', 3) - actions = actions.split(',') - - case type - when 'repository' - process_repository_access(type, name, actions) - end - end - - def process_repository_access(type, name, actions) - project = Project.find_with_namespace(name) - return unless project - - actions = actions.select do |action| - can_access?(project, action) - end - - { type: 'repository', name: name, actions: actions } if actions - end - - def default_payload - { - aud: 'docker', - sub: @user.try(:username), - aud: params[:service], - } - end - - def private_key - @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key - end - - def encode(payload) - issued_at = Time.now - payload = payload.merge( - iss: Gitlab.config.registry.issuer, - iat: issued_at.to_i, - nbf: issued_at.to_i - 5.seconds.to_i, - exp: issued_at.to_i + 60.minutes.to_i, - jti: SecureRandom.uuid, - ) - headers = { - kid: kid(private_key) - } - JWT.encode(payload, private_key, 'RS256', headers) - end - - def can_access?(project, action) - case action - when 'pull' - project == @ci_project || can?(@user, :download_code, project) - when 'push' - project == @ci_project || can?(@user, :push_code, project) - else - false - end - end - - def kid(private_key) - sha256 = Digest::SHA256.new - sha256.update(private_key.public_key.to_der) - payload = StringIO.new(sha256.digest).read(30) - Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| - mem << slice.join - end.join(':') - end end diff --git a/app/services/jwt/docker_authentication_service.rb b/app/services/jwt/docker_authentication_service.rb new file mode 100644 index 0000000000..ce28085e5d --- /dev/null +++ b/app/services/jwt/docker_authentication_service.rb @@ -0,0 +1,65 @@ +module Jwt + class DockerAuthenticationService < BaseService + def execute + if params[:offline_token] + return error('forbidden', 403) unless current_user + end + + { token: token.encoded } + end + + private + + def token + token = ::Jwt::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token[:access] = access + token + end + + def access + return unless params[:scope] + + scope = process_scope(params[:scope]) + [scope].compact + end + + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + current_project = Project.find_with_namespace(name) + return unless current_project + + actions = actions.select do |action| + can_access?(current_project, action) + end + + { type: type, name: name, actions: actions } if actions + end + + def can_access?(current_project, action) + case action + when 'pull' + current_project == project || can?(current_user, :download_code, current_project) + when 'push' + current_project == project || can?(current_user, :push_code, current_project) + else + false + end + end + + def registry + Gitlab.config.registry + end + end +end diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb new file mode 100644 index 0000000000..cc265e3b31 --- /dev/null +++ b/lib/jwt/rsa_token.rb @@ -0,0 +1,36 @@ +module Jwt + class RSAToken < Token + attr_reader :key_file + + def initialize(key_file) + super() + @key_file = key_file + end + + def encoded + headers = { + kid: kid + } + JWT.encode(payload, key, 'RS256', headers) + end + + private + + def key_data + @key_data ||= File.read(key_file) + end + + def key + @key ||= OpenSSL::PKey::RSA.new(key_data) + end + + def kid + sha256 = Digest::SHA256.new + sha256.update(key.public_key.to_der) + payload = StringIO.new(sha256.digest).read(30) + Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| + mem << slice.join + end.join(':') + end + end +end diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb new file mode 100644 index 0000000000..38cbc8004e --- /dev/null +++ b/lib/jwt/token.rb @@ -0,0 +1,48 @@ +module Jwt + class Token + attr_accessor :issuer, :subject, :audience, :id + attr_accessor :issued_at, :not_before, :expire_time + + def initialize + @payload = {} + @id = SecureRandom.uuid + @issued_at = Time.now + @not_before = issued_at - 5.seconds + @expire_time = issued_at + 1.minute + end + + def [](key) + @payload[key] + end + + def []=(key, value) + @payload[key] = value + end + + def encoded + raise NotImplementedError + end + + def payload + @payload.merge(default_payload) + end + + def to_json + payload.to_json + end + + private + + def default_payload + { + jti: id, + aud: audience, + sub: subject, + iss: issuer, + iat: issued_at.to_i, + nbf: not_before.to_i, + exp: expire_time.to_i + }.compact + end + end +end \ No newline at end of file From f4e0c56279007fd6cec3d8e6bd684f0483b0e0ff Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 3 May 2016 13:03:10 +0200 Subject: [PATCH 040/449] 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 dfa9bd259e..366373b0f0 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 1e3b481559..818abbf4cc 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 c1c51302e7..6ffdb18339 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 041/449] 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 366373b0f0..9869d90831 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 042/449] 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 9869d90831..47524b1cf0 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 b0ddbaa07cd780b0ed86aa4e3c24744c6426b1e1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Apr 2016 08:14:40 -0400 Subject: [PATCH 043/449] 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 0000000000..5b10746aa0 --- /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 f07eff3fb5..66cb41cc49 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 3d5e61d2c1..6d1e630a09 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 76265a59ea..496f9f3e34 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 0000000000..b4ef60a016 --- /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 479bde3371..2577afefa9 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 0000000000..648aeeef2d --- /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 0000000000..338f3e5662 --- /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 ca58ae92d1..71e3c9d7db 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 5b48819dd9..0280898acc 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 ace906a6f5..9b8f416ddf 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 044/449] 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 5b10746aa0..cf3bdd42cf 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 496f9f3e34..b905ebbfca 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 b4ef60a016..0000000000 --- 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 338f3e5662..0987c7a39e 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 0000000000..1aeeba7a68 --- /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 0000000000..b2e43ce4ae --- /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 0000000000..1c2abec1bf --- /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 0000000000..d8de8e392e --- /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 0000000000..f4f4ba65af --- /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 0000000000..2bf0b8e345 --- /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 87518a7b39..0000000000 --- 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 045/449] 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 71e3c9d7db..ca58ae92d1 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 046/449] 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 df5054f08d..70bfc1fd53 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 f4f4ba65af..c45fa2911e 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 047/449] 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 1aeeba7a68..43665149e2 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 b2e43ce4ae..84375ce802 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 c45fa2911e..763d866955 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 2bf0b8e345..76946a6ce5 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 048/449] 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 84375ce802..a443205909 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 763d866955..43e8e7720d 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 049/449] 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 b905ebbfca..c50ea45d3e 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 ce28085e5d..16d77193a1 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 050/449] 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 648aeeef2d..f583e7fcfe 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 0987c7a39e..3732698c08 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 051/449] 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 2577afefa9..bef350adf3 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 3732698c08..08f67345b4 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 8b813277b0cbf57b2a07ad2e1b4cb87dadfb66c5 Mon Sep 17 00:00:00 2001 From: Yatish Mehta Date: Wed, 4 May 2016 14:04:13 -0700 Subject: [PATCH 052/449] Fixed typo in zen.scss and corresponding views --- app/assets/stylesheets/framework/zen.scss | 2 +- app/views/projects/_md_preview.html.haml | 2 +- app/views/projects/_zen.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index f870ea0d87..ff02ebdd34 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -32,7 +32,7 @@ } } -.zen-cotrol { +.zen-control { padding: 0; color: #555; background: none; diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 8de44a6c91..81afea2c60 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -8,7 +8,7 @@ %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview %li.pull-right - %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } + %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } Go full screen .md-write-holder diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index e1e3501396..413477a2d3 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -4,5 +4,5 @@ = f.text_area attr, class: classes, placeholder: placeholder - else = text_area_tag attr, nil, class: classes, placeholder: placeholder - %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } + %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') From 8dc19494c3fdae366daa8849b5e2a3f58f98878c Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Thu, 5 May 2016 13:26:36 +0700 Subject: [PATCH 053/449] Remove unused code, update spec, and update changelog --- CHANGELOG | 1 + app/controllers/snippets_controller.rb | 20 +------------------- spec/routing/routing_spec.rb | 8 -------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 558897ad89..6c044192d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.0 (unreleased) + - Snippets tab under user profile. !4001 (Long Nguyen) - Remove future dates from contribution calendar graph. - Fix error when visiting commit builds page before build was updated - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2daceed039..f0bd842ca5 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -10,29 +10,11 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :user_index, :show, :raw] + skip_before_action :authenticate_user!, only: [:show, :raw] layout 'snippets' respond_to :html - def index - if params[:username].present? - @user = User.find_by(username: params[:username]) - - render_404 and return unless @user - - @snippets = SnippetsFinder.new.execute(current_user, { - filter: :by_user, - user: @user, - scope: params[:scope] }). - page(params[:page]) - - render 'index' - else - redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path) - end - end - def new @snippet = PersonalSnippet.new end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1527eddfa4..9deffd0a1e 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -27,18 +27,10 @@ end # PUT /snippets/:id(.:format) snippets#update # DELETE /snippets/:id(.:format) snippets#destroy describe SnippetsController, "routing" do - it "to #user_index" do - expect(get("/s/User")).to route_to('snippets#index', username: 'User') - end - it "to #raw" do expect(get("/snippets/1/raw")).to route_to('snippets#raw', id: '1') end - it "to #index" do - expect(get("/snippets")).to route_to('snippets#index') - end - it "to #create" do expect(post("/snippets")).to route_to('snippets#create') end From ae29ec31e4f71d722e975bfce945aaed7e0d0bd1 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Thu, 5 May 2016 14:57:34 +0700 Subject: [PATCH 054/449] Remove unused view and update redirect when destroy snippet --- app/controllers/snippets_controller.rb | 2 +- app/views/snippets/index.html.haml | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 app/views/snippets/index.html.haml diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index f0bd842ca5..2c038bdfda 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -43,7 +43,7 @@ class SnippetsController < ApplicationController @snippet.destroy - redirect_to snippets_path + redirect_to dashboard_snippets_path end def raw diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml deleted file mode 100644 index 7e4918a608..0000000000 --- a/app/views/snippets/index.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- page_title "By #{@user.name}", "Snippets" - -%ol.breadcrumb - %li - = link_to snippets_path do - Snippets - %li - = @user.name - .pull-right.hidden-xs - = link_to user_path(@user) do - #{@user.name} profile page - -= render 'snippets' From ffda8a1a0eb273e62fcb0197f352400946571778 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Sun, 8 May 2016 15:27:33 +0700 Subject: [PATCH 055/449] user routings refactor --- app/controllers/snippets_controller.rb | 22 ++++++++++- .../search/results/_snippet_blob.html.haml | 2 +- .../search/results/_snippet_title.html.haml | 2 +- app/views/shared/snippets/_snippet.html.haml | 2 +- app/views/snippets/index.html.haml | 13 +++++++ app/views/users/calendar.html.haml | 2 +- app/views/users/show.html.haml | 12 +++--- config/routes.rb | 37 +++++++++---------- spec/routing/routing_spec.rb | 4 ++ 9 files changed, 64 insertions(+), 32 deletions(-) create mode 100644 app/views/snippets/index.html.haml diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2c038bdfda..2a17c1f34d 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -10,11 +10,29 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:show, :raw] + skip_before_action :authenticate_user!, only: [:index, :show, :raw] layout 'snippets' respond_to :html + def index + if params[:username].present? + @user = User.find_by(username: params[:username]) + + render_404 and return unless @user + + @snippets = SnippetsFinder.new.execute(current_user, { + filter: :by_user, + user: @user, + scope: params[:scope] }). + page(params[:page]) + + render 'index' + else + redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path) + end + end + def new @snippet = PersonalSnippet.new end @@ -43,7 +61,7 @@ class SnippetsController < ApplicationController @snippet.destroy - redirect_to dashboard_snippets_path + redirect_to snippets_path end def raw diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index c9b7bd154a..6b7e06f2b2 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -6,7 +6,7 @@ %span = snippet.title by - = link_to user_snippets_path(snippet.author) do + = link_to snippets_user_path(snippet.author) do = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' = snippet.author_name %span.light #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index c414acb6a1..8c884926db 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -17,7 +17,7 @@ = "##{snippet_title.id}" %span by - = link_to user_snippets_path(snippet_title.author) do + = link_to snippets_user_path(snippet_title.author) do = image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: '' = snippet_title.author_name %span.light #{time_ago_with_tooltip(snippet_title.created_at)} diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index c96dfefe17..e6a7a7777b 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -16,6 +16,6 @@ = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) .snippet-info - = link_to user_snippets_path(snippet.author) do + = link_to snippets_user_path(snippet.author) do = snippet.author_name authored #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml new file mode 100644 index 0000000000..8aa3d490fa --- /dev/null +++ b/app/views/snippets/index.html.haml @@ -0,0 +1,13 @@ + - page_title "By #{@user.name}", "Snippets" + + %ol.breadcrumb + %li + = link_to snippets_path do + Snippets + %li + = @user.name + .pull-right.hidden-xs + = link_to user_path(@user) do + #{@user.name} profile page + + = render 'snippets' diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 1de71f37d1..6ff4eb79b0 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -4,7 +4,7 @@ #{@timestamps.to_json}, #{@starting_year}, #{@starting_month}, - '#{user_calendar_activities_path}' + '#{calendar_activities_user_path}' ); .calendar-hint Summary of issues, merge requests, and push events diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 2ccdf843a6..e894a8e939 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -70,19 +70,19 @@ %ul.nav-links.center.user-profile-nav %li.js-activity-tab - = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do + = link_to calendar_activities_user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do Activity %li.js-groups-tab - = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do + = link_to groups_user_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do Groups %li.js-contributed-tab - = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do + = link_to contributed_projects_user_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do Contributed projects %li.projects-tab - = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do + = link_to projects_user_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects %li.snippets-tab - = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + = link_to snippets_user_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do Snippets %div{ class: container_class } @@ -90,7 +90,7 @@ #activity.tab-pane .gray-content-block.calender-block.white.second-block.hidden-xs %div{ class: container_class } - .user-calendar{data: {href: user_calendar_path}} + .user-calendar{data: {href: calendar_user_path}} %h4.center.light %i.fa.fa-spinner.fa-spin .user-calendar-activities diff --git a/config/routes.rb b/config/routes.rb index 2bfaa2c2af..533c3b98e8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,6 +91,9 @@ Rails.application.routes.draw do end end + get '/s/:username', to: redirect('/u/:username/snippets'), + constraints: { username: /[a-zA-Z.0-9_\-]+(? 'users#calendar', as: :user_calendar, - constraints: { username: /.*/ } - - get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities, - constraints: { username: /.*/ } - - get 'u/:username/groups' => 'users#groups', as: :user_groups, - constraints: { username: /.*/ } - - get 'u/:username/projects' => 'users#projects', as: :user_projects, - constraints: { username: /.*/ } - - get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects, - constraints: { username: /.*/ } - - get 'u/:username/snippets' => 'users#snippets', as: :user_snippets, - constraints: { username: /.*/ } - - get '/u/:username' => 'users#show', as: :user, - constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Sun, 8 May 2016 15:29:51 +0700 Subject: [PATCH 056/449] Fix routing error --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 533c3b98e8..299b0dd1ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,7 +91,7 @@ Rails.application.routes.draw do end end - get '/s/:username', to: redirect('/u/:username/snippets'), + get '/s/:username', to: redirect('/u/%{username}/snippets'), constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Sun, 8 May 2016 20:48:07 +0700 Subject: [PATCH 057/449] Routing refactoring --- .../search/results/_snippet_blob.html.haml | 2 +- .../search/results/_snippet_title.html.haml | 2 +- app/views/shared/snippets/_snippet.html.haml | 2 +- app/views/users/calendar.html.haml | 2 +- app/views/users/show.html.haml | 12 +++++----- config/routes.rb | 23 ++++++++----------- 6 files changed, 20 insertions(+), 23 deletions(-) diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 6b7e06f2b2..c9b7bd154a 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -6,7 +6,7 @@ %span = snippet.title by - = link_to snippets_user_path(snippet.author) do + = link_to user_snippets_path(snippet.author) do = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' = snippet.author_name %span.light #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 8c884926db..c414acb6a1 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -17,7 +17,7 @@ = "##{snippet_title.id}" %span by - = link_to snippets_user_path(snippet_title.author) do + = link_to user_snippets_path(snippet_title.author) do = image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: '' = snippet_title.author_name %span.light #{time_ago_with_tooltip(snippet_title.created_at)} diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index e6a7a7777b..c96dfefe17 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -16,6 +16,6 @@ = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) .snippet-info - = link_to snippets_user_path(snippet.author) do + = link_to user_snippets_path(snippet.author) do = snippet.author_name authored #{time_ago_with_tooltip(snippet.created_at)} diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 6ff4eb79b0..1de71f37d1 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -4,7 +4,7 @@ #{@timestamps.to_json}, #{@starting_year}, #{@starting_month}, - '#{calendar_activities_user_path}' + '#{user_calendar_activities_path}' ); .calendar-hint Summary of issues, merge requests, and push events diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index d5f482f716..9017fd54fc 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -70,19 +70,19 @@ %ul.nav-links.center.user-profile-nav %li.js-activity-tab - = link_to calendar_activities_user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do + = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do Activity %li.js-groups-tab - = link_to groups_user_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do + = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do Groups %li.js-contributed-tab - = link_to contributed_projects_user_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do + = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do Contributed projects %li.projects-tab - = link_to projects_user_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do + = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects %li.snippets-tab - = link_to snippets_user_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do Snippets %div{ class: container_class } @@ -90,7 +90,7 @@ #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs %div{ class: container_class } - .user-calendar{data: {href: calendar_user_path}} + .user-calendar{data: {href: user_calendar_path}} %h4.center.light %i.fa.fa-spinner.fa-spin .user-calendar-activities diff --git a/config/routes.rb b/config/routes.rb index 299b0dd1ac..594bab204f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -343,19 +343,16 @@ Rails.application.routes.draw do end end - resources(:users, - path: 'u', - param: :username, - constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Sun, 8 May 2016 22:06:19 +0700 Subject: [PATCH 058/449] Add specs for user routing and update spec for user controller --- spec/controllers/users_controller_spec.rb | 22 ++++++++++++++ spec/routing/routing_spec.rb | 37 +++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 8045c8b940..c61ec17466 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -112,4 +112,26 @@ describe UsersController do expect(response).to render_template('calendar_activities') end end + + describe 'GET #snippets' do + before do + sign_in(user) + end + + context 'format html' do + it 'renders snippets page' do + get :snippets, username: user.username + expect(response.status).to eq(200) + expect(response).to render_template('show') + end + end + + context 'format json' do + it 'response with snippets json data' do + get :snippets, username: user.username, format: :json + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to have_key('html') + end + end + end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 543088fa08..8530a2f31d 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -1,5 +1,42 @@ require 'spec_helper' +# user GET /u/:username/ +# user_groups GET /u/:username/groups(.:format) +# user_projects GET /u/:username/projects(.:format) +# user_contributed_projects GET /u/:username/contributed(.:format) +# user_snippets GET /u/:username/snippets(.:format) +# user_calendar GET /u/:username/calendar(.:format) +# user_calendar_activities GET /u/:username/calendar_activities(.:format) +describe UsersController, "routing" do + it "to #show" do + expect(get("/u/User")).to route_to('users#show', username: 'User') + end + + it "to #groups" do + expect(get("/u/User/groups")).to route_to('users#groups', username: 'User') + end + + it "to #projects" do + expect(get("/u/User/projects")).to route_to('users#projects', username: 'User') + end + + it "to #contributed" do + expect(get("/u/User/contributed")).to route_to('users#contributed', username: 'User') + end + + it "to #snippets" do + expect(get("/u/User/snippets")).to route_to('users#snippets', username: 'User') + end + + it "to #calendar" do + expect(get("/u/User/calendar")).to route_to('users#calendar', username: 'User') + end + + it "to #calendar_activities" do + expect(get("/u/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') + end +end + # search GET /search(.:format) search#show describe SearchController, "routing" do it "to #show" do From 5196f8e993491e8e9dea6e36a7c5c3b8d95a5491 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 8 May 2016 22:50:30 +0200 Subject: [PATCH 059/449] WIP --- .../projects/container_registry_controller.rb | 31 +++++++++++++++++++ app/controllers/projects/images_controller.rb | 28 ----------------- app/helpers/gitlab_routing_helper.rb | 4 +-- app/models/project.rb | 4 +-- app/views/layouts/nav/_project.html.haml | 6 ++-- .../_header_title.html.haml | 1 + .../index.html.haml | 4 +-- .../projects/images/_header_title.html.haml | 1 - config/routes.rb | 2 +- lib/gitlab/regex.rb | 2 +- 10 files changed, 43 insertions(+), 40 deletions(-) create mode 100644 app/controllers/projects/container_registry_controller.rb delete mode 100644 app/controllers/projects/images_controller.rb create mode 100644 app/views/projects/container_registry/_header_title.html.haml rename app/views/projects/{images => container_registry}/index.html.haml (86%) delete mode 100644 app/views/projects/images/_header_title.html.haml diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb new file mode 100644 index 0000000000..ffd455e647 --- /dev/null +++ b/app/controllers/projects/container_registry_controller.rb @@ -0,0 +1,31 @@ +class Projects::ContainerRegistryController < Projects::ApplicationController + before_action :authorize_read_image! + before_action :authorize_update_image!, only: [:destroy] + before_action :tag, except: [:index] + layout 'project' + + def index + @tags = container_registry.tags + + other_repository = container_registry.registry["gitlab/gitlab-test3"] + container_registry.copy_to(other_repository) + end + + def destroy + if tag.delete + redirect_to namespace_project_container_registry_index_path(project.namespace, project) + else + redirect_to namespace_project_container_registry_index_path(project.namespace, project), alert: 'Failed to remove tag' + end + end + + private + + def container_registry + @container_registry ||= project.container_registry + end + + def tag + @tag ||= container_registry[params[:id]] + end +end diff --git a/app/controllers/projects/images_controller.rb b/app/controllers/projects/images_controller.rb deleted file mode 100644 index cf3bdd42cf..0000000000 --- a/app/controllers/projects/images_controller.rb +++ /dev/null @@ -1,28 +0,0 @@ -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 = image_repository.tags - end - - def destroy - 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 image_repository - @image_repository ||= project.image_repository - end - - def tag - @tag ||= image_repository[params[:id]] - end -end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 66cb41cc49..dd9536b146 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -33,8 +33,8 @@ 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) + def project_container_registry_path(project, *args) + namespace_project_container_registry_index_url(project.namespace, project, *args) end def activity_project_path(project, *args) diff --git a/app/models/project.rb b/app/models/project.rb index c50ea45d3e..0e32be5a53 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -370,10 +370,10 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end - def image_repository + def container_registry @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) + @container_registry ||= ImageRegistry::Repository.new(@registry, path_with_namespace) end def registry_repository_url diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index bef350adf3..8448599c1c 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -47,11 +47,11 @@ %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 + = nav_link(controller: %w(container_registry)) do + = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-images' do = icon('hdd-o fw') %span - Container Images + Container Registry - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do diff --git a/app/views/projects/container_registry/_header_title.html.haml b/app/views/projects/container_registry/_header_title.html.haml new file mode 100644 index 0000000000..f1863c52a3 --- /dev/null +++ b/app/views/projects/container_registry/_header_title.html.haml @@ -0,0 +1 @@ +- header_title project_title(@project, "Container Registry", project_container_registry_path(@project)) diff --git a/app/views/projects/images/index.html.haml b/app/views/projects/container_registry/index.html.haml similarity index 86% rename from app/views/projects/images/index.html.haml rename to app/views/projects/container_registry/index.html.haml index 08f67345b4..1ac3a62f54 100644 --- a/app/views/projects/images/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -1,4 +1,4 @@ -- page_title "Container Images" +- page_title "Container Registry" = render "header_title" .light.prepend-top-default @@ -53,5 +53,5 @@ = time_ago_in_words(tag.created_at) %td.content .controls.hidden-xs.pull-right - = 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 + = link_to namespace_project_container_registry_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/app/views/projects/images/_header_title.html.haml b/app/views/projects/images/_header_title.html.haml deleted file mode 100644 index f583e7fcfe..0000000000 --- a/app/views/projects/images/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Container Images", project_images_path(@project)) diff --git a/config/routes.rb b/config/routes.rb index 0280898acc..ce75329597 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -690,7 +690,7 @@ Rails.application.routes.draw do end end - resources :images, only: [:index, :destroy], constraints: { id: Gitlab::Regex.image_reference_regex } + resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex } resources :milestones, constraints: { id: /\d+/ } do member do diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 9b8f416ddf..1cbd6d945a 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -97,7 +97,7 @@ module Gitlab }x.freeze end - def image_reference_regex + def container_registry_reference_regex git_reference_regex end end From 9cc0937b3a41caca89fa6722149248a8f7b0a447 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sun, 8 May 2016 15:33:34 -0600 Subject: [PATCH 060/449] Enable the Rubocop DeprecatedClassMethods cop This reports uses of `File.exists?` and `Dir.exists?`, which were both deprecated in Ruby and will eventually be removed in favor of `.exist?`. Also fixes all existing uses of the deprecated methods. --- .rubocop.yml | 2 +- app/models/ci/build.rb | 2 +- app/models/merge_request.rb | 2 +- config/boot.rb | 2 +- config/initializers/carrierwave.rb | 2 +- lib/gitlab/backend/shell.rb | 2 +- lib/gitlab/redis.rb | 2 +- spec/config/mail_room_spec.rb | 2 +- spec/services/projects/create_service_spec.rb | 4 ++-- spec/services/projects/destroy_service_spec.rb | 8 ++++---- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 9f179efa3c..b49b60d676 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -770,7 +770,7 @@ Lint/DefEndAlignment: # Check for deprecated class method calls. Lint/DeprecatedClassMethods: - Enabled: false + Enabled: true # Check for duplicate method definitions. Lint/DuplicateMethods: diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4bc3a225e2..073ec9dd89 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -238,7 +238,7 @@ module Ci end def recreate_trace_dir - unless Dir.exists?(dir_to_trace) + unless Dir.exist?(dir_to_trace) FileUtils.mkdir_p(dir_to_trace) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4175e1e5fb..365ccc90b4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -544,7 +544,7 @@ class MergeRequest < ActiveRecord::Base end def ref_is_fetched? - File.exists?(File.join(project.repository.path_to_repo, ref_path)) + File.exist?(File.join(project.repository.path_to_repo, ref_path)) end def ensure_ref_fetched diff --git a/config/boot.rb b/config/boot.rb index 4489e58688..f2830ae316 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -3,4 +3,4 @@ require 'rubygems' # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index df28d30d75..0d278717d6 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -2,7 +2,7 @@ CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/ aws_file = Rails.root.join('config', 'aws.yml') -if File.exists?(aws_file) +if File.exist?(aws_file) AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env] CarrierWave.configure do |config| diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 132f9cd196..3e3986d638 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -180,7 +180,7 @@ module Gitlab # exists?('gitlab/cookies.git') # def exists?(dir_name) - File.exists?(full_path(dir_name)) + File.exist?(full_path(dir_name)) end protected diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 5c352c96de..f7db7a6391 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -42,7 +42,7 @@ module Gitlab config_file = File.expand_path('../../../config/resque.yml', __FILE__) @url = "redis://localhost:6379" - if File.exists?(config_file) + if File.exist?(config_file) @url =YAML.load_file(config_file)[rails_env] end end diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 462afb24f0..6fad7e2b9e 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -43,7 +43,7 @@ describe "mail_room.yml" do redis_config_file = Rails.root.join('config', 'resque.yml') redis_url = - if File.exists?(redis_config_file) + if File.exist?(redis_config_file) YAML.load_file(redis_config_file)[Rails.env] else "redis://localhost:6379" diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index e43903dbd3..fd11435946 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -64,7 +64,7 @@ describe Projects::CreateService, services: true do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { expect(File.exists?(@path)).to be_truthy } + it { expect(File.exist?(@path)).to be_truthy } end context 'wiki_enabled false does not create wiki repository directory' do @@ -74,7 +74,7 @@ describe Projects::CreateService, services: true do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { expect(File.exists?(@path)).to be_falsey } + it { expect(File.exist?(@path)).to be_falsey } end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 1ec2707771..a5cb6f382e 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -13,8 +13,8 @@ describe Projects::DestroyService, services: true do end it { expect(Project.all).not_to include(project) } - it { expect(Dir.exists?(path)).to be_falsey } - it { expect(Dir.exists?(remove_path)).to be_falsey } + it { expect(Dir.exist?(path)).to be_falsey } + it { expect(Dir.exist?(remove_path)).to be_falsey } end context 'Sidekiq fake' do @@ -24,8 +24,8 @@ describe Projects::DestroyService, services: true do end it { expect(Project.all).not_to include(project) } - it { expect(Dir.exists?(path)).to be_falsey } - it { expect(Dir.exists?(remove_path)).to be_truthy } + it { expect(Dir.exist?(path)).to be_falsey } + it { expect(Dir.exist?(remove_path)).to be_truthy } end def destroy_project(project, user, params) From 5bd32954f619b521496bca200a0ed31615b8e736 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 9 May 2016 13:52:20 +0200 Subject: [PATCH 061/449] Remove space --- doc/web_hooks/web_hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 6ffdb18339..45506ac1d7 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -16,7 +16,7 @@ Webhooks can be used to update an external issue tracker, trigger CI builds, upd ## Webhook endpoint tips If you are writing your own endpoint (web server) that will receive -GitLab web hooks keep in mind the following things: +GitLab webhooks 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. From a19508d6b79aa3a399f479b09b52b35b5289a92a Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 9 May 2016 13:58:43 +0200 Subject: [PATCH 062/449] Fix test failures --- features/steps/project/hooks.rb | 2 +- spec/models/hooks/web_hook_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index b1ffe7f7b4..13c0713669 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -59,7 +59,7 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps step 'hook should be triggered' do expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project) expect(page).to have_selector '.flash-notice', - text: 'Hook successfully executed.' + text: 'Hook executed successfully: HTTP 200' end step 'I should see hook error message' do diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 37a27d73aa..f9bab487b9 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -95,13 +95,13 @@ describe WebHook, models: true do it "handles 200 status code" do WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: "Success") - expect(project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + expect(project_hook.execute(@data, 'push_hooks')).to eq([200, 'Success']) end it "handles 2xx status codes" do WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: "Success") - expect(project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + expect(project_hook.execute(@data, 'push_hooks')).to eq([201, 'Success']) end end end From baef6728fa4e8e515ccdeba1ea54da996f322aab Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 19:59:45 +0300 Subject: [PATCH 063/449] Send trace to a browser incrementally when build is running We send a state of ansi2html to client, client needs to send this state back. The state describes the configuration of generator and position within trace. --- app/controllers/projects/builds_controller.rb | 15 +++ app/models/ci/build.rb | 8 +- app/views/projects/builds/show.html.haml | 7 +- config/routes.rb | 1 + lib/ci/ansi2html.rb | 78 ++++++++---- spec/lib/ci/ansi2html_spec.rb | 111 ++++++++++++------ 6 files changed, 162 insertions(+), 58 deletions(-) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index b8b9e78427..3c9a52a5dd 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -38,6 +38,14 @@ class Projects::BuildsController < Projects::ApplicationController end end + def trace + respond_to do |format| + format.json do + render json: @build.trace_with_state(params_state).merge!(id: @build.id, status: @build.status) + end + end + end + def retry unless @build.retryable? return render_404 @@ -72,6 +80,13 @@ class Projects::BuildsController < Projects::ApplicationController private + def params_state + begin + JSON.parse(params[:state], symbolize_names: true) + rescue + end + end + def build @build ||= project.builds.unscoped.find_by!(id: params[:id]) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4bc3a225e2..1eb6a0d902 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -132,8 +132,12 @@ module Ci end def trace_html - html = Ci::Ansi2html::convert(trace) if trace.present? - html || '' + trace_with_state[:html] + end + + def trace_with_state(state = nil) + trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present? + trace_with_state || {} end def timeout diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c0f7a7686f..0da0477bdd 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,5 +1,6 @@ - page_title "#{@build.name} (##{@build.id})", "Builds" = render "header_title" +- trace = build.trace_for_state .build-page .row-content-block.top-block @@ -85,7 +86,9 @@ %pre.trace#build-trace %code.bash = preserve do - = raw @build.trace_html + = raw trace[:html] + - if @build.active? + %i{:class => "fa fa-refresh fa-spin"} %div#down-build-trace @@ -216,4 +219,4 @@ :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}") + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace[:state]}") diff --git a/config/routes.rb b/config/routes.rb index dafecc9464..e5c7d656da 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -672,6 +672,7 @@ Rails.application.routes.draw do post :cancel post :retry post :erase + get :trace get :raw end diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index ac6d667cf8..d29e68570f 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -23,8 +23,8 @@ module Ci cross: 0x10, } - def self.convert(ansi) - Converter.new().convert(ansi) + def self.convert(ansi, state = nil) + Converter.new.convert(ansi, state) end class Converter @@ -84,22 +84,36 @@ module Ci def on_107(s) set_bg_color(7, 'l') end def on_109(s) set_bg_color(9, 'l') end - def convert(ansi) - @out = "" - @n_open_tags = 0 - reset() + attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask - s = StringScanner.new(ansi.gsub("<", "<")) + STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask] + + def convert(raw, new_state) + reset_state + restore_state(new_state) if new_state && new_state[:offset].to_i < raw.length + + start = @offset + ansi = raw[@offset..-1] + + open_new_tag + + s = StringScanner.new(ansi) while(!s.eos?) if s.scan(/\e([@-_])(.*?)([@-~])/) handle_sequence(s) + elsif s.scan(/\e(([@-_])(.*?)?)?$/) + break + elsif s.scan(/ 0 } end def handle_sequence(s) @@ -121,6 +135,20 @@ module Ci evaluate_command_stack(commands) + open_new_tag + end + + def evaluate_command_stack(stack) + return unless command = stack.shift() + + if self.respond_to?("on_#{command}", true) + self.send("on_#{command}", stack) + end + + evaluate_command_stack(stack) + end + + def open_new_tag css_classes = [] unless @fg_color.nil? @@ -138,20 +166,8 @@ module Ci css_classes << "term-#{css_class}" if @style_mask & flag != 0 end - open_new_tag(css_classes) if css_classes.length > 0 - end + return if css_classes.empty? - def evaluate_command_stack(stack) - return unless command = stack.shift() - - if self.respond_to?("on_#{command}", true) - self.send("on_#{command}", stack) - end - - evaluate_command_stack(stack) - end - - def open_new_tag(css_classes) @out << %{} @n_open_tags += 1 end @@ -163,6 +179,26 @@ module Ci end end + def reset_state + @offset = 0 + @n_open_tags = 0 + @out = '' + reset + end + + def state + STATE_PARAMS.inject({}) do |h, param| + h[param] = send(param) + h + end + end + + def restore_state(new_state) + STATE_PARAMS.each do |param| + send("#{param}=".to_sym, new_state[param]) + end + end + def reset @fg_color = nil @bg_color = nil diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb index 3a2b568f4c..04afbd0692 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/ci/ansi2html_spec.rb @@ -4,131 +4,176 @@ describe Ci::Ansi2html, lib: true do subject { Ci::Ansi2html } it "prints non-ansi as-is" do - expect(subject.convert("Hello")).to eq('Hello') + expect(subject.convert("Hello")[:html]).to eq('Hello') end it "strips non-color-changing controll sequences" do - expect(subject.convert("Hello \e[2Kworld")).to eq('Hello world') + expect(subject.convert("Hello \e[2Kworld")[:html]).to eq('Hello world') end it "prints simply red" do - expect(subject.convert("\e[31mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[31mHello\e[0m")[:html]).to eq('Hello') end it "prints simply red without trailing reset" do - expect(subject.convert("\e[31mHello")).to eq('Hello') + expect(subject.convert("\e[31mHello")[:html]).to eq('Hello') end it "prints simply yellow" do - expect(subject.convert("\e[33mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[33mHello\e[0m")[:html]).to eq('Hello') end it "prints default on blue" do - expect(subject.convert("\e[39;44mHello")).to eq('Hello') + expect(subject.convert("\e[39;44mHello")[:html]).to eq('Hello') end it "prints red on blue" do - expect(subject.convert("\e[31;44mHello")).to eq('Hello') + expect(subject.convert("\e[31;44mHello")[:html]).to eq('Hello') end it "resets colors after red on blue" do - expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello\e[0m world")[:html]).to eq('Hello world') end it "performs color change from red/blue to yellow/blue" do - expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello \e[33mworld")[:html]).to eq('Hello world') end it "performs color change from red/blue to yellow/green" do - expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello \e[33;42mworld")[:html]).to eq('Hello world') end it "performs color change from red/blue to reset to yellow/green" do - expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")[:html]).to eq('Hello world') end it "ignores unsupported codes" do - expect(subject.convert("\e[51mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[51mHello\e[0m")[:html]).to eq('Hello') end it "prints light red" do - expect(subject.convert("\e[91mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[91mHello\e[0m")[:html]).to eq('Hello') end it "prints default on light red" do - expect(subject.convert("\e[101mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[101mHello\e[0m")[:html]).to eq('Hello') end it "performs color change from red/blue to default/blue" do - expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello \e[39mworld")[:html]).to eq('Hello world') end it "performs color change from light red/blue to default/blue" do - expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('Hello world') + expect(subject.convert("\e[91;44mHello \e[39mworld")[:html]).to eq('Hello world') end it "prints bold text" do - expect(subject.convert("\e[1mHello")).to eq('Hello') + expect(subject.convert("\e[1mHello")[:html]).to eq('Hello') end it "resets bold text" do - expect(subject.convert("\e[1mHello\e[21m world")).to eq('Hello world') - expect(subject.convert("\e[1mHello\e[22m world")).to eq('Hello world') + expect(subject.convert("\e[1mHello\e[21m world")[:html]).to eq('Hello world') + expect(subject.convert("\e[1mHello\e[22m world")[:html]).to eq('Hello world') end it "prints italic text" do - expect(subject.convert("\e[3mHello")).to eq('Hello') + expect(subject.convert("\e[3mHello")[:html]).to eq('Hello') end it "resets italic text" do - expect(subject.convert("\e[3mHello\e[23m world")).to eq('Hello world') + expect(subject.convert("\e[3mHello\e[23m world")[:html]).to eq('Hello world') end it "prints underlined text" do - expect(subject.convert("\e[4mHello")).to eq('Hello') + expect(subject.convert("\e[4mHello")[:html]).to eq('Hello') end it "resets underlined text" do - expect(subject.convert("\e[4mHello\e[24m world")).to eq('Hello world') + expect(subject.convert("\e[4mHello\e[24m world")[:html]).to eq('Hello world') end it "prints concealed text" do - expect(subject.convert("\e[8mHello")).to eq('Hello') + expect(subject.convert("\e[8mHello")[:html]).to eq('Hello') end it "resets concealed text" do - expect(subject.convert("\e[8mHello\e[28m world")).to eq('Hello world') + expect(subject.convert("\e[8mHello\e[28m world")[:html]).to eq('Hello world') end it "prints crossed-out text" do - expect(subject.convert("\e[9mHello")).to eq('Hello') + expect(subject.convert("\e[9mHello")[:html]).to eq('Hello') end it "resets crossed-out text" do - expect(subject.convert("\e[9mHello\e[29m world")).to eq('Hello world') + expect(subject.convert("\e[9mHello\e[29m world")[:html]).to eq('Hello world') end it "can print 256 xterm fg colors" do - expect(subject.convert("\e[38;5;16mHello")).to eq('Hello') + expect(subject.convert("\e[38;5;16mHello")[:html]).to eq('Hello') end it "can print 256 xterm fg colors on normal magenta background" do - expect(subject.convert("\e[38;5;16;45mHello")).to eq('Hello') + expect(subject.convert("\e[38;5;16;45mHello")[:html]).to eq('Hello') end it "can print 256 xterm bg colors" do - expect(subject.convert("\e[48;5;240mHello")).to eq('Hello') + expect(subject.convert("\e[48;5;240mHello")[:html]).to eq('Hello') end it "can print 256 xterm bg colors on normal magenta foreground" do - expect(subject.convert("\e[48;5;16;35mHello")).to eq('Hello') + expect(subject.convert("\e[48;5;16;35mHello")[:html]).to eq('Hello') end it "prints bold colored text vividly" do - expect(subject.convert("\e[1;31mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[1;31mHello\e[0m")[:html]).to eq('Hello') end it "prints bold light colored text correctly" do - expect(subject.convert("\e[1;91mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[1;91mHello\e[0m")[:html]).to eq('Hello') + end + + it "prints <" do + expect(subject.convert("<")[:html]).to eq('<') + end + + describe "incremental update" do + shared_examples 'stateable converter' do + let(:pass1) { subject.convert(pre_text) } + let(:pass2) { subject.convert(pre_text + text, pass1[:state]) } + + it "to returns html to append" do + expect(pass2[:append]).to be_truthy + expect(pass2[:html]).to eq(html) + expect(pass1[:text] + pass2[:text]).to eq(pre_text + text) + expect(pass1[:html] + pass2[:html]).to eq(pre_html + html) + end + end + + context "with split word" do + let(:pre_text) { "\e[1mHello" } + let(:pre_html) { "Hello" } + let(:text) { "\e[1mWorld" } + let(:html) { "World" } + + it_behaves_like 'stateable converter' + end + + context "with split sequence" do + let(:pre_text) { "\e[1m" } + let(:pre_html) { "" } + let(:text) { "Hello" } + let(:html) { "Hello" } + + it_behaves_like 'stateable converter' + end + + context "with partial sequence" do + let(:pre_text) { "Hello\e" } + let(:pre_html) { "Hello" } + let(:text) { "[1m World" } + let(:html) { " World" } + + it_behaves_like 'stateable converter' + end end end From 74520f23db51c95b4aea8856fb51c4246785f776 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:19:27 +0300 Subject: [PATCH 064/449] Encode state as base64 string --- app/assets/javascripts/ci/build.coffee | 21 ++++++++++++------- app/controllers/projects/builds_controller.rb | 9 +------- app/views/projects/builds/show.html.haml | 6 +++--- lib/ci/ansi2html.rb | 13 ++++++++---- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 7afe8bf79e..fca0c3bae5 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -1,9 +1,12 @@ class CiBuild @interval: null + @state: null - constructor: (build_url, build_status) -> + constructor: (build_url, build_status, build_state) -> clearInterval(CiBuild.interval) + @state = build_state + @initScrollButtonAffix() if build_status == "running" || build_status == "pending" @@ -26,14 +29,18 @@ class CiBuild CiBuild.interval = setInterval => if window.location.href.split("#").first() is build_url $.ajax - url: build_url + url: build_url + "/trace.json?state=" + encodeURIComponent(@state) dataType: "json" - success: (build) => - if build.status == "running" - $('#build-trace code').html build.trace_html - $('#build-trace code').append '' + success: (log) => + @state = log.state + if log.status is "running" + if log.append + $('.fa-refresh').before log.html + else + $('#build-trace code').html log.html + $('#build-trace code').append '' @checkAutoscroll() - else if build.status != build_status + else if log.status isnt build_status Turbolinks.visit build_url , 4000 diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 3c9a52a5dd..bb1f6c5e98 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -41,7 +41,7 @@ class Projects::BuildsController < Projects::ApplicationController def trace respond_to do |format| format.json do - render json: @build.trace_with_state(params_state).merge!(id: @build.id, status: @build.status) + render json: @build.trace_with_state(params[:state]).merge!(id: @build.id, status: @build.status) end end end @@ -80,13 +80,6 @@ class Projects::BuildsController < Projects::ApplicationController private - def params_state - begin - JSON.parse(params[:state], symbolize_names: true) - rescue - end - end - def build @build ||= project.builds.unscoped.find_by!(id: params[:id]) end diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 0da0477bdd..c7b9c36a3a 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,6 +1,6 @@ - page_title "#{@build.name} (##{@build.id})", "Builds" = render "header_title" -- trace = build.trace_for_state +- trace_with_state = @build.trace_with_state .build-page .row-content-block.top-block @@ -86,7 +86,7 @@ %pre.trace#build-trace %code.bash = preserve do - = raw trace[:html] + = raw trace_with_state[:html] - if @build.active? %i{:class => "fa fa-refresh fa-spin"} @@ -219,4 +219,4 @@ :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace[:state]}") + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace_with_state[:state]}") diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index d29e68570f..5fed43aaeb 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -90,7 +90,7 @@ module Ci def convert(raw, new_state) reset_state - restore_state(new_state) if new_state && new_state[:offset].to_i < raw.length + restore_state(raw, new_state) if new_state start = @offset ansi = raw[@offset..-1] @@ -187,15 +187,20 @@ module Ci end def state - STATE_PARAMS.inject({}) do |h, param| + state = STATE_PARAMS.inject({}) do |h, param| h[param] = send(param) h end + Base64.urlsafe_encode64(state.to_json) end - def restore_state(new_state) + def restore_state(raw, new_state) + state = Base64.urlsafe_decode64(new_state) + state = JSON.parse(state, symbolize_names: true) + return if state[:offset].to_i > raw.length + STATE_PARAMS.each do |param| - send("#{param}=".to_sym, new_state[param]) + send("#{param}=".to_sym, state[param]) end end From 51a8811e262c48e2d3aaa426b3c87693dda87b37 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:26:14 +0300 Subject: [PATCH 065/449] Revert "Eager load `lib/api`" This reverts commit 0a280158efeb7f681589ae7af24f0ed9052de809. --- config/application.rb | 2 - config/routes.rb | 1 + lib/api/api.rb | 4 +- lib/api/api_guard.rb | 306 +++++++++++++++++++++--------------------- lib/ci/api/api.rb | 2 +- 5 files changed, 157 insertions(+), 158 deletions(-) diff --git a/config/application.rb b/config/application.rb index d4b86bb38b..b602e2b616 100644 --- a/config/application.rb +++ b/config/application.rb @@ -102,8 +102,6 @@ module Gitlab # This is needed for gitlab-shell ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH'] - config.eager_load_paths += ["#{Rails.root}/lib"] - config.generators do |g| g.factory_girl false end diff --git a/config/routes.rb b/config/routes.rb index bfc6818a8d..3c855cfafd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ require 'sidekiq/web' require 'sidekiq/cron/web' +require 'api/api' Rails.application.routes.draw do if Gitlab::Sherlock.enabled? diff --git a/lib/api/api.rb b/lib/api/api.rb index d41b4b7186..6ddfe11d98 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,6 +1,8 @@ +Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} + module API class API < Grape::API - include ::API::APIGuard + include APIGuard version 'v3', using: :path rescue_from ActiveRecord::RecordNotFound do diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 6dfd6e4396..b9994fcefd 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -2,173 +2,171 @@ require 'rack/oauth2' -module API - module APIGuard - extend ActiveSupport::Concern +module APIGuard + extend ActiveSupport::Concern - included do |base| - # OAuth2 Resource Server Authentication - use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| - # The authenticator only fetches the raw token string + included do |base| + # OAuth2 Resource Server Authentication + use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| + # The authenticator only fetches the raw token string - # Must yield access token to store it in the env - request.access_token - end - - helpers HelperMethods - - install_error_responders(base) + # Must yield access token to store it in the env + request.access_token end - # Helper Methods for Grape Endpoint - module HelperMethods - # Invokes the doorkeeper guard. - # - # If token is presented and valid, then it sets @current_user. - # - # If the token does not have sufficient scopes to cover the requred scopes, - # then it raises InsufficientScopeError. - # - # If the token is expired, then it raises ExpiredError. - # - # If the token is revoked, then it raises RevokedError. - # - # If the token is not found (nil), then it raises TokenNotFoundError. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def doorkeeper_guard!(scopes: []) - if (access_token = find_access_token).nil? - raise TokenNotFoundError + helpers HelperMethods - else - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end - end - end - - def doorkeeper_guard(scopes: []) - if access_token = find_access_token - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) - end - end - end - - def current_user - @current_user - end - - private - def find_access_token - @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) - end - - def doorkeeper_request - @doorkeeper_request ||= ActionDispatch::Request.new(env) - end - - def validate_access_token(access_token, scopes) - Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) - end - end - - module ClassMethods - # Installs the doorkeeper guard on the whole Grape API endpoint. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def guard_all!(scopes: []) - before do - guard! scopes: scopes - end - end - - private - def install_error_responders(base) - error_classes = [ MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] - - base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler - end - - def oauth2_bearer_token_error_handler - Proc.new do |e| - response = - case e - when MissingTokenError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new - - when TokenNotFoundError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Bad Access Token.") - - when ExpiredError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token is expired. You can either do re-authorization or token refresh.") - - when RevokedError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token was revoked. You have to re-authorize from the user.") - - when InsufficientScopeError - # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) - # does not include WWW-Authenticate header, which breaks the standard. - Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( - :insufficient_scope, - Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], - { scope: e.scopes }) - end - - response.finish - end - end - end + install_error_responders(base) + end + # Helper Methods for Grape Endpoint + module HelperMethods + # Invokes the doorkeeper guard. # - # Exceptions + # If token is presented and valid, then it sets @current_user. # + # If the token does not have sufficient scopes to cover the requred scopes, + # then it raises InsufficientScopeError. + # + # If the token is expired, then it raises ExpiredError. + # + # If the token is revoked, then it raises RevokedError. + # + # If the token is not found (nil), then it raises TokenNotFoundError. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def doorkeeper_guard!(scopes: []) + if (access_token = find_access_token).nil? + raise TokenNotFoundError - class MissingTokenError < StandardError; end + else + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end - class TokenNotFoundError < StandardError; end + def doorkeeper_guard(scopes: []) + if access_token = find_access_token + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) - class ExpiredError < StandardError; end + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError - class RevokedError < StandardError; end + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError - class InsufficientScopeError < StandardError - attr_reader :scopes - def initialize(scopes) - @scopes = scopes + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end + end + end + + def current_user + @current_user + end + + private + def find_access_token + @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + end + + def doorkeeper_request + @doorkeeper_request ||= ActionDispatch::Request.new(env) + end + + def validate_access_token(access_token, scopes) + Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) + end + end + + module ClassMethods + # Installs the doorkeeper guard on the whole Grape API endpoint. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def guard_all!(scopes: []) + before do + guard! scopes: scopes + end + end + + private + def install_error_responders(base) + error_classes = [ MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] + + base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + end + + def oauth2_bearer_token_error_handler + Proc.new do |e| + response = + case e + when MissingTokenError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new + + when TokenNotFoundError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Bad Access Token.") + + when ExpiredError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token is expired. You can either do re-authorization or token refresh.") + + when RevokedError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token was revoked. You have to re-authorize from the user.") + + when InsufficientScopeError + # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) + # does not include WWW-Authenticate header, which breaks the standard. + Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( + :insufficient_scope, + Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], + { scope: e.scopes }) + end + + response.finish end end end -end \ No newline at end of file + + # + # Exceptions + # + + class MissingTokenError < StandardError; end + + class TokenNotFoundError < StandardError; end + + class ExpiredError < StandardError; end + + class RevokedError < StandardError; end + + class InsufficientScopeError < StandardError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes + end + end +end diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 7cd8b6fbae..353c4ddebf 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -3,7 +3,7 @@ Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file} module Ci module API class API < Grape::API - include ::API::APIGuard + include APIGuard version 'v1', using: :path rescue_from ActiveRecord::RecordNotFound do From 0094d8f19644152a66b9d21b8cd86f797199311f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:29:57 +0300 Subject: [PATCH 066/449] Rename `images` to `container_registry` --- app/controllers/projects_controller.rb | 2 +- app/models/ability.rb | 12 ++++++++---- app/models/ci/build.rb | 1 - app/models/project.rb | 8 +++++--- app/views/projects/edit.html.haml | 8 ++++---- config/gitlab.yml.example | 1 + config/initializers/1_settings.rb | 12 ++++++------ .../20160407120251_add_images_enabled_for_project.rb | 2 +- db/schema.rb | 2 +- doc/permissions/permissions.md | 2 ++ lib/api/entities.rb | 2 +- lib/api/projects.rb | 10 +++++----- 12 files changed, 35 insertions(+), 27 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 52f7b99334..f4ec60ad2c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -235,7 +235,7 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :images_enabled, + :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled, :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, diff --git a/app/models/ability.rb b/app/models/ability.rb index ba27b9a9b1..59d5195f5b 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -203,7 +203,7 @@ class Ability :admin_label, :read_commit_status, :read_build, - :read_image, + :read_container_registry, ] end @@ -218,8 +218,8 @@ class Ability :create_merge_request, :create_wiki, :push_code, - :create_image, - :update_image, + :create_container_registry, + :update_container_registry, ] end @@ -246,7 +246,7 @@ class Ability :admin_project, :admin_commit_status, :admin_build, - :admin_image + :admin_container_registry, ] end @@ -291,6 +291,10 @@ class Ability rules += named_abilities('build') end + unless project.container_registry_enabled + rules += named_abilities('container_registry') + end + rules end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 2fea804714..4bc3a225e2 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -426,7 +426,6 @@ module Ci variables << { key: :CI_BUILD_NAME, value: name, public: true } variables << { key: :CI_BUILD_STAGE, value: stage, public: true } variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request - variables << { key: :CI_DOCKER_REGISTRY, value: project.registry_repository_url, public: true } if project.registry_repository_url variables end end diff --git a/app/models/project.rb b/app/models/project.rb index bce2545537..ab9ee9bad0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -69,7 +69,7 @@ class Project < ActiveRecord::Base default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets - default_value_for :images_enabled, gitlab_config_features.images + default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at @@ -375,8 +375,10 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end - def registry_repository_url - "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" if images_enabled? && Gitlab.config.registry.enabled + def container_registry_url + if container_registry_enabled? && Gitlab.config.registry.enabled + "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" + end end def commit(id = 'HEAD') diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 5c7960031e..f6a53fddf1 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -88,11 +88,11 @@ .form-group .col-sm-offset-2.col-sm-10 .checkbox - = f.label :images_enabled do - = f.check_box :images_enabled - %strong Images + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry %br - %span.descr Use Docker Registry for this repository + %span.descr Enable Container Registry for this repository = render 'builds_settings', f: f diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index cbb7c656fe..f7a5875342 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -98,6 +98,7 @@ production: &base wiki: true snippets: false builds: true + container_registry: true ## Webhook settings # Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b94f3f2f90..140d086054 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -230,12 +230,12 @@ Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 Settings.gitlab['session_expire_delay'] ||= 10080 -Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? -Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? -Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? -Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? -Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? -Settings.gitlab.default_projects_features['images'] = true if Settings.gitlab.default_projects_features['images'].nil? +Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? +Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? +Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? +Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? +Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? +Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb index 6a221a7fb0..47f0ca8e8d 100644 --- a/db/migrate/20160407120251_add_images_enabled_for_project.rb +++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb @@ -1,5 +1,5 @@ class AddImagesEnabledForProject < ActiveRecord::Migration def change - add_column :projects, :images_enabled, :boolean + add_column :projects, :container_registry_enabled, :boolean end end diff --git a/db/schema.rb b/db/schema.rb index 7ea16e2135..103a18d362 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -760,7 +760,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" - t.boolean "images_enabled" + t.boolean "container_registry_enabled" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 6219693b8a..6be5ea0b48 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -27,6 +27,7 @@ documentation](../workflow/add-user/add-user.md). | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | | See a commit status | | ✓ | ✓ | ✓ | ✓ | +| See a container registry | | ✓ | ✓ | ✓ | ✓ | | Manage merge requests | | | ✓ | ✓ | ✓ | | Create new merge request | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ | @@ -37,6 +38,7 @@ documentation](../workflow/add-user/add-user.md). | Write a wiki | | | ✓ | ✓ | ✓ | | Cancel and retry builds | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ | +| Update a container registry | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 95c3597b03..d62575e0a3 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -66,7 +66,7 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :images_enabled + expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled expose :created_at, :last_activity_at expose :shared_runners_enabled expose :creator_id diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6f85bc4b1b..d14b28e17f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -94,7 +94,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) - # images_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # namespace_id (optional) - defaults to user namespace # public (optional) - if true same as setting visibility_level = 20 @@ -113,7 +113,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, - :images_enabled, + :container_registry_enabled, :shared_runners_enabled, :namespace_id, :public, @@ -145,7 +145,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) - # images_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) @@ -209,7 +209,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) - # images_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project @@ -226,7 +226,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, - :images_enabled, + :container_registry_enabled, :shared_runners_enabled, :public, :visibility_level, From 1dea54c2201ec5f51bcd8772757dbc6612cc9fdc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:35:01 +0300 Subject: [PATCH 067/449] Remove unused parts --- config/gitlab.yml.example | 1 - config/initializers/1_settings.rb | 1 - lib/api/api.rb | 1 - lib/api/auth.rb | 187 ------------------------------ 4 files changed, 190 deletions(-) delete mode 100644 lib/api/auth.rb diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index f7a5875342..37639e52e6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -184,7 +184,6 @@ production: &base # internal_host: localhost # key: config/registry.key # issuer: omnibus-certificate - # path: shared/registry # # 2. GitLab CI settings diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 140d086054..1040d840e3 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -272,7 +272,6 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes # Settings['registry'] ||= Settingslogic.new({}) Settings.registry['registry'] = false if Settings.registry['enabled'].nil? -Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) Settings.registry['host'] ||= "example.com" Settings.registry['internal_host']||= "localhost" Settings.registry['key'] ||= nil diff --git a/lib/api/api.rb b/lib/api/api.rb index 6ddfe11d98..cc1004f800 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -58,6 +58,5 @@ module API mount Variables mount Runners mount Licenses - mount Auth end end diff --git a/lib/api/auth.rb b/lib/api/auth.rb deleted file mode 100644 index dab04bca81..0000000000 --- a/lib/api/auth.rb +++ /dev/null @@ -1,187 +0,0 @@ -module API - # Projects builds API - class Auth < Grape::API - namespace 'auth' do - get 'token' do - required_attributes! [:service] - keys = attributes_for_keys [:offline_token, :scope, :service] - - case keys[:service] - when 'docker' - docker_token_auth(keys[:scope], keys[:offline_token]) - else - not_found! - end - end - end - - helpers do - def docker_token_auth(scope, offline_token) - auth! - - if offline_token - forbidden! unless @user - elsif scope - @type, @path, actions = scope.split(':', 3) - bad_request!("invalid type: #{@type}") unless @type == 'repository' - - @actions = actions.split(',') - bad_request!('missing actions') if @actions.empty? - - @project = Project.find_with_namespace(@path) - not_found!('Project') unless @project - - authorize_actions!(@actions) - end - - { token: encode(docker_payload) } - end - - def auth! - auth = BasicRequest.new(request.env) - return unless auth.provided? - - return bad_request unless auth.basic? - - # Authentication with username and password - login, password = auth.credentials - - if ci_request?(login, password) - @ci = true - return - end - - @user = authenticate_user(login, password) - - if @user - request.env['REMOTE_USER'] = @user.username - end - end - - def ci_request?(login, password) - matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) - - if @project && matched_login.present? - underscored_service = matched_login['s'].underscore - - if underscored_service == 'gitlab_ci' - return @project.valid_build_token?(password) - end - end - - false - end - - def authenticate_user(login, password) - user = Gitlab::Auth.new.find(login, password) - - # If the user authenticated successfully, we reset the auth failure count - # from Rack::Attack for that IP. A client may attempt to authenticate - # with a username and blank password first, and only after it receives - # a 401 error does it present a password. Resetting the count prevents - # false positives from occurring. - # - # Otherwise, we let Rack::Attack know there was a failed authentication - # attempt from this IP. This information is stored in the Rails cache - # (Redis) and will be used by the Rack::Attack middleware to decide - # whether to block requests from this IP. - config = Gitlab.config.rack_attack.git_basic_auth - - if config.enabled - if user - # A successful login will reset the auth failure count from this IP - Rack::Attack::Allow2Ban.reset(@request.ip, config) - else - banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do - # Unless the IP is whitelisted, return true so that Allow2Ban - # increments the counter (stored in Rails.cache) for the IP - if config.ip_whitelist.include?(@request.ip) - false - else - true - end - end - - if banned - Rails.logger.info "IP #{@request.ip} failed to login " \ - "as #{login} but has been temporarily banned from Git auth" - end - end - end - - user - end - - def docker_payload - issued_at = Time.now - { - access: [ - type: @type, - name: @path, - actions: @actions - ], - iss: Gitlab.config.registry.issuer, - aud: "docker", - sub: @user.try(:username), - aud: @service, - iat: issued_at, - nbf: issued_at - 5.seconds, - exp: issued_at + 60.minutes, - jti: SecureRandom.uuid, - exp: Time.now.to_i + 3600 - }.compact - end - - def private_key - @private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key - end - - def encode(payload) - headers = { - kid: kid(private_key) - } - JWT.encode(payload, private_key, 'RS256', headers) - end - - def authorize_actions!(actions) - actions.each do |action| - forbidden! unless can_access?(action) - end - end - - def can_access?(action) - case action - when 'pull' - @ci || can?(@user, :download_code, @project) - when 'push' - @ci || can?(@user, :push_code, @project) - else - false - end - end - - def kid(private_key) - sha256 = Digest::SHA256.new - sha256.update(private_key.public_key.to_der) - payload = StringIO.new(sha256.digest).read(30) - Base32.encode(payload).split("").each_slice(4).each_with_object([]) do |slice, mem| - mem << slice.join - end.join(":") - end - - class BasicRequest < Rack::Auth::AbstractRequest - def basic? - "basic" == scheme - end - - def credentials - @credentials ||= params.unpack("m*").first.split(/:/, 2) - end - - def username - credentials.first - end - end - end - end -end From 04badd28f969ea2cf7b2d767f34fa67f0dcdc980 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:35:10 +0300 Subject: [PATCH 068/449] Added CHANGELOG entry --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index fed3caef7e..7cccb9c946 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.8.0 (unreleased) - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project - Update SVG sanitizer to conform to SVG 1.1 - Updated search UI + - Added authentication service for Container Registry - Display informative message when new milestone is created - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view From 9f679ac2079dc1d412aaaed806c3d06bdc071046 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:35:25 +0300 Subject: [PATCH 069/449] Update docs/api/projects.md with container registry enabled option --- doc/api/projects.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/api/projects.md b/doc/api/projects.md index de1faadebf..f5f195b97d 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -424,6 +424,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -447,6 +448,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -472,6 +474,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `public_builds` (optional) From daca2144c80546169fb35fcf76b1f3d052b643cc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 20:47:06 +0300 Subject: [PATCH 070/449] Make code more clear in what is done --- app/controllers/jwt_controller.rb | 9 +++++---- .../jwt/docker_authentication_service.rb | 18 +++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 2a92627cb1..9bf1ddbba2 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -8,8 +8,9 @@ class JwtController < ApplicationController def auth @authenticated = authenticate_with_http_basic do |login, password| - @ci_project = ci_project(login, password) - @user = authenticate_user(login, password) unless @ci_project + # if it's possible we first try to authenticate project with login and password + @project = authenticate_project(login, password) + @user = authenticate_user(login, password) unless @project end unless @authenticated @@ -19,7 +20,7 @@ class JwtController < ApplicationController service = SERVICES[params[:service]] head :not_found unless service - result = service.new(@ci_project, @user, auth_params).execute + result = service.new(@project, @user, auth_params).execute return head result[:http_status] if result[:http_status] render json: result @@ -31,7 +32,7 @@ class JwtController < ApplicationController params.permit(:service, :scope, :offline_token, :account, :client_id) end - def ci_project(login, password) + def authenticate_project(login, password) matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) if matched_login.present? diff --git a/app/services/jwt/docker_authentication_service.rb b/app/services/jwt/docker_authentication_service.rb index ce28085e5d..fb0c41a12f 100644 --- a/app/services/jwt/docker_authentication_service.rb +++ b/app/services/jwt/docker_authentication_service.rb @@ -5,12 +5,12 @@ module Jwt return error('forbidden', 403) unless current_user end - { token: token.encoded } + { token: authorized_token.encoded } end private - def token + def authorized_token token = ::Jwt::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = params[:service] @@ -37,22 +37,22 @@ module Jwt end def process_repository_access(type, name, actions) - current_project = Project.find_with_namespace(name) - return unless current_project + requested_project = Project.find_with_namespace(name) + return unless requested_project actions = actions.select do |action| - can_access?(current_project, action) + can_access?(requested_project, action) end { type: type, name: name, actions: actions } if actions end - def can_access?(current_project, action) - case action + def can_access?(requested_project, requested_action) + case requested_action when 'pull' - current_project == project || can?(current_user, :download_code, current_project) + requested_project.public? || requested_project == project || can?(current_user, :download_code, requested_project) when 'push' - current_project == project || can?(current_user, :push_code, current_project) + requested_project == project || can?(current_user, :push_code, requested_project) else false end From b180d79cdca2ce0f6aa7425baf47db5b9c1ec2e3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 22:04:42 +0300 Subject: [PATCH 071/449] Rename DockerAuthenticationService to ContainerRegistryAuthenticationService --- app/controllers/jwt_controller.rb | 2 +- ...tainer_registry_authentication_service.rb} | 22 +++++++++++-------- lib/jwt/token.rb | 14 +++++------- 3 files changed, 20 insertions(+), 18 deletions(-) rename app/services/jwt/{docker_authentication_service.rb => container_registry_authentication_service.rb} (68%) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 9bf1ddbba2..0048a1a31e 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -3,7 +3,7 @@ class JwtController < ApplicationController skip_before_action :verify_authenticity_token SERVICES = { - 'docker' => Jwt::DockerAuthenticationService, + 'container_registry' => Jwt::ContainerRegistryAuthenticationService, } def auth diff --git a/app/services/jwt/docker_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb similarity index 68% rename from app/services/jwt/docker_authentication_service.rb rename to app/services/jwt/container_registry_authentication_service.rb index fb0c41a12f..b9fcd38047 100644 --- a/app/services/jwt/docker_authentication_service.rb +++ b/app/services/jwt/container_registry_authentication_service.rb @@ -1,16 +1,18 @@ module Jwt - class DockerAuthenticationService < BaseService + class ContainerRegistryAuthenticationService < BaseService def execute if params[:offline_token] return error('forbidden', 403) unless current_user end - { token: authorized_token.encoded } + return error('forbidden', 401) if scopes.empty? + + { token: authorized_token(scopes).encoded } end private - def authorized_token + def authorized_token(access) token = ::Jwt::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = params[:service] @@ -19,11 +21,13 @@ module Jwt token end - def access + def scopes return unless params[:scope] - scope = process_scope(params[:scope]) - [scope].compact + @scopes ||= begin + scope = process_scope(params[:scope]) + [scope].compact + end end def process_scope(scope) @@ -44,15 +48,15 @@ module Jwt can_access?(requested_project, action) end - { type: type, name: name, actions: actions } if actions + { type: type, name: name, actions: actions } if actions.present? end def can_access?(requested_project, requested_action) case requested_action when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :download_code, requested_project) + requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) when 'push' - requested_project == project || can?(current_user, :push_code, requested_project) + requested_project == project || can?(current_user, :create_container_registry, requested_project) else false end diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb index 38cbc8004e..765ab0d60c 100644 --- a/lib/jwt/token.rb +++ b/lib/jwt/token.rb @@ -4,19 +4,21 @@ module Jwt attr_accessor :issued_at, :not_before, :expire_time def initialize - @payload = {} @id = SecureRandom.uuid @issued_at = Time.now + # we give a few seconds for time shift @not_before = issued_at - 5.seconds + # default 60 seconds should be more than enough for this authentication token @expire_time = issued_at + 1.minute + @custom_payload = {} end def [](key) - @payload[key] + @custom_payload[key] end def []=(key, value) - @payload[key] = value + @custom_payload[key] = value end def encoded @@ -24,11 +26,7 @@ module Jwt end def payload - @payload.merge(default_payload) - end - - def to_json - payload.to_json + @custom_payload.merge(default_payload) end private From c78b97df0eb275415d6ed5ef297841ee2f61b473 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 22:06:32 +0300 Subject: [PATCH 072/449] Added rspec for testing container registry authentication service --- spec/lib/jwt/rsa_token_spec.rb | 31 +++ spec/lib/jwt/token_spec.rb | 18 ++ ...er_registry_authentication_service_spec.rb | 192 ++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 spec/lib/jwt/rsa_token_spec.rb create mode 100644 spec/lib/jwt/token_spec.rb create mode 100644 spec/services/jwt/container_registry_authentication_service_spec.rb diff --git a/spec/lib/jwt/rsa_token_spec.rb b/spec/lib/jwt/rsa_token_spec.rb new file mode 100644 index 0000000000..710801923e --- /dev/null +++ b/spec/lib/jwt/rsa_token_spec.rb @@ -0,0 +1,31 @@ +describe Jwt::RSAToken do + let(:rsa_key) { generate_key } + let(:rsa_token) { described_class.new(nil) } + let(:rsa_encoded) { rsa_token.encoded } + + before { allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) } + + context 'token' do + context 'for valid key to be validated' do + before { rsa_token['key'] = 'value' } + + subject { JWT.decode(rsa_encoded, rsa_key) } + + it { expect{subject}.to_not raise_error } + it { expect(subject.first).to include('key' => 'value') } + end + + context 'for invalid key to raise an exception' do + let(:new_key) { generate_key } + subject { JWT.decode(rsa_encoded, new_key) } + + it { expect{subject}.to raise_error(JWT::DecodeError) } + end + end + + private + + def generate_key + OpenSSL::PKey::RSA.generate(512) + end +end diff --git a/spec/lib/jwt/token_spec.rb b/spec/lib/jwt/token_spec.rb new file mode 100644 index 0000000000..a56b4cf39b --- /dev/null +++ b/spec/lib/jwt/token_spec.rb @@ -0,0 +1,18 @@ +describe Jwt::Token do + let(:token) { described_class.new } + + context 'custom parameters' do + let(:value) { 'value' } + before { token[:key] = value } + + it { expect(token[:key]).to eq(value) } + it { expect(token.payload).to include(key: value) } + end + + context 'embeds default payload' do + subject { token.payload } + let(:default) { token.send(:default_payload) } + + it { is_expected.to include(default) } + end +end diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/jwt/container_registry_authentication_service_spec.rb new file mode 100644 index 0000000000..ea91f499d0 --- /dev/null +++ b/spec/services/jwt/container_registry_authentication_service_spec.rb @@ -0,0 +1,192 @@ +require 'spec_helper' + +describe Jwt::ContainerRegistryAuthenticationService, services: true do + let(:current_project) { nil } + let(:current_user) { nil } + let(:current_params) { {} } + let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } + let(:registry_settings) { + { + issuer: 'rspec', + key: nil + } + } + let(:payload) { JWT.decode(subject[:token], rsa_key).first } + + subject { described_class.new(current_project, current_user, current_params).execute } + + before do + allow(Gitlab.config.registry).to receive_messages(registry_settings) + allow_any_instance_of(Jwt::RSAToken).to receive(:key).and_return(rsa_key) + end + + shared_examples 'an authenticated' do + it { is_expected.to include(:token) } + it { expect(payload).to include('access') } + end + + shared_examples 'a accessible' do + let(:access) { + [{ + 'type' => 'repository', + 'name' => project.path_with_namespace, + 'actions' => actions, + }] + } + + it_behaves_like 'an authenticated' + it { expect(payload).to include('access' => access) } + end + + shared_examples 'a pullable' do + it_behaves_like 'a accessible' do + let(:actions) { ['pull'] } + end + end + + shared_examples 'a pushable' do + it_behaves_like 'a accessible' do + let(:actions) { ['push'] } + end + end + + shared_examples 'a pullable and pushable' do + it_behaves_like 'a accessible' do + let(:actions) { ['pull', 'push'] } + end + end + + shared_examples 'a forbidden' do + it { is_expected.to include(http_status: 401) } + it { is_expected.to_not include(:token) } + end + + context 'user authorization' do + let(:project) { create(:project) } + let(:current_user) { create(:user) } + + context 'allow developer to push images' do + before { project.team << [current_user, :developer] } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:push" } + } + + it_behaves_like 'a pushable' + end + + context 'allow reporter to pull images' do + before { project.team << [current_user, :reporter] } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull" } + } + + it_behaves_like 'a pullable' + end + + context 'return a least of privileges' do + before { project.team << [current_user, :reporter] } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:push,pull" } + } + + it_behaves_like 'a pullable' + end + + context 'disallow guest to pull or push images' do + before { project.team << [current_user, :guest] } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull,push" } + } + + it_behaves_like 'a forbidden' + end + end + + context 'project authorization' do + let(:current_project) { create(:empty_project) } + + context 'allow to pull and push images' do + let(:current_params) { + { scope: "repository:#{current_project.path_with_namespace}:pull,push" } + } + + it_behaves_like 'a pullable and pushable' do + let(:project) { current_project } + end + end + + context 'for other projects' do + context 'when pulling' do + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull" } + } + + context 'allow for public' do + let(:project) { create(:empty_project, :public) } + it_behaves_like 'a pullable' + end + + context 'disallow for private' do + let(:project) { create(:empty_project, :private) } + it_behaves_like 'a forbidden' + end + end + + context 'when pushing' do + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:push" } + } + + context 'disallow for all' do + let(:project) { create(:empty_project, :public) } + it_behaves_like 'a forbidden' + end + end + + end + end + + context 'unauthorized' do + context 'for invalid scope' do + let(:current_params) { + { scope: 'invalid:aa:bb' } + } + + it_behaves_like 'a forbidden' + end + + context 'for private project' do + let(:project) { create(:empty_project, :private) } + + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull" } + } + + it_behaves_like 'a forbidden' + end + + context 'for public project' do + let(:project) { create(:empty_project, :public) } + + context 'when pulling and pushing' do + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:pull,push" } + } + + it_behaves_like 'a pullable' + end + + context 'when pushing' do + let(:current_params) { + { scope: "repository:#{project.path_with_namespace}:push" } + } + + it_behaves_like 'a forbidden' + end + end + end +end From 08396be619eee2e71c2f5f7aa27eea6f5ddf10ff Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 22:14:46 +0300 Subject: [PATCH 073/449] Rename ImageRegistry to ContainerRegistry --- .../projects/container_registry_controller.rb | 9 +++------ app/models/project.rb | 8 +++++--- lib/{image_registry => container_registry}/blob.rb | 2 +- lib/{image_registry => container_registry}/client.rb | 2 +- lib/{image_registry => container_registry}/config.rb | 2 +- lib/{image_registry => container_registry}/registry.rb | 6 +++--- lib/{image_registry => container_registry}/repository.rb | 6 +++--- lib/{image_registry => container_registry}/tag.rb | 8 ++++---- 8 files changed, 21 insertions(+), 22 deletions(-) rename lib/{image_registry => container_registry}/blob.rb (97%) rename lib/{image_registry => container_registry}/client.rb (98%) rename lib/{image_registry => container_registry}/config.rb (90%) rename lib/{image_registry => container_registry}/registry.rb (53%) rename lib/{image_registry => container_registry}/repository.rb (89%) rename lib/{image_registry => container_registry}/tag.rb (86%) diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index ffd455e647..94f7580f0e 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -5,10 +5,7 @@ class Projects::ContainerRegistryController < Projects::ApplicationController layout 'project' def index - @tags = container_registry.tags - - other_repository = container_registry.registry["gitlab/gitlab-test3"] - container_registry.copy_to(other_repository) + @tags = container_registry_repository.tags end def destroy @@ -21,8 +18,8 @@ class Projects::ContainerRegistryController < Projects::ApplicationController private - def container_registry - @container_registry ||= project.container_registry + def container_registry_repository + @container_registry_repository ||= project.container_registry_repository end def tag diff --git a/app/models/project.rb b/app/models/project.rb index f81d9a5262..d384c58ca3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -376,9 +376,11 @@ class Project < ActiveRecord::Base end def container_registry - @registry_token ||= Jwt::DockerAuthenticationService.full_access_token(path_with_namespace) - @registry ||= ImageRegistry::Registry.new(Gitlab.config.registry.api_url, token: @registry_token) - @container_registry ||= ImageRegistry::Repository.new(@registry, path_with_namespace) + @container_registry_repository ||= begin + token = Jwt::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace) + registry = ContainerRegistry::Registry.new(Gitlab.config.registry.api_url, token: token) + registry[path_with_namespace] + end end def container_registry_url diff --git a/lib/image_registry/blob.rb b/lib/container_registry/blob.rb similarity index 97% rename from lib/image_registry/blob.rb rename to lib/container_registry/blob.rb index 43665149e2..e0d9923f21 100644 --- a/lib/image_registry/blob.rb +++ b/lib/container_registry/blob.rb @@ -1,4 +1,4 @@ -module ImageRegistry +module ContainerRegistry class Blob attr_reader :repository, :config diff --git a/lib/image_registry/client.rb b/lib/container_registry/client.rb similarity index 98% rename from lib/image_registry/client.rb rename to lib/container_registry/client.rb index a443205909..b823428344 100644 --- a/lib/image_registry/client.rb +++ b/lib/container_registry/client.rb @@ -1,7 +1,7 @@ require 'faraday' require 'faraday_middleware' -module ImageRegistry +module ContainerRegistry class Client attr_accessor :uri diff --git a/lib/image_registry/config.rb b/lib/container_registry/config.rb similarity index 90% rename from lib/image_registry/config.rb rename to lib/container_registry/config.rb index 1c2abec1bf..626b36cbaa 100644 --- a/lib/image_registry/config.rb +++ b/lib/container_registry/config.rb @@ -1,4 +1,4 @@ -module ImageRegistry +module ContainerRegistry class Config attr_reader :tag, :blob, :data diff --git a/lib/image_registry/registry.rb b/lib/container_registry/registry.rb similarity index 53% rename from lib/image_registry/registry.rb rename to lib/container_registry/registry.rb index d8de8e392e..f866862db2 100644 --- a/lib/image_registry/registry.rb +++ b/lib/container_registry/registry.rb @@ -1,14 +1,14 @@ -module ImageRegistry +module ContainerRegistry class Registry attr_reader :uri, :client def initialize(uri, options = {}) @uri = URI.parse(uri) - @client = ImageRegistry::Client.new(uri, options) + @client = ContainerRegistry::Client.new(uri, options) end def [](name) - ImageRegistry::Repository.new(self, name) + ContainerRegistry::Repository.new(self, name) end end end diff --git a/lib/image_registry/repository.rb b/lib/container_registry/repository.rb similarity index 89% rename from lib/image_registry/repository.rb rename to lib/container_registry/repository.rb index 43e8e7720d..c930878d26 100644 --- a/lib/image_registry/repository.rb +++ b/lib/container_registry/repository.rb @@ -1,4 +1,4 @@ -module ImageRegistry +module ContainerRegistry class Repository attr_reader :registry, :name @@ -11,7 +11,7 @@ module ImageRegistry end def [](tag) - ImageRegistry::Tag.new(self, tag) + ContainerRegistry::Tag.new(self, tag) end def manifest @@ -27,7 +27,7 @@ module ImageRegistry return @tags if defined?(@tags) return [] unless manifest && manifest['tags'] @tags = manifest['tags'].map do |tag| - ImageRegistry::Tag.new(self, tag) + ContainerRegistry::Tag.new(self, tag) end @tags ||= [] end diff --git a/lib/image_registry/tag.rb b/lib/container_registry/tag.rb similarity index 86% rename from lib/image_registry/tag.rb rename to lib/container_registry/tag.rb index 76946a6ce5..324778bdf2 100644 --- a/lib/image_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -1,4 +1,4 @@ -module ImageRegistry +module ContainerRegistry class Tag attr_reader :repository, :name @@ -28,12 +28,12 @@ module ImageRegistry def config_blob return @config_blob if defined?(@config_blob) return unless manifest && manifest['config'] - @config_blob = ImageRegistry::Blob.new(repository, manifest['config']) + @config_blob = ContainerRegistry::Blob.new(repository, manifest['config']) end def config return unless config_blob - @config ||= ImageRegistry::Config.new(self, config_blob) + @config ||= ContainerRegistry::Config.new(self, config_blob) end def created_at @@ -45,7 +45,7 @@ module ImageRegistry return @layers if defined?(@layers) return unless manifest @layers = manifest['layers'].map do |layer| - ImageRegistry::Blob.new(repository, layer) + ContainerRegistry::Blob.new(repository, layer) end end From d7b91fb596e50411c0adb5aa8fb17341087f3f57 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 22:27:06 +0300 Subject: [PATCH 074/449] Simplify Container Registry view implementation --- app/models/project.rb | 10 +++-- .../container_registry/index.html.haml | 41 ++++++++++--------- lib/container_registry/blob.rb | 4 ++ lib/container_registry/registry.rb | 3 +- lib/container_registry/repository.rb | 4 ++ lib/container_registry/tag.rb | 4 ++ 6 files changed, 41 insertions(+), 25 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index d384c58ca3..36ce472f3a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -375,17 +375,19 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end - def container_registry + def container_registry_repository @container_registry_repository ||= begin token = Jwt::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace) - registry = ContainerRegistry::Registry.new(Gitlab.config.registry.api_url, token: token) + url = Gitlab.config.registry.api_url + host_port = Gitlab.config.registry.host_port + registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) registry[path_with_namespace] end end - def container_registry_url + def container_registry_repository_url if container_registry_enabled? && Gitlab.config.registry.enabled - "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" + "#{Gitlab.config.registry.host_port}/#{path_with_namespace}" end end diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index 1ac3a62f54..5b7dd27ace 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -1,35 +1,36 @@ - page_title "Container Registry" = render "header_title" -.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} - %hr %ul.content-list - if @tags.blank? %li - .nothing-here-block No images to show + .nothing-here-block No images in Container Registry for this project. + + .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 #{escape_once(@project.container_registry_repository_url)} . + %br + docker push #{escape_once(@project.container_registry_repository_url)} + - else .table-holder %table.table.builds %thead %tr %th Name - %th Digest + %th Image ID %th Size %th Created %th @@ -37,8 +38,8 @@ - @tags.each do |tag| %tr %td - #{tag.repository.name}:#{tag.name} - = clipboard_button(clipboard_text: "docker pull #{Gitlab.config.registry.host_port}/#{tag.repository.name}:#{tag.name}") + = escape_once(tag.name) + = clipboard_button(clipboard_text: "docker pull #{tag.path}") %td - if layer = tag.layers.first %span.has-tooltip(title="#{layer.revision}") diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb index e0d9923f21..16e3f85341 100644 --- a/lib/container_registry/blob.rb +++ b/lib/container_registry/blob.rb @@ -11,6 +11,10 @@ module ContainerRegistry digest.present? end + def path + "#{repository.path}@#{digest}" + end + def digest config['digest'] end diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb index f866862db2..a86ddb9326 100644 --- a/lib/container_registry/registry.rb +++ b/lib/container_registry/registry.rb @@ -1,8 +1,9 @@ module ContainerRegistry class Registry - attr_reader :uri, :client + attr_reader :uri, :client, :path def initialize(uri, options = {}) + @path = uri || options[:path] @uri = URI.parse(uri) @client = ContainerRegistry::Client.new(uri, options) end diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb index c930878d26..f01330f364 100644 --- a/lib/container_registry/repository.rb +++ b/lib/container_registry/repository.rb @@ -10,6 +10,10 @@ module ContainerRegistry @client ||= registry.client end + def path + [registry.path, name].compact.join('/') + end + def [](tag) ContainerRegistry::Tag.new(self, tag) end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 324778bdf2..14cee8be88 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -15,6 +15,10 @@ module ContainerRegistry @manifest = client.repository_manifest(repository.name, name) end + def path + "#{repository.path}:#{name}" + end + def [](key) return unless manifest manifest[key] From 60869580023da553613ee18bcdbc8682ebdde2cf Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 22:34:10 +0300 Subject: [PATCH 075/449] Rename image to container_registry --- app/controllers/projects/container_registry_controller.rb | 6 +++--- app/helpers/projects_helper.rb | 4 ++-- app/views/layouts/nav/_project.html.haml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index 94f7580f0e..82c621b29e 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -1,6 +1,6 @@ class Projects::ContainerRegistryController < Projects::ApplicationController - before_action :authorize_read_image! - before_action :authorize_update_image!, only: [:destroy] + before_action :authorize_read_container_registry! + before_action :authorize_update_container_registry!, only: [:destroy] before_action :tag, except: [:index] layout 'project' @@ -23,6 +23,6 @@ class Projects::ContainerRegistryController < Projects::ApplicationController end def tag - @tag ||= container_registry[params[:id]] + @tag ||= container_registry_repository[params[:id]] end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a76b5e2260..664fdb6d74 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -152,8 +152,8 @@ module ProjectsHelper nav_tabs << :builds end - if can?(current_user, :read_image, project) - nav_tabs << :images + if can?(current_user, :read_container_registry, project) + nav_tabs << :container_registry end if can?(current_user, :admin_project, project) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 8448599c1c..d3d715aad3 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -46,9 +46,9 @@ Builds %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) - - if project_nav_tab? :images + - if project_nav_tab? :container_registry = nav_link(controller: %w(container_registry)) do - = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-images' do + = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do = icon('hdd-o fw') %span Container Registry From b5043d5d33ed2a213889dfe52c819addd0d847ef Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 22:34:24 +0300 Subject: [PATCH 076/449] Fix review comments --- doc/permissions/permissions.md | 1 + lib/container_registry/blob.rb | 1 - lib/container_registry/client.rb | 33 ++++++++++++++++++-------------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 6be5ea0b48..30f6c75e1c 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -39,6 +39,7 @@ documentation](../workflow/add-user/add-user.md). | Cancel and retry builds | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ | | Update a container registry | | | ✓ | ✓ | ✓ | +| Remove a container registry images | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb index 16e3f85341..d59792a383 100644 --- a/lib/container_registry/blob.rb +++ b/lib/container_registry/blob.rb @@ -44,7 +44,6 @@ module ContainerRegistry end def data - return @data if defined?(@data) @data ||= client.blob(repository.name, digest, type) end diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index b823428344..41d9cb46ae 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -9,20 +9,8 @@ module ContainerRegistry 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(:authorization, :bearer, options[:token].to_s) - end - - builder.adapter :net_http + @faraday = Faraday.new(@base_uri) do |conn| + initialize_connection(conn) end end @@ -60,5 +48,22 @@ module ContainerRegistry def delete_blob(name, digest) @faraday.delete("/v2/#{name}/blobs/#{digest}").success? end + + private + + def initialize_connection(conn) + conn.request :json + conn.headers['Accept'] = MANIFEST_VERSION + + conn.response :json, :content_type => /\bjson$/ + + if options[:user] && options[:password] + conn.request(:basic_auth, options[:user].to_s, options[:password].to_s) + elsif options[:token] + conn.request(:authorization, :bearer, options[:token].to_s) + end + + conn.adapter :net_http + end end end From d5d8e76bd79cd9d61c66539a5069104cf46be2bd Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 22:41:48 +0300 Subject: [PATCH 077/449] Block renaming project or repository if it has container registry tags --- app/models/namespace.rb | 8 ++++++++ app/models/project.rb | 11 +++++++++++ app/services/projects/transfer_service.rb | 5 +++++ 3 files changed, 24 insertions(+) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 741e912171..0f61cee788 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -127,6 +127,10 @@ class Namespace < ActiveRecord::Base # Ensure old directory exists before moving it gitlab_shell.add_namespace(path_was) + if any_project_has_container_registry_tags? + raise Exception.new('namespace cannot be moved, because at least one project has tags in container registry') + end + if gitlab_shell.mv_namespace(path_was, path) Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) @@ -148,6 +152,10 @@ class Namespace < ActiveRecord::Base end end + def any_project_has_container_registry_tags? + projects.any?(:has_container_registry_tags?) + end + def send_update_instructions projects.each do |project| project.send_move_instructions("#{path_was}/#{project.path}") diff --git a/app/models/project.rb b/app/models/project.rb index 36ce472f3a..d3ae7803be 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -391,6 +391,12 @@ class Project < ActiveRecord::Base end end + def has_container_registry_tags? + if container_registry_enabled? && Gitlab.config.registry.enabled + container_registry_repository.tags.any? + end + end + def commit(id = 'HEAD') repository.commit(id) end @@ -806,6 +812,11 @@ class Project < ActiveRecord::Base expire_caches_before_rename(old_path_with_namespace) + if has_container_registry_tags? + # we currently doesn't support renaming repository if it contains tags in container registry + raise Exception.new('repository cannot be renamed, due to tags in container registry') + end + if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) # If repository moved successfully we need to send update instructions to users. # However we cannot allow rollback since we moved repository diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 111b3ec05e..0d72286dec 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -34,6 +34,11 @@ module Projects raise TransferError.new("Project with same path in target namespace already exists") end + if project.has_container_registry_tags? + # we currently doesn't support renaming repository if it contains tags in container registry + raise TransferError.new('Repository cannot be renamed, due to tags in container registry') + end + project.expire_caches_before_rename(old_path) # Apply new namespace id and visibility level From d05f0030a3de42ab3ec6d8c8be290b74698bb929 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 23:32:18 +0300 Subject: [PATCH 078/449] Added Docker Registry View tests --- app/controllers/jwt_controller.rb | 2 +- ...ntainer_registry_authentication_service.rb | 6 ++- app/services/projects/destroy_service.rb | 2 +- .../container_registry/_tag.html.haml | 20 +++++++++ .../container_registry/index.html.haml | 23 +--------- lib/container_registry/client.rb | 4 +- spec/features/container_registry_spec.rb | 43 +++++++++++++++++++ .../container_registry/config_blob.json | 1 + .../container_registry/tag_manifest.json | 1 + spec/support/stub_gitlab_calls.rb | 12 ++++++ 10 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 app/views/projects/container_registry/_tag.html.haml create mode 100644 spec/features/container_registry_spec.rb create mode 100644 spec/fixtures/container_registry/config_blob.json create mode 100644 spec/fixtures/container_registry/tag_manifest.json diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 0048a1a31e..07a842970b 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -3,7 +3,7 @@ class JwtController < ApplicationController skip_before_action :verify_authenticity_token SERVICES = { - 'container_registry' => Jwt::ContainerRegistryAuthenticationService, + Jwt::ContainerRegistryAuthenticationService::AUDIENCE => Jwt::ContainerRegistryAuthenticationService, } def auth diff --git a/app/services/jwt/container_registry_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb index 88af4f8361..2edee1f0ab 100644 --- a/app/services/jwt/container_registry_authentication_service.rb +++ b/app/services/jwt/container_registry_authentication_service.rb @@ -1,5 +1,7 @@ module Jwt class ContainerRegistryAuthenticationService < BaseService + AUDIENCE = 'container_registry' + def execute if params[:offline_token] return error('forbidden', 403) unless current_user @@ -14,7 +16,7 @@ module Jwt registry = Gitlab.config.registry token = ::Jwt::RSAToken.new(registry.key) token.issuer = registry.issuer - token.audience = 'docker' + token.audience = AUDIENCE token[:access] = names.map do |name| { type: 'repository', name: name, actions: %w(pull push) } end @@ -26,7 +28,7 @@ module Jwt def authorized_token(access) token = ::Jwt::RSAToken.new(registry.key) token.issuer = registry.issuer - token.audience = params[:service] + token.audience = AUDIENCE token.subject = current_user.try(:username) token[:access] = access token diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index a8b31f95c4..8e2e46346c 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -27,7 +27,7 @@ module Projects project.destroy! unless remove_registry_tags - raise_error('Failed to remove project image registry. Please try again or contact administrator') + raise_error('Failed to remove project container registry. Please try again or contact administrator') end unless remove_repository(repo_path) diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml new file mode 100644 index 0000000000..f97988d20c --- /dev/null +++ b/app/views/projects/container_registry/_tag.html.haml @@ -0,0 +1,20 @@ +%tr.tag + %td + = escape_once(tag.name) + = clipboard_button(clipboard_text: "docker pull #{tag.path}") + %td + - if layer = tag.layers.first + %span.has-tooltip(title="#{layer.revision}") + = layer.short_revision + - else + \- + %td + = number_to_human_size(tag.total_size) + · + = pluralize(tag.layers.size, "layer") + %td + = time_ago_in_words(tag.created_at) + %td.content + .controls.hidden-xs.pull-right + = link_to namespace_project_container_registry_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/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index 5b7dd27ace..701f1a86b5 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -26,7 +26,7 @@ - else .table-holder - %table.table.builds + %table.table.tags %thead %tr %th Name @@ -36,23 +36,4 @@ %th - @tags.each do |tag| - %tr - %td - = escape_once(tag.name) - = clipboard_button(clipboard_text: "docker pull #{tag.path}") - %td - - if layer = tag.layers.first - %span.has-tooltip(title="#{layer.revision}") - = layer.short_revision - - else - \- - %td - = number_to_human_size(tag.total_size) - · - = pluralize(tag.layers.size, "layer") - %td - = time_ago_in_words(tag.created_at) - %td.content - .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_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") + = render 'tag', tag: tag \ No newline at end of file diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index 41d9cb46ae..0bfb6baffd 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -10,7 +10,7 @@ module ContainerRegistry def initialize(base_uri, options = {}) @base_uri = base_uri @faraday = Faraday.new(@base_uri) do |conn| - initialize_connection(conn) + initialize_connection(conn, options) end end @@ -51,7 +51,7 @@ module ContainerRegistry private - def initialize_connection(conn) + def initialize_connection(conn, options) conn.request :json conn.headers['Accept'] = MANIFEST_VERSION diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb new file mode 100644 index 0000000000..7bef7a2ee8 --- /dev/null +++ b/spec/features/container_registry_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe "Container Registry" do + let(:project) { create(:empty_project) } + let(:repository) { project.container_registry_repository } + let(:tag_name) { 'latest' } + let(:tags) { [tag_name] } + + before do + end + + before do + login_as(:user) + project.team << [@user, :developer] + stub_container_registry(*tags) + end + + describe 'GET /:project/container_registry' do + before do + visit namespace_project_container_registry_index_path(project.namespace, project) + end + + context 'when no tags' do + let(:tags) { [] } + it { expect(page).to have_content('No images in Container Registry for this project') } + end + + context 'when there are tags' do + it { expect(page).to have_content(tag_name)} + end + end + + describe 'DELETE /:project/container_registry/tag' do + before do + visit namespace_project_container_registry_index_path(project.namespace, project) + end + + it do + expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true) + click_on 'Remove' + end + end +end \ No newline at end of file diff --git a/spec/fixtures/container_registry/config_blob.json b/spec/fixtures/container_registry/config_blob.json new file mode 100644 index 0000000000..1028c994a2 --- /dev/null +++ b/spec/fixtures/container_registry/config_blob.json @@ -0,0 +1 @@ +{"architecture":"amd64","config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"b14cd82987550b01af9a666a2f4c996280a6152e66873134fae5a0f223dc5976","container_config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"],"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2016-04-01T20:53:00.160300546Z","docker_version":"1.9.1","history":[{"created":"2016-04-01T20:53:00.160300546Z","created_by":"/bin/sh -c #(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c56b7dabbc7aa730eeab07668bdcbd7e3d40855047ca9a0cc1bfed23a2486111"]}} diff --git a/spec/fixtures/container_registry/tag_manifest.json b/spec/fixtures/container_registry/tag_manifest.json new file mode 100644 index 0000000000..1b6008e287 --- /dev/null +++ b/spec/fixtures/container_registry/tag_manifest.json @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]} diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index eec2e68111..6b3a401006 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -25,6 +25,18 @@ module StubGitlabCalls allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false) end + def stub_container_registry(*tags) + allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_tags).and_return( + { "tags" => tags } + ) + allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return( + JSON.load(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json')) + ) + allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return( + File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json') + ) + end + private def gitlab_url From 1ad0c968d579fe6ac0b2fc00a1dae32449ceb2c3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 9 May 2016 23:39:48 +0300 Subject: [PATCH 079/449] Make a build views nicer --- app/models/commit_status.rb | 2 +- app/views/projects/ci/builds/_build.html.haml | 52 +++++++++++++------ .../projects/ci/commits/_commit.html.haml | 6 ++- .../projects/commit/_ci_commit.html.haml | 28 +++++----- app/views/projects/pipelines/index.html.haml | 7 ++- 5 files changed, 61 insertions(+), 34 deletions(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index aa56314aa1..c0ae30820e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -89,7 +89,7 @@ class CommitStatus < ActiveRecord::Base def self.stages order_by = 'max(stage_idx)' - group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact + CommitStatus.where(id: all).group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact end def self.stages_status diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 745ff461d7..7e7e54c68a 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -42,25 +42,43 @@ %td = build.name - .label-container - - if build.tags.any? - - build.tags.each do |tag| - %span.label.label-primary - = tag - - if build.try(:trigger_request) - %span.label.label-info triggered - - if build.try(:allow_failure) - %span.label.label-danger allowed to fail - - if defined?(retried) && retried - %span.label.label-warning retried + .pull-right + .label-container + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail + - if defined?(retried) && retried + %span.label.label-warning retried - %td.duration - - if build.duration - #{duration_in_words(build.finished_at, build.started_at)} + - if defined?(new_duration) && new_duration + %td.duration + - if build.duration + %p + %i.fa.fa-clock-o +   + #{duration_in_words(build.finished_at, build.started_at)} + - if build.finished_at + %p + %i.fa.fa-calendar +   + #{time_ago_with_tooltip(build.finished_at)} + - else + %td.duration + - if build.duration + %i.fa.fa-clock-o +   + #{duration_in_words(build.finished_at, build.started_at)} - %td.timestamp - - if build.finished_at - %span #{time_ago_with_tooltip(build.finished_at)} + %td.timestamp + - if build.finished_at + %i.fa.fa-calendar +   + %span #{time_ago_with_tooltip(build.finished_at)} - if defined?(coverage) && coverage %td.coverage diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index 32f85cb8f8..d5dd6c7b0a 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -35,7 +35,8 @@ - stages.each do |stage| %td - if status = stages_status[stage] - %span.has-tooltip(title="#{stage.titleize}: #{status}"){class: "ci-status-icon-#{status}"} + - tooltip = "#{stage.titleize}: #{status}" + %span.has-tooltip(title="#{tooltip}"){class: "ci-status-icon-#{status}"} = ci_icon_for_status(status) %td @@ -52,7 +53,7 @@ %td .controls.hidden-xs.pull-right - - artifacts = commit.builds.latest + - artifacts = commit.builds.latest.select { |b| b.artifacts? } - if artifacts.present? .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} @@ -66,6 +67,7 @@ %span #{build.name} - if can?(current_user, :update_pipeline, @project) +   - 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") diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 21a3008086..ba0baa1e2a 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -36,17 +36,19 @@ .table-holder %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Stage - %th Name - %th Duration - %th Finished at - - if @project.build_coverage_enabled? - %th Coverage - %th - builds = ci_commit.statuses.latest.ordered - = render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true - = render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false, retried: true + - CommitStatus.where(id: builds).stages.each do |stage| + - stage_builds = builds.where(stage: stage) + %tr + %th{colspan: 10} + %strong + - stage_status = CommitStatus.where(id: stage_builds.ids).status + %span{class: "ci-status-link ci-status-icon-#{stage_status}"} + = ci_icon_for_status(stage_status) +   + = stage.titleize.pluralize + = render stage_builds, coverage: @project.build_coverage_enabled?, tage: false, ref: false, allow_retry: true + = render ci_commit.retried.select { |build| build.stage == stage }, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true + %tr + %td{colspan: 10} +   diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 838b2986d4..0e08753fd3 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -38,7 +38,12 @@ %span CI Lint .gray-content-block - Pipelines for #{(@scope || 'changes')} on this project + - if @scope == 'running' + Running pipelines for this project + - elsif @scope.nil? + Pipelines for this project + - else + #{@scope.titleize} for this project %ul.content-list - stages = @pipelines.stages From 504a1fac95a2af2cf90f00f310d244d8d37f0015 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 10 May 2016 00:58:53 +0300 Subject: [PATCH 080/449] Fix SQL queries for calculating stages status --- app/models/commit_status.rb | 12 +++++++----- app/views/projects/commit/_ci_commit.html.haml | 5 ++--- app/views/projects/commit/_ci_stage.html.haml | 10 +++++----- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index b031259e25..0c0c3d38f9 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -51,7 +51,7 @@ class CommitStatus < ActiveRecord::Base alias_attribute :author, :user scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } - scope :ordered, -> { order(:ref, :stage_idx, :name) } + scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } state_machine :status, initial: :pending do @@ -91,13 +91,15 @@ class CommitStatus < ActiveRecord::Base end def self.stages - order_by = 'max(stage_idx)' - CommitStatus.where(id: all).group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact + # We group by stage name, but order stages by their's index + unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') end def self.stages_status - all.stages.inject({}) do |h, stage| - h[stage] = all.where(stage: stage).status + # We execute subquery for each of the stages which calculates an Stage Status + statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) + statuses.inject({}) do |h, k| + h[k.first] = k.last h end end diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 1a17d1b4ed..c844376361 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -36,6 +36,5 @@ .table-holder %table.table.builds - - stages = ci_commit.statuses.latest.stages_status - - stages.each do |stage, status| - = render 'ci_stage', stage: stage, status: status + - ci_commit.statuses.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: ci_commit.statuses.where(stage: stage) diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml index 0fb428dcd2..f9f2757dc3 100644 --- a/app/views/projects/commit/_ci_stage.html.haml +++ b/app/views/projects/commit/_ci_stage.html.haml @@ -1,15 +1,15 @@ -- statuses = ci_commit.statuses.where(stage: stage).to_a -- retried = statuses.select(&:retried?) -- latest = statuses - retried +- latest = statuses.latest +- retried = statuses.where.not(id: latest) %tr %th{colspan: 10} %strong + - status = latest.status %span{class: "ci-status-link ci-status-icon-#{status}"} = ci_icon_for_status(status)   = stage.titleize.pluralize - = render latest, coverage: @project.build_coverage_enabled?, tage: false, ref: false, allow_retry: true - = render retried, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true + = render latest.ordered, coverage: @project.build_coverage_enabled?, tage: false, ref: false, allow_retry: true + = render retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true %tr %td{colspan: 10}   From e56e3cdc62f96541b9bd8b7814204e92f1909253 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 9 May 2016 19:35:37 -0300 Subject: [PATCH 081/449] Fix api leaking notes when user is not authorized to read noteable --- CHANGELOG | 1 + lib/api/notes.rb | 29 +++++++++++++++++------------ spec/requests/api/notes_spec.rb | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 63077523aa..2c44944f66 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.8.0 (unreleased) - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. - Support e-mail notifications for comments on project snippets + - Fix API leak of notes of unauthorized issues, snippets and merge requests - Use ActionDispatch Remote IP for Akismet checking - Fix error when visiting commit builds page before build was updated - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 71a53e6f0d..4ac08a3e8c 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -20,19 +20,24 @@ module API # GET /projects/:id/snippets/:noteable_id/notes get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + read_ability_name = "read_#{@noteable.class.to_s.underscore.downcase}".to_sym - # We exclude notes that are cross-references and that cannot be viewed - # by the current user. By doing this exclusion at this level and not - # at the DB query level (which we cannot in that case), the current - # page can have less elements than :per_page even if - # there's more than one page. - notes = - # paginate() only works with a relation. This could lead to a - # mismatch between the pagination headers info and the actual notes - # array returned, but this is really a edge-case. - paginate(@noteable.notes). - reject { |n| n.cross_reference_not_visible_for?(current_user) } - present notes, with: Entities::Note + if can?(current_user, read_ability_name, @noteable) + # We exclude notes that are cross-references and that cannot be viewed + # by the current user. By doing this exclusion at this level and not + # at the DB query level (which we cannot in that case), the current + # page can have less elements than :per_page even if + # there's more than one page. + notes = + # paginate() only works with a relation. This could lead to a + # mismatch between the pagination headers info and the actual notes + # array returned, but this is really a edge-case. + paginate(@noteable.notes). + reject { |n| n.cross_reference_not_visible_for?(current_user) } + present notes, with: Entities::Note + else + render_api_error!("Not found.", 404) + end end # Get a single +noteable+ note diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 49091fc0f4..f5b31be1ba 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -57,6 +57,15 @@ describe API::API, api: true do expect(json_response).to be_empty end + context "and issue is confidential" do + before { ext_issue.update_attributes(confidential: true) } + + it "returns 404" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + expect(response.status).to eq(404) + end + end + context "and current user can view the note" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) @@ -80,6 +89,11 @@ describe API::API, api: true do get api("/projects/#{project.id}/snippets/42/notes", user) expect(response.status).to eq(404) end + + it "returns 404 when not authorized" do + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user) + expect(response.status).to eq(404) + end end context "when noteable is a Merge Request" do @@ -94,6 +108,11 @@ describe API::API, api: true do get api("/projects/#{project.id}/merge_requests/4444/notes", user) expect(response.status).to eq(404) end + + it "returns 404 when not authorized" do + get api("/projects/#{project.id}/merge_requests/4444/notes", private_user) + expect(response.status).to eq(404) + end end end From fe2137d871b978033007a3375cf6f1edf0f04cd4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 10 May 2016 02:26:13 +0300 Subject: [PATCH 082/449] Improve pipelines design --- .../projects/pipelines_controller.rb | 65 +++++-------------- app/finders/pipelines_finder.rb | 38 +++++++++++ app/helpers/ci_status_helper.rb | 12 ++-- app/models/ci/commit.rb | 8 +++ app/services/ci/create_pipeline_service.rb | 42 ++++++++++++ app/views/layouts/nav/_project.html.haml | 6 +- app/views/projects/ci/builds/_build.html.haml | 18 ++--- .../projects/ci/commits/_commit.html.haml | 8 +-- spec/models/project_spec.rb | 2 +- 9 files changed, 129 insertions(+), 70 deletions(-) create mode 100644 app/finders/pipelines_finder.rb create mode 100644 app/services/ci/create_pipeline_service.rb diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index aba64e4a73..a261d03f4d 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -4,63 +4,28 @@ class Projects::PipelinesController < Projects::ApplicationController 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_pipelines = project.ci_commits - @pipelines = @all_pipelines.order(id: :desc) - @pipelines = - case @scope - when 'running' - @pipelines.running_or_pending - when 'branches' - @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' - @tags = project.repository.tags.map(&:name) - @tags_ids = @all_pipelines.where(ref: @tags).group(:ref).select('max(id)') - @pipelines.where(id: @tags_ids) - else - @pipelines - end - @pipelines = @pipelines.page(params[:page]).per(30) + @pipelines = PipelinesFinder.new(project).execute(@all_pipelines, @scope) + @pipelines = @pipelines.order(id: :desc).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 + begin + pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute + redirect_to namespace_project_pipeline_path(project.namespace, project, pipeline) + rescue ArgumentError => e + @error = e.message + render 'new' + rescue => e + @error = 'Undefined error' + render 'new' end - - commit = project.commit(params[:ref]) - unless commit - @error = 'Commit not found' - 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 - 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(current_user) - end - - redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.id) end def show @@ -70,19 +35,23 @@ class Projects::PipelinesController < Projects::ApplicationController end def retry - pipeline.builds.latest.failed.select(&:retryable?).each(&:retry) + pipeline.retry_failed redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) end def cancel - pipeline.builds.running_or_pending.each(&:cancel) + pipeline.cancel_running redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) end private + def create_params + params.permit(:ref) + end + def pipeline @pipeline ||= project.ci_commits.find_by!(id: params[:id]) end diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb new file mode 100644 index 0000000000..c19a795d46 --- /dev/null +++ b/app/finders/pipelines_finder.rb @@ -0,0 +1,38 @@ +class PipelinesFinder + attr_reader :project + + def initialize(project) + @project = project + end + + def execute(pipelines, scope) + case scope + when 'running' + pipelines.running_or_pending + when 'branches' + from_ids(pipelines, ids_for_ref(pipelines, branches)) + when 'tags' + from_ids(pipelines, ids_for_ref(pipelines, tags)) + else + pipelines + end + end + + private + + def ids_for_ref(pipelines, refs) + pipelines.where(ref: refs).group(:ref).select('max(id)') + end + + def from_ids(pipelines, ids) + pipelines.unscoped.where(id: ids) + end + + def branches + project.repository.branches.map(&:name) + end + + def tags + project.repository.tags.map(&:name) + end +end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index acc01b008b..cfad17dcac 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -50,6 +50,13 @@ module CiStatusHelper render_status_with_link('pipeline', pipeline.status, path, tooltip_placement) end + def no_runners_for_project?(project) + project.runners.blank? && + Ci::Runner.shared.blank? + end + + private + def render_status_with_link(type, status, path, tooltip_placement) link_to ci_icon_for_status(status), path, @@ -57,9 +64,4 @@ module CiStatusHelper title: "#{type.titleize}: #{ci_label_for_status(status)}", data: { toggle: 'tooltip', placement: tooltip_placement } end - - def no_runners_for_project?(project) - project.runners.blank? && - Ci::Runner.shared.blank? - end end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 759c31b005..71696a3923 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -89,6 +89,14 @@ module Ci end end + def cancel_running + builds.running_or_pending.each(&:cancel) + end + + def retry_failed + builds.latest.failed.select(&:retryable?).each(&:retry) + end + def latest? return false unless ref commit = project.commit(ref) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb new file mode 100644 index 0000000000..40414b4986 --- /dev/null +++ b/app/services/ci/create_pipeline_service.rb @@ -0,0 +1,42 @@ +module Ci + class CreatePipelineService < BaseService + def execute + unless ref_names.include?(params[:ref]) + raise ArgumentError, 'Reference not found' + end + + unless commit + raise ArgumentError, 'Commit not found' + end + + unless can?(current_user, :create_pipeline, project) + raise RuntimeError, 'Insufficient permissions to create a new pipeline' + end + + Ci::Commit.transaction do + unless pipeline.config_processor + raise ArgumentError, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file' + end + + pipeline.save! + pipeline.create_builds(current_user) + end + + pipeline + end + + private + + def ref_names + @ref_names ||= project.repository.ref_names + end + + def commit + @commit ||= project.commit(params[:ref]) + end + + def pipeline + @pipeline ||= project.ci_commits.new(sha: commit.id, ref: params[:ref], before_sha: Gitlab::Git::BLANK_SHA) + end + end +end \ No newline at end of file diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 6c3f8efc74..8f5739cf92 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -39,12 +39,12 @@ Commits - if project_nav_tab? :builds - = nav_link(controller: %w(pipelines)) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-builds' do + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do = icon('ship fw') %span Pipelines - %span.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count(:all)) + %span.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) = nav_link(controller: %w(builds)) do = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 161436e533..7384bfa1a7 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -13,9 +13,9 @@ %strong ##{build.id} - if build.stuck? - %i.fa.fa-warning.text-warning.has-tooltip(title="Build is stuck. Check runners.") + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') - if defined?(retried) && retried - %i.fa.fa-warning.has-tooltip(title="Build was retried") + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - if defined?(commit_sha) && commit_sha %td @@ -59,24 +59,24 @@ %td.duration - if build.duration %p - %i.fa.fa-clock-o + = icon("clock-o")   #{duration_in_words(build.finished_at, build.started_at)} - if build.finished_at %p - %i.fa.fa-calendar + = icon("calendar")   #{time_ago_with_tooltip(build.finished_at)} - else %td.duration - if build.duration - %i.fa.fa-clock-o + = icon("clock-o")   #{duration_in_words(build.finished_at, build.started_at)} %td.timestamp - if build.finished_at - %i.fa.fa-calendar + = icon("calendar")   %span #{time_ago_with_tooltip(build.finished_at)} @@ -89,11 +89,11 @@ .pull-right - if can?(current_user, :read_build, build) && build.artifacts? = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do - %i.fa.fa-download + = icon('download') - if can?(current_user, :update_build, build) - if build.active? = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do - %i.fa.fa-remove.cred + = icon('remove', class: 'cred') - elsif defined?(allow_retry) && allow_retry && build.retryable? = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do - %i.fa.fa-refresh + = icon('refresh') diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index d5dd6c7b0a..c6359c7c98 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -42,12 +42,12 @@ %td - if commit.started_at && commit.finished_at %p - %i.fa.fa-clock-o + = icon("clock-o")   #{duration_in_words(commit.finished_at, commit.started_at)} - if commit.finished_at %p - %i.fa.fa-calendar + = icon("calendar")   #{time_ago_with_tooltip(commit.finished_at)} @@ -63,7 +63,7 @@ - artifacts.each do |build| %li = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do - %i.fa.fa-download + = icon("download") %span #{build.name} - if can?(current_user, :update_pipeline, @project) @@ -74,4 +74,4 @@   - 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") + = icon("remove", class: "cred") diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 970cc95ad3..5b1cf71337 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(:pipelines) } + it { is_expected.to have_many(:ci_commits) } it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runners) } From 3953e5fcd7060e77405327e670f92ebd7d0ee5a0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 10 May 2016 02:38:25 +0300 Subject: [PATCH 083/449] Update generic commit status to make it look like a build --- app/models/commit_status.rb | 4 +-- app/views/projects/ci/builds/_build.html.haml | 33 ++++++------------- .../_generic_commit_status.html.haml | 9 +++++ 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 0c0c3d38f9..2d09edf3ca 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -91,12 +91,12 @@ class CommitStatus < ActiveRecord::Base end def self.stages - # We group by stage name, but order stages by their's index + # We group by stage name, but order stages by theirs' index unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') end def self.stages_status - # We execute subquery for each of the stages which calculates an Stage Status + # We execute subquery for each stage to calculate a stage status statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) statuses.inject({}) do |h, k| h[k.first] = k.last diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 7384bfa1a7..962b9fb259 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -55,30 +55,17 @@ - if defined?(retried) && retried %span.label.label-warning retried - - if defined?(new_duration) && new_duration - %td.duration - - if build.duration - %p - = icon("clock-o") -   - #{duration_in_words(build.finished_at, build.started_at)} - - if build.finished_at - %p - = icon("calendar") -   - #{time_ago_with_tooltip(build.finished_at)} - - else - %td.duration - - if build.duration - = icon("clock-o") -   - #{duration_in_words(build.finished_at, build.started_at)} + %td.duration + - if build.duration + = icon("clock-o") +   + #{duration_in_words(build.finished_at, build.started_at)} - %td.timestamp - - if build.finished_at - = icon("calendar") -   - %span #{time_ago_with_tooltip(build.finished_at)} + %td.timestamp + - if build.finished_at + = icon("calendar") +   + %span #{time_ago_with_tooltip(build.finished_at)} - if defined?(coverage) && coverage %td.coverage diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index f21c864e35..8129514964 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -12,6 +12,9 @@ - else %strong ##{generic_commit_status.id} + - if defined?(retried) && retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.') + - if defined?(commit_sha) && commit_sha %td = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" @@ -42,13 +45,19 @@ - generic_commit_status.tags.each do |tag| %span.label.label-primary = tag + - if defined?(retried) && retried + %span.label.label-warning retried %td.duration - if generic_commit_status.duration + = icon("clock-o") +   #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)} %td.timestamp - if generic_commit_status.finished_at + = icon("calendar") +   %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} - if defined?(coverage) && coverage From 86cedb082576ceca3522c33c10d367d765e02cf8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 10 May 2016 02:41:18 +0300 Subject: [PATCH 084/449] Use pipeline permissions instead of build --- app/views/projects/commit/_ci_commit.html.haml | 2 +- app/views/projects/pipelines/index.html.haml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index c844376361..0ff679d0e3 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -1,6 +1,6 @@ .row-content-block.build-content.middle-block .pull-right - - if can?(current_user, :update_build, @project) + - if can?(current_user, :update_pipeline, @project) - if ci_commit.builds.latest.failed.any?(&:retryable?) = link_to "Retry failed", retry_namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), class: 'btn btn-grouped btn-primary', method: :post diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 0e08753fd3..d247ea445e 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -29,7 +29,6 @@ = 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' @@ -54,7 +53,7 @@ .table-holder %table.table.builds %tbody - %th Pipeline ID + %th ID %th Commit - @pipelines.stages.each do |stage| %th From dd1ad6bf3e5a378eb8698e95b73e58f935eb0956 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Tue, 10 May 2016 09:56:12 +0700 Subject: [PATCH 085/449] Code improve --- app/views/snippets/index.html.haml | 22 +++++++++++----------- config/routes.rb | 15 ++++++++------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index 8aa3d490fa..7e4918a608 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,13 +1,13 @@ - - page_title "By #{@user.name}", "Snippets" +- page_title "By #{@user.name}", "Snippets" - %ol.breadcrumb - %li - = link_to snippets_path do - Snippets - %li - = @user.name - .pull-right.hidden-xs - = link_to user_path(@user) do - #{@user.name} profile page +%ol.breadcrumb + %li + = link_to snippets_path do + Snippets + %li + = @user.name + .pull-right.hidden-xs + = link_to user_path(@user) do + #{@user.name} profile page - = render 'snippets' += render 'snippets' diff --git a/config/routes.rb b/config/routes.rb index 594bab204f..881de37f10 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -344,15 +344,16 @@ Rails.application.routes.draw do end scope(path: 'u/:username', + as: :user, constraints: { username: /[a-zA-Z.0-9_\-]+(? Date: Tue, 10 May 2016 11:10:51 +0200 Subject: [PATCH 086/449] create import data in service --- app/services/projects/create_service.rb | 2 ++ lib/gitlab/bitbucket_import/project_creator.rb | 7 ++----- lib/gitlab/fogbugz_import/project_creator.rb | 9 +++------ lib/gitlab/google_code_import/project_creator.rb | 9 +++------ 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 501e58c140..a77652480b 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -6,6 +6,7 @@ module Projects def execute forked_from_project_id = params.delete(:forked_from_project_id) + import_data = params.delete(:import_data) @project = Project.new(params) @@ -50,6 +51,7 @@ module Projects end Project.transaction do + @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data @project.save if @project.persisted? && !@project.import? diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index 941f818b84..b90ef0b0fb 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -11,7 +11,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo["name"], path: repo["slug"], @@ -21,11 +21,8 @@ module Gitlab import_type: "bitbucket", import_source: "#{repo["owner"]}/#{repo["slug"]}", import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git", + import_data: { credentials: { bb_session: session_data } } ).execute - - project.create_or_update_import_data(credentials: { bb_session: session_data }) - - project end end end diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb index 3840765db8..1918d5b208 100644 --- a/lib/gitlab/fogbugz_import/project_creator.rb +++ b/lib/gitlab/fogbugz_import/project_creator.rb @@ -12,7 +12,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo.safe_name, path: repo.path, @@ -21,12 +21,9 @@ module Gitlab visibility_level: Gitlab::VisibilityLevel::INTERNAL, import_type: 'fogbugz', import_source: repo.name, - import_url: Project::UNKNOWN_IMPORT_URL + import_url: Project::UNKNOWN_IMPORT_URL, + import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session } } ).execute - - project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session }) - - project end end end diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb index 0abb7a64c1..326cfcaa8a 100644 --- a/lib/gitlab/google_code_import/project_creator.rb +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -11,7 +11,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo.name, path: repo.name, @@ -21,12 +21,9 @@ module Gitlab visibility_level: Gitlab::VisibilityLevel::PUBLIC, import_type: "google_code", import_source: repo.name, - import_url: repo.import_url + import_url: repo.import_url, + import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map } } ).execute - - project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }) - - project end end end From ebf80db3abcaf4a0e08273bf180aa33368610b8a Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 10 May 2016 11:14:46 +0200 Subject: [PATCH 087/449] Remove extra sanitization Robert Speicher and I believe this string gets sanitized further down the stack anyway. Doing this in a model class feels wrong. --- app/models/hooks/web_hook.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 818abbf4cc..a5ac8bee6b 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, ActionView::Base.full_sanitizer.sanitize(response.to_s)] + [response.code, response.to_s] rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e logger.error("WebHook Error => #{e}") [false, e.to_s] From 732e98eee7da718d282903844649d02f935fd2be Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 9 May 2016 12:47:44 +0200 Subject: [PATCH 088/449] Reuse runners edit form in admin and project area --- app/views/admin/runners/show.html.haml | 22 +++--------------- app/views/projects/runners/_form.html.haml | 25 +++++++++++++++++++++ app/views/projects/runners/edit.html.haml | 26 +--------------------- 3 files changed, 29 insertions(+), 44 deletions(-) create mode 100644 app/views/projects/runners/_form.html.haml diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 8700b4820c..4dfb3ed05b 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -22,25 +22,9 @@ %h4 This runner will process builds only from ASSIGNED projects %p You can't make this a shared runner. %hr -= form_for @runner, url: admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| - .form-group - = label_tag :token, class: 'control-label' do - Token - .col-sm-10 - = f.text_field :token, class: 'form-control', readonly: true - .form-group - = label_tag :description, class: 'control-label' do - Description - .col-sm-10 - = f.text_field :description, class: 'form-control' - .form-group - = label_tag :tag_list, class: 'control-label' do - Tags - .col-sm-10 - = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' - .help-block You can setup builds to only use runners with specific tags - .form-actions - = f.submit 'Save', class: 'btn btn-save' + +.append-bottom-20 + = render '/projects/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner) .row .col-md-6 diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml new file mode 100644 index 0000000000..2d6c964ae9 --- /dev/null +++ b/app/views/projects/runners/_form.html.haml @@ -0,0 +1,25 @@ += form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f| + .form-group + = label :active, "Active", class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :active + %span.light Paused runners don't accept new builds + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, value: runner.tag_list.to_s, class: 'form-control' + .help-block You can setup jobs to only use runners with specific tags + .form-actions + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index eba03028af..771947d790 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -2,28 +2,4 @@ %h4 Runner ##{@runner.id} %hr -= form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f| - .form-group - = label :active, "Active", class: 'control-label' - .col-sm-10 - .checkbox - = f.check_box :active - %span.light Paused runners don't accept new builds - .form-group - = label_tag :token, class: 'control-label' do - Token - .col-sm-10 - = f.text_field :token, class: 'form-control', readonly: true - .form-group - = label_tag :description, class: 'control-label' do - Description - .col-sm-10 - = f.text_field :description, class: 'form-control' - .form-group - = label_tag :tag_list, class: 'control-label' do - Tags - .col-sm-10 - = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' - .help-block You can setup jobs to only use runners with specific tags - .form-actions - = f.submit 'Save changes', class: 'btn btn-save' + = render 'form', runner: @runner, runner_form_url: runner_path(@runner) From 7d6d3421d64085c05c8b7a17dcf0b70fbdd478b5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 9 May 2016 13:07:10 +0200 Subject: [PATCH 089/449] Share form editable attributes mf runner via model --- app/controllers/admin/runners_controller.rb | 2 +- app/controllers/projects/runners_controller.rb | 2 +- app/models/ci/runner.rb | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index a701d49b84..8b8a732007 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -58,6 +58,6 @@ class Admin::RunnersController < Admin::ApplicationController end def runner_params - params.require(:runner).permit(:token, :description, :tag_list, :active) + params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 0dd2d6a99b..3a9d67aff6 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -64,6 +64,6 @@ class Projects::RunnersController < Projects::ApplicationController end def runner_params - params.require(:runner).permit(:description, :tag_list, :active) + params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index add59a0889..d27963e8ec 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -23,6 +23,7 @@ module Ci LAST_CONTACT_TIME = 5.minutes.ago AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] + FORM_EDITABLE = [:description, :tag_list, :active] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' From 67dc3b9c1131b5a7f3f861a0fe35169fda5cd398 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Fri, 22 Apr 2016 09:50:08 -0700 Subject: [PATCH 090/449] Add the health_check gem end expose the health_check route --- Gemfile | 3 +++ Gemfile.lock | 3 +++ config/initializers/health_check.rb | 28 ++++++++++++++++++++++++++++ config/routes.rb | 3 +++ 4 files changed, 37 insertions(+) create mode 100644 config/initializers/health_check.rb diff --git a/Gemfile b/Gemfile index 3e5c604ae0..204d19f711 100644 --- a/Gemfile +++ b/Gemfile @@ -333,3 +333,6 @@ gem 'oauth2', '~> 1.0.0' # Soft deletion gem "paranoia", "~> 2.0" + +# Health check +gem 'health_check', '~> 1.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index 86b9142ef2..de0f536641 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -402,6 +402,8 @@ GEM html2haml (>= 1.0.1) railties (>= 4.0.1) hashie (3.4.3) + health_check (1.5.1) + rails (>= 2.3.0) highline (1.7.8) hipchat (1.5.2) httparty @@ -947,6 +949,7 @@ DEPENDENCIES grape (~> 0.13.0) grape-entity (~> 0.4.2) haml-rails (~> 0.9.0) + health_check (~> 1.5.1) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty (~> 0.13.3) diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb new file mode 100644 index 0000000000..c111cfdf5c --- /dev/null +++ b/config/initializers/health_check.rb @@ -0,0 +1,28 @@ +# HealthCheck.setup do |config| +# # Text output upon success +# config.success = 'success' +# +# # Timeout in seconds used when checking smtp server +# config.smtp_timeout = 30.0 +# +# # http status code used when plain text error message is output +# # Set to 200 if you want your want to distinguish between partial (text does not include success) and +# # total failure of rails application (http status of 500 etc) +# config.http_status_for_error_text = 500 +# +# # http status code used when an error object is output (json or xml) +# # Set to 200 if you want your want to distinguish between partial (healthy property == false) and +# # total failure of rails application (http status of 500 etc) +# config.http_status_for_error_object = 500 +# +# # You can customize which checks happen on a standard health check +# config.standard_checks = [ 'database', 'migrations', 'custom' ] +# +# # You can set what tests are run with the 'full' or 'all' parameter +# config.full_checks = ['database', 'migrations', 'custom', 'email', 'cache'] +# +# # Add one or more custom checks that return a blank string if ok, or an error message if there is an error +# config.add_custom_check do +# any code that returns blank on success and non blank string upon failure +# end +# end diff --git a/config/routes.rb b/config/routes.rb index dafecc9464..cac800b9a9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -73,6 +73,9 @@ Rails.application.routes.draw do mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq end + # Health check + health_check_routes + # Enable Grack support mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] From 9898f9b4e6b80edaa914675edfa9b229498b31fe Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 5 May 2016 12:25:02 -0700 Subject: [PATCH 091/449] Set the standard health_check options to include the cache check. --- config/initializers/health_check.rb | 31 +++-------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb index c111cfdf5c..a93e427303 100644 --- a/config/initializers/health_check.rb +++ b/config/initializers/health_check.rb @@ -1,28 +1,3 @@ -# HealthCheck.setup do |config| -# # Text output upon success -# config.success = 'success' -# -# # Timeout in seconds used when checking smtp server -# config.smtp_timeout = 30.0 -# -# # http status code used when plain text error message is output -# # Set to 200 if you want your want to distinguish between partial (text does not include success) and -# # total failure of rails application (http status of 500 etc) -# config.http_status_for_error_text = 500 -# -# # http status code used when an error object is output (json or xml) -# # Set to 200 if you want your want to distinguish between partial (healthy property == false) and -# # total failure of rails application (http status of 500 etc) -# config.http_status_for_error_object = 500 -# -# # You can customize which checks happen on a standard health check -# config.standard_checks = [ 'database', 'migrations', 'custom' ] -# -# # You can set what tests are run with the 'full' or 'all' parameter -# config.full_checks = ['database', 'migrations', 'custom', 'email', 'cache'] -# -# # Add one or more custom checks that return a blank string if ok, or an error message if there is an error -# config.add_custom_check do -# any code that returns blank on success and non blank string upon failure -# end -# end +HealthCheck.setup do |config| + config.standard_checks = [ 'database', 'migrations', 'cache' ] +end From 160ef66d1bbbbc593516c7575d6b02ddb019c000 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Mon, 9 May 2016 16:21:22 -0700 Subject: [PATCH 092/449] Add health_check access token, and enforce on the health_check endpoint Also added a health check page to the admin section for resetting the token. --- .../admin/application_settings_controller.rb | 6 +++ .../admin/health_check_controller.rb | 9 +++++ app/controllers/health_check_controller.rb | 13 ++++++ app/models/application_setting.rb | 6 +++ app/views/admin/health_check/show.html.haml | 40 +++++++++++++++++++ app/views/layouts/nav/_admin.html.haml | 5 +++ config/routes.rb | 4 +- ...ck_access_token_to_application_settings.rb | 5 +++ db/schema.rb | 3 +- 9 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 app/controllers/admin/health_check_controller.rb create mode 100644 app/controllers/health_check_controller.rb create mode 100644 app/views/admin/health_check/show.html.haml create mode 100644 db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ec22548dde..7b9a88cd31 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -19,6 +19,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to admin_runners_path end + def reset_health_check_token + @application_setting.reset_health_check_access_token! + flash[:notice] = 'New health check access token has been generated!' + redirect_to :back + end + def clear_repository_check_states RepositoryCheck::ClearWorker.perform_async diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb new file mode 100644 index 0000000000..3153a765e3 --- /dev/null +++ b/app/controllers/admin/health_check_controller.rb @@ -0,0 +1,9 @@ +class Admin::HealthCheckController < Admin::ApplicationController + def show + begin + @errors = HealthCheck::Utils.process_checks('standard') + rescue => e + @errors = e.message.blank? ? e.class.to_s : e.message.to_s + end + end +end diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb new file mode 100644 index 0000000000..b974489836 --- /dev/null +++ b/app/controllers/health_check_controller.rb @@ -0,0 +1,13 @@ +class HealthCheckController < HealthCheck::HealthCheckController + before_action :validate_health_check_access! + + protected + + def validate_health_check_access! + return render_404 unless params[:token].presence && params[:token] == current_application_settings.health_check_access_token + end + + def render_404 + render file: Rails.root.join("public", "404"), layout: false, status: "404" + end +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 7039db2d41..bf88326c11 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,6 +1,7 @@ class ApplicationSetting < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :runners_registration_token + add_authentication_token_field :health_check_access_token CACHE_KEY = 'application_setting.last' @@ -70,6 +71,7 @@ class ApplicationSetting < ActiveRecord::Base end before_save :ensure_runners_registration_token + before_save :ensure_health_check_access_token after_commit do Rails.cache.write(CACHE_KEY, self) @@ -133,4 +135,8 @@ class ApplicationSetting < ActiveRecord::Base def runners_registration_token ensure_runners_registration_token! end + + def health_check_access_token + ensure_health_check_access_token! + end end diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml new file mode 100644 index 0000000000..70e5d04e35 --- /dev/null +++ b/app/views/admin/health_check/show.html.haml @@ -0,0 +1,40 @@ +- page_title "Health Check" + +%h3.page-title + Health Check +%p.light + Health information can be reteived as plain text, json, or xml using: + %ul + %li + %code= "/health_check?token=#{current_application_settings.health_check_access_token}" + %li + %code= "/health_check.json?token=#{current_application_settings.health_check_access_token}" + %li + %code= "/health_check.xml?token=#{current_application_settings.health_check_access_token}" + +.bs-callout.clearfix + .pull-left + %p + You can reset the health check access token by pressing the button below. + %p + = button_to reset_health_check_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset the health check token?' } do + = icon('refresh') + Reset health check access token + +%hr +.panel.panel-default + .panel-heading + Current Status: + - if @errors.blank? + = icon('circle', class: 'cgreen') + Healthy + - else + = icon('warning', class: 'cred') + Unhealthy + .panel-body + - if @errors.blank? + No Health Problems Detected + - else + = @errors diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 280a1b9372..f292730fe4 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -41,6 +41,11 @@ = icon('file-text fw') %span Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + = icon('medkit fw') + %span + Health Check = nav_link(controller: :broadcast_messages) do = link_to admin_broadcast_messages_path, title: 'Messages' do = icon('bullhorn fw') diff --git a/config/routes.rb b/config/routes.rb index cac800b9a9..c81bf294a5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,7 +74,7 @@ Rails.application.routes.draw do end # Health check - health_check_routes + get 'health_check(/:checks)(.:format)' => 'health_check#index' # Enable Grack support mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] @@ -256,6 +256,7 @@ Rails.application.routes.draw do end resource :logs, only: [:show] + resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do @@ -287,6 +288,7 @@ Rails.application.routes.draw do resource :application_settings, only: [:show, :update] do resources :services put :reset_runners_token + put :reset_health_check_token put :clear_repository_check_states end diff --git a/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb new file mode 100644 index 0000000000..9d729fec18 --- /dev/null +++ b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddHealthCheckAccessTokenToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :health_check_access_token, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 71d953afe3..8ac3eeae62 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160508194200) do +ActiveRecord::Schema.define(version: 20160509201028) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -80,6 +80,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.boolean "repository_checks_enabled", default: false t.text "shared_runners_text" t.integer "metrics_packet_size", default: 1 + t.string "health_check_access_token" end create_table "audit_events", force: :cascade do |t| From 97424ea544d0954e582a356586270e983d3bbb7a Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 10 May 2016 18:03:55 +0100 Subject: [PATCH 093/449] Restrict starred projects to viewable ones `User#starred_projects` doesn't perform any visibility checks. This has a couple of problems: 1. It assumes a user can always view all of their starred projects in perpetuity (project not changed to private, access revoked, etc.). 2. It assumes that we'll only ever allow a user to star a project they can view. This is currently the case, but bugs happen. Add `User#viewable_starred_projects` to filter the starred projects by those the user either has explicit access to, or are public or internal. Then use that in all places where we list the user's starred projects. --- .../dashboard/projects_controller.rb | 2 +- app/controllers/dashboard_controller.rb | 2 +- app/models/user.rb | 5 ++++ lib/api/projects.rb | 2 +- spec/models/user_spec.rb | 24 +++++++++++++++++++ spec/requests/api/projects_spec.rb | 21 +++++++--------- 6 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 71acc244a9..c08eb81153 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -28,7 +28,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def starred - @projects = current_user.starred_projects.sorted_by_activity + @projects = current_user.viewable_starred_projects.sorted_by_activity @projects = filter_projects(@projects) @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.sort(@sort = params[:sort]) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 1dce4a2172..4dda4e51f6 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -25,7 +25,7 @@ class DashboardController < Dashboard::ApplicationController def load_events projects = if params[:filter] == "starred" - current_user.starred_projects + current_user.viewable_starred_projects else current_user.authorized_projects end diff --git a/app/models/user.rb b/app/models/user.rb index 1e4814641d..a0115957f0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -381,6 +381,11 @@ class User < ActiveRecord::Base Project.where("projects.id IN (#{projects_union.to_sql})") end + def viewable_starred_projects + starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})", + [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL]) + end + def owned_projects @owned_projects ||= Project.where('namespace_id IN (?) OR namespace_id = ?', diff --git a/lib/api/projects.rb b/lib/api/projects.rb index cc2c7a0c50..9b59577267 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -44,7 +44,7 @@ module API # Example Request: # GET /projects/starred get '/starred' do - @projects = current_user.starred_projects + @projects = current_user.viewable_starred_projects @projects = filter_projects(@projects) @projects = paginate @projects present @projects, with: Entities::Project diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 26d4e13939..06d1ca3b7d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -233,6 +233,8 @@ describe User, models: true do @project = create :project, namespace: @user.namespace @project_2 = create :project, group: create(:group) # Grant MASTER access to the user @project_3 = create :project, group: create(:group) # Grant DEVELOPER access to the user + @project_4 = create :project, group: create(:group) + @project_5 = create :project, group: create(:group) @project_2.team << [@user, :master] @project_3.team << [@user, :developer] @@ -782,4 +784,26 @@ describe User, models: true do it { is_expected.to eq([private_project]) } end + + describe '#viewable_starred_projects' do + let(:user) { create(:user) } + let(:public_project) { create(:project, :public) } + let(:private_project) { create(:project, :private) } + let(:private_viewable_project) { create(:project, :private) } + let(:viewable?) { -> (project) { user.can?(:read_project, project) } } + let(:projects) { [public_project, private_project, private_viewable_project] } + + before do + private_viewable_project.team << [user, Gitlab::Access::MASTER] + projects.each { |project| user.toggle_star(project) } + end + + it 'returns only starred projects the user can view' do + expect(user.viewable_starred_projects).to all(satisfy(&viewable?)) + end + + it 'rejects only starred projects the user can not view' do + expect(projects - user.viewable_starred_projects).not_to include(satisfy(&viewable?)) + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 66193eac05..f167813e07 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -10,20 +10,20 @@ describe API::API, api: true do let(:admin) { create(:admin) } let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) } - let(:project3) { create(:project, path: 'project3', creator_id: user.id, namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :master, user: user, project: project) } let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } let(:project3) do create(:project, + :private, name: 'second_project', path: 'second_project', creator_id: user.id, namespace: user.namespace, merge_requests_enabled: false, issues_enabled: false, wiki_enabled: false, - snippets_enabled: false, visibility_level: 0) + snippets_enabled: false) end let(:project_member3) do create(:project_member, @@ -164,21 +164,18 @@ describe API::API, api: true do end describe 'GET /projects/starred' do + let(:public_project) { create(:project, :public) } + before do - admin.starred_projects << project - admin.save! + project_member2 + user3.update_attributes(starred_projects: [project, project2, project3, public_project]) end - it 'should return the starred projects' do - get api('/projects/all', admin) + it 'should return the starred projects viewable by the user' do + get api('/projects/starred', user3) expect(response.status).to eq(200) expect(json_response).to be_an Array - - expect(json_response).to satisfy do |response| - response.one? do |entry| - entry['name'] == project.name - end - end + expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) end end From 93ca5c9964a26fbf31fcc794348b30193f4dff9f Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 10 May 2016 16:06:02 -0300 Subject: [PATCH 094/449] Fix notes API calls symbol convertions --- lib/api/notes.rb | 6 +++--- spec/requests/api/notes_spec.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 4ac08a3e8c..f0116acd90 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -19,7 +19,7 @@ module API # GET /projects/:id/issues/:noteable_id/notes # GET /projects/:id/snippets/:noteable_id/notes get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do - @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) read_ability_name = "read_#{@noteable.class.to_s.underscore.downcase}".to_sym if can?(current_user, read_ability_name, @noteable) @@ -36,7 +36,7 @@ module API reject { |n| n.cross_reference_not_visible_for?(current_user) } present notes, with: Entities::Note else - render_api_error!("Not found.", 404) + not_found!("Notes") end end @@ -50,7 +50,7 @@ module API # GET /projects/:id/issues/:noteable_id/notes/:note_id # GET /projects/:id/snippets/:noteable_id/notes/:note_id get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do - @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) @note = @noteable.notes.find(params[:note_id]) if @note.cross_reference_not_visible_for?(current_user) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index f5b31be1ba..f9bfee9442 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -39,6 +39,7 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should return an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(issue_note.note) @@ -46,12 +47,14 @@ describe API::API, api: true do it "should return a 404 error when issue id not found" do get api("/projects/#{project.id}/issues/12345/notes", user) + expect(response.status).to eq(404) end context "that references a private issue" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response).to be_empty @@ -62,6 +65,7 @@ describe API::API, api: true do it "returns 404" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + expect(response.status).to eq(404) end end @@ -69,6 +73,7 @@ describe API::API, api: true do context "and current user can view the note" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(cross_reference_note.note) @@ -80,6 +85,7 @@ describe API::API, api: true do context "when noteable is a Snippet" do it "should return an array of snippet notes" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(snippet_note.note) @@ -87,11 +93,13 @@ describe API::API, api: true do it "should return a 404 error when snippet id not found" do get api("/projects/#{project.id}/snippets/42/notes", user) + expect(response.status).to eq(404) end it "returns 404 when not authorized" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user) + expect(response.status).to eq(404) end end @@ -99,6 +107,7 @@ describe API::API, api: true do context "when noteable is a Merge Request" do it "should return an array of merge_requests notes" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(merge_request_note.note) @@ -106,11 +115,13 @@ describe API::API, api: true do it "should return a 404 error if merge request id not found" do get api("/projects/#{project.id}/merge_requests/4444/notes", user) + expect(response.status).to eq(404) end it "returns 404 when not authorized" do get api("/projects/#{project.id}/merge_requests/4444/notes", private_user) + expect(response.status).to eq(404) end end @@ -120,24 +131,28 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should return an issue note by id" do get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) + expect(response.status).to eq(200) expect(json_response['body']).to eq(issue_note.note) end it "should return a 404 error if issue note not found" do get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + expect(response.status).to eq(404) end context "that references a private issue" do it "should return a 404 error" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) + expect(response.status).to eq(404) end context "and current user can view the note" do it "should return an issue note by id" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) + expect(response.status).to eq(200) expect(json_response['body']).to eq(cross_reference_note.note) end @@ -148,12 +163,14 @@ describe API::API, api: true do context "when noteable is a Snippet" do it "should return a snippet note by id" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) + expect(response.status).to eq(200) expect(json_response['body']).to eq(snippet_note.note) end it "should return a 404 error if snippet note not found" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user) + expect(response.status).to eq(404) end end @@ -163,6 +180,7 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should create a new issue note" do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + expect(response.status).to eq(201) expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) @@ -170,11 +188,13 @@ describe API::API, api: true do it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + expect(response.status).to eq(400) end it "should return a 401 unauthorized error if user not authenticated" do post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' + expect(response.status).to eq(401) end @@ -183,6 +203,7 @@ describe API::API, api: true do creation_time = 2.weeks.ago post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!', created_at: creation_time + expect(response.status).to eq(201) expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) @@ -195,6 +216,7 @@ describe API::API, api: true do context "when noteable is a Snippet" do it "should create a new snippet note" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' + expect(response.status).to eq(201) expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) @@ -202,11 +224,13 @@ describe API::API, api: true do it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + expect(response.status).to eq(400) end it "should return a 401 unauthorized error if user not authenticated" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' + expect(response.status).to eq(401) end end @@ -246,6 +270,7 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user), body: 'Hello!' + expect(response.status).to eq(200) expect(json_response['body']).to eq('Hello!') end @@ -253,12 +278,14 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), body: 'Hello!' + expect(response.status).to eq(404) end it 'should return a 400 bad request error if body not given' do put api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) + expect(response.status).to eq(400) end end @@ -267,6 +294,7 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user), body: 'Hello!' + expect(response.status).to eq(200) expect(json_response['body']).to eq('Hello!') end @@ -274,6 +302,7 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/12345", user), body: "Hello!" + expect(response.status).to eq(404) end end @@ -282,6 +311,7 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ "notes/#{merge_request_note.id}", user), body: 'Hello!' + expect(response.status).to eq(200) expect(json_response['body']).to eq('Hello!') end @@ -289,6 +319,7 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ "notes/12345", user), body: "Hello!" + expect(response.status).to eq(404) end end From e689d9fb70d7484de708f8f436b294b40a9f6486 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 10 May 2016 16:32:59 -0500 Subject: [PATCH 095/449] Reduce width of side nav --- app/assets/stylesheets/framework/sidebar.scss | 11 +++++++---- app/assets/stylesheets/framework/variables.scss | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index bd91f51708..a509ef2f14 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -63,7 +63,7 @@ float: left; height: $header-height; width: 100%; - padding-left: 22px; + padding-left: 16px; overflow: hidden; outline: none; transition-duration: .3s; @@ -78,7 +78,7 @@ } .gitlab-text-container { - width: 230px; + width: 220px; h3 { width: 158px; @@ -146,12 +146,12 @@ a { padding: 7px 15px; + transition-duration: .3s; font-size: $gl-font-size; line-height: 24px; color: $gray; display: block; text-decoration: none; - padding-left: 23px; font-weight: normal; outline: none; @@ -253,6 +253,9 @@ width: auto; a { + padding-left: 23px; + transition-duration: .3s; + span { display: none; } @@ -312,7 +315,7 @@ } .nav-sidebar li a { - width: 230px; + width: 220px; &.back-link { i { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 84e74db06b..5fa4c26660 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -2,7 +2,7 @@ * Layout */ $sidebar_collapsed_width: 62px; -$sidebar_width: 230px; +$sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 258px; From a51064906bd4dcb29ea61c2278e2c32e848a5de6 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 10 May 2016 16:51:57 -0500 Subject: [PATCH 096/449] No extra link padding at xs screen size --- app/assets/stylesheets/framework/sidebar.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index a509ef2f14..c243dd8a42 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -253,8 +253,10 @@ width: auto; a { - padding-left: 23px; - transition-duration: .3s; + @media (min-width: $screen-xs-min) { + padding-left: 23px; + transition-duration: .3s; + } span { display: none; From 0e0caf4d17c28b6b0f3488b25efa265ce2804cc4 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Tue, 10 May 2016 16:19:16 -0700 Subject: [PATCH 097/449] Add tests for the health check feature --- app/views/admin/health_check/show.html.haml | 3 + .../health_check_controller_spec.rb | 90 +++++++++++++++++++ .../features/admin/admin_health_check_spec.rb | 55 ++++++++++++ spec/routing/admin_routing_spec.rb | 7 ++ spec/routing/routing_spec.rb | 10 +++ 5 files changed, 165 insertions(+) create mode 100644 spec/controllers/health_check_controller_spec.rb create mode 100644 spec/features/admin/admin_health_check_spec.rb diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 70e5d04e35..23a931995a 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -2,6 +2,9 @@ %h3.page-title Health Check +%p.light + Access token is + %code{ id:'health-check-token' }= "#{current_application_settings.health_check_access_token}" %p.light Health information can be reteived as plain text, json, or xml using: %ul diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb new file mode 100644 index 0000000000..3b9cc5c98f --- /dev/null +++ b/spec/controllers/health_check_controller_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe HealthCheckController do + let(:token) { current_application_settings.health_check_access_token } + let(:json_response) { JSON.parse(response.body) } + let(:xml_response) { Hash.from_xml(response.body)['hash'] } + + describe 'GET #index' do + context 'when services are up but NO access token' do + it 'returns a not found page' do + get :index + expect(response).to be_not_found + end + end + + context 'when services are up and an access token is provided' do + it 'supports successful plaintest response' do + get :index, token: token + expect(response).to be_success + expect(response.content_type).to eq 'text/plain' + end + + it 'supports successful json response' do + get :index, token: token, format: :json + expect(response).to be_success + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be true + end + + it 'supports successful xml response' do + get :index, token: token, format: :xml + expect(response).to be_success + expect(response.content_type).to eq 'application/xml' + expect(xml_response['healthy']).to be true + end + + it 'supports successful responses for specific checks' do + get :index, token: token, checks: 'email', format: :json + expect(response).to be_success + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be true + end + end + + context 'when a service is down but NO access token' do + it 'returns a not found page' do + get :index + expect(response).to be_not_found + end + end + + context 'when a service is down and an access token is provided' do + before do + allow(HealthCheck::Utils).to receive(:process_checks).with('standard').and_return('The server is on fire') + allow(HealthCheck::Utils).to receive(:process_checks).with('email').and_return('Email is on fire') + end + + it 'supports failure plaintest response' do + get :index, token: token + expect(response.status).to eq(500) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to include('The server is on fire') + end + + it 'supports failure json response' do + get :index, token: token, format: :json + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be false + expect(json_response['message']).to include('The server is on fire') + end + + it 'supports failure xml response' do + get :index, token: token, format: :xml + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/xml' + expect(xml_response['healthy']).to be false + expect(xml_response['message']).to include('The server is on fire') + end + + it 'supports failure responses for specific checks' do + get :index, token: token, checks: 'email', format: :json + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be false + expect(json_response['message']).to include('Email is on fire') + end + end + end +end diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb new file mode 100644 index 0000000000..4fde04b609 --- /dev/null +++ b/spec/features/admin/admin_health_check_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +feature "Admin Health Check", feature: true do + include WaitForAjax + + before do + login_as :admin + end + + describe '#show' do + before do + visit admin_health_check_path + end + + it { page.has_text? 'Health Check' } + it { page.has_text? 'Health information can be reteived' } + + it 'has a health check access token' do + token = current_application_settings.health_check_access_token + expect(page).to have_content("Access token is #{token}") + expect(page).to have_selector('#health-check-token', text: token) + end + + describe 'reload access token', js: true do + it 'changes the access token' do + orig_token = current_application_settings.health_check_access_token + click_button 'Reset health check access token' + wait_for_ajax + expect(find('#health-check-token').text).not_to eq orig_token + end + end + end + + context 'when services are up' do + before do + visit admin_health_check_path + end + + it 'shows healthy status' do + expect(page).to have_content('Current Status: Healthy') + end + end + + context 'when a service is down' do + before do + allow(HealthCheck::Utils).to receive(:process_checks).and_return('The server is on fire') + visit admin_health_check_path + end + + it 'shows unhealthy status' do + expect(page).to have_content('Current Status: Unhealthy') + expect(page).to have_content('The server is on fire') + end + end +end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index cd16a8e632..b5ed8584c8 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -118,3 +118,10 @@ describe Admin::DashboardController, "routing" do expect(get("/admin")).to route_to('admin/dashboard#index') end end + +# admin_health_check GET /admin/health_check(.:format) admin/health_check#show +describe Admin::HealthCheckController, "routing" do + it "to #show" do + expect(get("/admin/health_check")).to route_to('admin/health_check#show') + end +end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1527eddfa4..e4dfd4bca3 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -243,3 +243,13 @@ describe "Groups", "routing" do expect(get('/1')).to route_to('namespaces#show', id: '1') end end + +describe HealthCheckController, 'routing' do + it 'to #index' do + expect(get('/health_check')).to route_to('health_check#index') + end + + it 'also supports passing checks in the url' do + expect(get('/health_check/email')).to route_to('health_check#index', checks: 'email') + end +end From b42f39d2c27ec2a87e21dea7414d9845f2512144 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 28 Apr 2016 19:44:56 -0600 Subject: [PATCH 098/449] Remove activerecord-deprecated_finders gem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t use any of the deprecated finders, so it should be safe to remove. Resolves #17015. --- Gemfile | 1 - Gemfile.lock | 2 -- 2 files changed, 3 deletions(-) diff --git a/Gemfile b/Gemfile index 77d351419f..8de9602510 100644 --- a/Gemfile +++ b/Gemfile @@ -324,7 +324,6 @@ gem "mail_room", "~> 0.7" gem 'email_reply_parser', '~> 0.5.8' ## CI -gem 'activerecord-deprecated_finders', '~> 1.0.3' gem 'activerecord-session_store', '~> 0.1.0' gem "nested_form", '~> 0.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index c02698bcca..45d6de18f2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,7 +33,6 @@ GEM activemodel (= 4.2.6) activesupport (= 4.2.6) arel (~> 6.0) - activerecord-deprecated_finders (1.0.4) activerecord-session_store (0.1.2) actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) @@ -883,7 +882,6 @@ PLATFORMS DEPENDENCIES RedCloth (~> 4.2.9) ace-rails-ap (~> 4.0.2) - activerecord-deprecated_finders (~> 1.0.3) activerecord-session_store (~> 0.1.0) acts-as-taggable-on (~> 3.4) addressable (~> 2.3.8) From 51e198fb38428387a976385cae2d5e4fc455ee9e Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 10 May 2016 21:56:40 -0600 Subject: [PATCH 099/449] Don't show the Todo count if there aren't any Todos. When there are 0 Todos, the Todos icon still had the blue badge with a "0" in it, which made me think I had a new Todo left. This makes it less annoying. Follow-up on !4077. --- app/views/layouts/header/_default.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 172579dafd..c33740e23f 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -27,8 +27,9 @@ %li = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('bell fw') - %span.badge.todos-pending-count - = todos_pending_count + - unless todos_pending_count == 0 + %span.badge.todos-pending-count + = todos_pending_count - if current_user.can_create_project? %li = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do From f2548f3f2b3dcd1c6ecbae20af28933f3ab88fdd Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 10 May 2016 22:07:13 -0700 Subject: [PATCH 100/449] Improve log message when a project is destroyed to include the namespace --- app/services/projects/destroy_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 19aab999e0..48a6131b44 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -35,7 +35,7 @@ module Projects end end - log_info("Project \"#{project.name}\" was removed") + log_info("Project \"#{project.path_with_namespace}\" was removed") system_hook_service.execute_hooks_for(project, :destroy) true end From acd8930c46c06312f87c5ea0bb549b5435feece8 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 11 May 2016 09:03:23 +0100 Subject: [PATCH 101/449] Tidy up user project specs --- app/models/user.rb | 2 +- spec/models/user_spec.rb | 21 ++++++++------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index a0115957f0..489bff3fa4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -383,7 +383,7 @@ class User < ActiveRecord::Base def viewable_starred_projects starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})", - [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL]) + [Project::PUBLIC, Project::INTERNAL]) end def owned_projects diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 06d1ca3b7d..10e7e69357 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -233,8 +233,6 @@ describe User, models: true do @project = create :project, namespace: @user.namespace @project_2 = create :project, group: create(:group) # Grant MASTER access to the user @project_3 = create :project, group: create(:group) # Grant DEVELOPER access to the user - @project_4 = create :project, group: create(:group) - @project_5 = create :project, group: create(:group) @project_2.team << [@user, :master] @project_3.team << [@user, :developer] @@ -787,23 +785,20 @@ describe User, models: true do describe '#viewable_starred_projects' do let(:user) { create(:user) } - let(:public_project) { create(:project, :public) } - let(:private_project) { create(:project, :private) } - let(:private_viewable_project) { create(:project, :private) } - let(:viewable?) { -> (project) { user.can?(:read_project, project) } } - let(:projects) { [public_project, private_project, private_viewable_project] } + let(:public_project) { create(:empty_project, :public) } + let(:private_project) { create(:empty_project, :private) } + let(:private_viewable_project) { create(:empty_project, :private) } before do private_viewable_project.team << [user, Gitlab::Access::MASTER] - projects.each { |project| user.toggle_star(project) } + + [public_project, private_project, private_viewable_project].each do |project| + user.toggle_star(project) + end end it 'returns only starred projects the user can view' do - expect(user.viewable_starred_projects).to all(satisfy(&viewable?)) - end - - it 'rejects only starred projects the user can not view' do - expect(projects - user.viewable_starred_projects).not_to include(satisfy(&viewable?)) + expect(user.viewable_starred_projects).not_to include(private_project) end end end From 5f27e26bb4d073c04fd6d3f4116fc1a122db8c00 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 6 May 2016 13:16:53 +0100 Subject: [PATCH 102/449] Only generate repository push email once The repository push email can be very expensive to generate, especially with syntax-highlighted diffs. Instead of generating the email for each recipient, generate one email object and reset the Message-Id and To headers for each recipient. (Cloning would also be expensive in the case of large emails, although probably not as bad as generating from scratch.) --- CHANGELOG | 1 + app/mailers/emails/projects.rb | 5 +- app/workers/emails_on_push_worker.rb | 25 +++++-- lib/gitlab/email/message/repository_push.rb | 4 +- .../email/message/repository_push_spec.rb | 2 +- spec/mailers/notify_spec.rb | 36 ++-------- spec/workers/emails_on_push_worker_spec.rb | 65 +++++++++++++++---- 7 files changed, 83 insertions(+), 55 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1c21ad36b6..96643fc88a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,7 @@ v 8.8.0 (unreleased) - 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 - Update SVG sanitizer to conform to SVG 1.1 + - Speed up push emails with multiple recipients by only generating the email once - Updated search UI - Display informative message when new milestone is created - Sanitize milestones and labels titles diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 377c2999d6..5489283432 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -59,9 +59,9 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, opts = {}) + def repository_push_email(project_id, opts = {}) @message = - Gitlab::Email::Message::RepositoryPush.new(self, project_id, recipient, opts) + Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts) # used in notify layout @target_url = @message.target_url @@ -72,7 +72,6 @@ module Emails mail(from: sender(@message.author_id, @message.send_from_committer_email?), reply_to: @message.reply_to, - to: @message.recipient, subject: @message.subject) end end diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index c4d8595d45..6ebcba5f39 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,6 +1,8 @@ class EmailsOnPushWorker include Sidekiq::Worker + attr_reader :email, :skip_premailer + def perform(project_id, recipients, push_data, options = {}) options.symbolize_keys! options.reverse_merge!( @@ -41,11 +43,11 @@ class EmailsOnPushWorker end end - recipients.split(" ").each do |recipient| + recipients.split.each do |recipient| begin - Notify.repository_push_email( - project_id, + send_email( recipient, + project_id, author_id: author_id, ref: ref, action: action, @@ -53,14 +55,29 @@ class EmailsOnPushWorker reverse_compare: reverse_compare, send_from_committer_email: send_from_committer_email, disable_diffs: disable_diffs - ).deliver_now + ) + # These are input errors and won't be corrected even if Sidekiq retries rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}") end end ensure + @email = nil compare = nil GC.start end + + private + + def send_email(recipient, project_id, options) + # Generating the body of this email can be expensive, so only do it once + @skip_premailer ||= email.present? + @email ||= Notify.repository_push_email(project_id, options) + + email.to = recipient + email.add_message_id + email.header[:skip_premailer] = true if skip_premailer + email.deliver_now + end end diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 8f9be6cd9a..2c91a0487c 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -2,7 +2,6 @@ module Gitlab module Email module Message class RepositoryPush - attr_accessor :recipient attr_reader :author_id, :ref, :action include Gitlab::Routing.url_helpers @@ -11,13 +10,12 @@ module Gitlab delegate :name, to: :author, prefix: :author delegate :username, to: :author, prefix: :author - def initialize(notify, project_id, recipient, opts = {}) + def initialize(notify, project_id, opts = {}) raise ArgumentError, 'Missing options: author_id, ref, action' unless opts[:author_id] && opts[:ref] && opts[:action] @notify = notify @project_id = project_id - @recipient = recipient @opts = opts.dup @author_id = @opts.delete(:author_id) diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb index b2d7a79981..7d6cce6dae 100644 --- a/spec/lib/gitlab/email/message/repository_push_spec.rb +++ b/spec/lib/gitlab/email/message/repository_push_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::Email::Message::RepositoryPush do let!(:author) { create(:author, name: 'Author') } let(:message) do - described_class.new(Notify, project.id, 'recipient@example.com', opts) + described_class.new(Notify, project.id, opts) end context 'new commits have been pushed to repository' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 495c5cbac0..5f7e4a526e 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -593,7 +593,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -606,10 +606,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Pushed new branch master/ end @@ -624,7 +620,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -637,10 +633,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Pushed new tag v1\.0/ end @@ -654,7 +646,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -667,10 +659,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Deleted branch master/ end @@ -680,7 +668,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -693,10 +681,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Deleted tag v1\.0/ end @@ -710,7 +694,7 @@ describe Notify do let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) } let(:send_from_committer_email) { false } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -723,10 +707,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/ end @@ -818,7 +798,7 @@ describe Notify do let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like "a user cannot unsubscribe through footer link" @@ -831,10 +811,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /#{commits.first.title}/ end diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index 3600c77107..439da765c2 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -6,29 +6,66 @@ describe EmailsOnPushWorker do let(:project) { create(:project) } let(:user) { create(:user) } let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) } + let(:recipients) { user.email } + let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) } subject { EmailsOnPushWorker.new } - before do - allow(Project).to receive(:find).and_return(project) - end - describe "#perform" do - it "sends mail" do - subject.perform(project.id, user.email, data.stringify_keys) + context "when there are no errors in sending" do + let(:email) { ActionMailer::Base.deliveries.last } - email = ActionMailer::Base.deliveries.last - expect(email.subject).to include('Change some files') - expect(email.to).to eq([user.email]) + before { perform } + + it "sends a mail with the correct subject" do + expect(email.subject).to include('Change some files') + end + + it "sends the mail to the correct recipient" do + expect(email.to).to eq([user.email]) + end end - it "gracefully handles an input SMTP error" do - ActionMailer::Base.deliveries.clear - allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError) + context "when there is an SMTP error" do + before do + ActionMailer::Base.deliveries.clear + allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError) + perform + end - subject.perform(project.id, user.email, data.stringify_keys) + it "gracefully handles an input SMTP error" do + expect(ActionMailer::Base.deliveries.count).to eq(0) + end + end - expect(ActionMailer::Base.deliveries.count).to eq(0) + context "when there are multiple recipients" do + let(:recipients) do + 1.upto(5).map { |i| user.email.sub('@', "+#{i}@") }.join("\n") + end + + before do + # This is a hack because we modify the mail object before sending, for efficency, + # but the TestMailer adapter just appends the objects to an array. To clone a mail + # object, create a new one! + # https://github.com/mikel/mail/issues/314#issuecomment-12750108 + allow_any_instance_of(Mail::TestMailer).to receive(:deliver!).and_wrap_original do |original, mail| + original.call(Mail.new(mail.encoded)) + end + + ActionMailer::Base.deliveries.clear + end + + it "sends the mail to each of the recipients" do + perform + expect(ActionMailer::Base.deliveries.count).to eq(5) + expect(ActionMailer::Base.deliveries.map(&:to).flatten).to contain_exactly(*recipients.split) + end + + it "only generates the mail once" do + expect(Notify).to receive(:repository_push_email).once.and_call_original + expect(Premailer::Rails::CustomizedPremailer).to receive(:new).once.and_call_original + perform + end end end end From d9574a7b93de7e88c77ed1351650843749da60f0 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 5 May 2016 10:00:31 +0100 Subject: [PATCH 103/449] Group commits by date in server timezone `Time#to_date` just takes the (timezone-less) year, date, and month, and creates a new date from that. Because the commits in the list are grouped by date, rather than chunked when the date changes, a commit can be shown in the wrong order if its CommitDate has a timezone-less date that's different to other commits around it. Convert all CommitDates to the server timezone before grouping, as that will at least produce consistent results. Users can still see a timestamp on the commit that doesn't match the date it's grouped under, because the timestamp shown uses the user's local timezone, and the grouping uses the server's timezone, but that was an issue anyway. --- CHANGELOG | 1 + app/views/projects/commits/_commits.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e989e622b9..ab41545548 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ v 8.8.0 (unreleased) - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) - Added multiple colors for labels in dropdowns when dups happen. + - Always group commits by server timezone, not commit timestamp - Improve description for the Two-factor Authentication sign-in screen. (Connor Shea) - API support for the 'since' and 'until' operators on commit requests (Paco Guzman) - Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko) diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 64e8da9201..82f39e5928 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -3,7 +3,7 @@ - commits, hidden = limited_commits(@commits) -- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| +- commits.group_by { |c| c.committed_date.in_time_zone.to_date }.sort.reverse.each do |day, commits| .row.commits-row .col-md-2.hidden-xs.hidden-sm %h5.commits-row-date From 68aca6f6635090a33cf4d96c6dbc7ceab6ac2385 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 11 May 2016 11:37:49 +0200 Subject: [PATCH 104/449] trying to fix timing issue with import status --- app/controllers/projects/imports_controller.rb | 5 ++++- app/models/project.rb | 5 ----- app/services/projects/create_service.rb | 6 ++++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 7756f0f0ed..c70b9bf2f5 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -19,7 +19,10 @@ class Projects::ImportsController < Projects::ApplicationController if @project.import_failed? @project.import_retry else - @project.import_start + Project.transaction do + @project.import_start + end + @project.add_import_job if @project.persisted? end end diff --git a/app/models/project.rb b/app/models/project.rb index 05e14185a3..d807802ce1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -246,7 +246,6 @@ class Project < ActiveRecord::Base state :finished state :failed - after_transition any => :started, do: :schedule_add_import_job after_transition any => :finished, do: :clear_import_data end @@ -385,10 +384,6 @@ class Project < ActiveRecord::Base id && persisted? end - def schedule_add_import_job - run_after_commit(:add_import_job) - end - def add_import_job if forked? job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index a77652480b..53a16e95f5 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -59,6 +59,10 @@ module Projects end end + Project.transaction do + @project.import_start if @project.import? + end + after_create_actions if @project.persisted? @project @@ -95,8 +99,6 @@ module Projects unless @project.group @project.team << [current_user, :master, current_user] end - - @project.import_start if @project.import? end end end From 941301339dd8d1e282c3e225808875ef86b45f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 11 May 2016 12:32:48 +0200 Subject: [PATCH 105/449] Remove Rails monkey-patches now that we're using Rails 4.2.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- config/initializers/monkey_patch.rb | 48 ----------------------------- 1 file changed, 48 deletions(-) delete mode 100644 config/initializers/monkey_patch.rb diff --git a/config/initializers/monkey_patch.rb b/config/initializers/monkey_patch.rb deleted file mode 100644 index 62b05a5528..0000000000 --- a/config/initializers/monkey_patch.rb +++ /dev/null @@ -1,48 +0,0 @@ -## This patch is from rails 4.2-stable. Remove it when 4.2.6 is released -## https://github.com/rails/rails/issues/21108 - -module ActiveRecord - module ConnectionAdapters - class AbstractMysqlAdapter < AbstractAdapter - # SHOW VARIABLES LIKE 'name' - def show_variable(name) - variables = select_all("select @@#{name} as 'Value'", 'SCHEMA') - variables.first['Value'] unless variables.empty? - rescue ActiveRecord::StatementInvalid - nil - end - - - # MySQL is too stupid to create a temporary table for use subquery, so we have - # to give it some prompting in the form of a subsubquery. Ugh! - def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] - - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - # Materialized subquery by adding distinct - # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.from subsubselect.distinct.as('__active_record_temp') - end - end - end -end - -module ActiveRecord - module ConnectionAdapters - class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL'.freeze - - # Get the client encoding for this database - def client_encoding - return @client_encoding if @client_encoding - - result = exec_query( - "select @@character_set_client", - 'SCHEMA') - @client_encoding = ENCODINGS[result.rows.last.last] - end - end - end -end From a2934f45317ed64beb948b8487199251a987c4a1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 11 May 2016 12:39:07 +0200 Subject: [PATCH 106/449] Use % notation for arrays in runner model constants --- app/models/ci/runner.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index d27963e8ec..e598a13286 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -22,8 +22,8 @@ module Ci extend Ci::Model LAST_CONTACT_TIME = 5.minutes.ago - AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] - FORM_EDITABLE = [:description, :tag_list, :active] + AVAILABLE_SCOPES = %w[specific shared active paused online] + FORM_EDITABLE = %i[description tag_list active] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' From c446a86510a6586c69a306ae09e41053f20d11a1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 11 May 2016 14:19:30 +0200 Subject: [PATCH 107/449] Updated 8.7.4 CHANGELOG entries [ci skip] --- CHANGELOG | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 54c79551b1..caa0923573 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,9 +43,13 @@ v 8.8.0 (unreleased) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 v 8.7.4 - - Fix always showing build notification message when switching between merge requests - - Links for Redmine issue references are generated correctly again (Benedikt Huss) - - Fix an issue when filtering merge requests with more than one label. !3886 + - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) + - Fix setting trusted proxies !3970 + - Fix BitBucket importer bug when throwing exceptions !3941 + - Use sign out path only if not empty !3989 + - Running rake gitlab:db:drop_tables now drops tables with cascade !4020 + - Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100 + - Use a case-insensitive comparison in sanitizing URI schemes v 8.7.3 - Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented From 5c59ba35c9ae6a7e5d6d6cd20d0b2ba51166f60f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 11 May 2016 15:08:27 +0200 Subject: [PATCH 108/449] fix complexity of method --- app/services/projects/create_service.rb | 30 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 53a16e95f5..1693ae609c 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -50,18 +50,9 @@ module Projects @project.build_forked_project_link(forked_from_project_id: forked_from_project_id) end - Project.transaction do - @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data - @project.save + process_import_data(import_data) - if @project.persisted? && !@project.import? - raise 'Failed to create repository' unless @project.create_repository - end - end - - Project.transaction do - @project.import_start if @project.import? - end + start_import if @project.import? after_create_actions if @project.persisted? @@ -100,5 +91,22 @@ module Projects @project.team << [current_user, :master, current_user] end end + + def start_import + Project.transaction do + @project.import_start + end + end + + def process_import_data(import_data) + Project.transaction do + @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data + @project.save + + if @project.persisted? && !@project.import? + raise 'Failed to create repository' unless @project.create_repository + end + end + end end end From 973cd0fec7b69d1398a86df67d10f7c38a9a4ced Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 11 May 2016 18:08:17 +0300 Subject: [PATCH 109/449] Update 8.8 guides --- doc/install/installation.md | 2 +- doc/update/8.7-to-8.8.md | 154 ++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 doc/update/8.7-to-8.8.md diff --git a/doc/install/installation.md b/doc/install/installation.md index e3af302226..44ae0be406 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -269,7 +269,7 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-7-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-8-stable gitlab **Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md new file mode 100644 index 0000000000..b4d9212289 --- /dev/null +++ b/doc/update/8.7-to-8.8.md @@ -0,0 +1,154 @@ +# From 8.7 to 8.8 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-8-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v2.7.2 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.1 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### Git configuration + +Disable `git gc --auto` because GitLab runs `git gc` for us already. + +```sh +sudo -u git -H git config --global gc.auto 0 +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-7-stable:lib/support/nginx/gitlab-ssl origin/8-8-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-7-stable:lib/support/nginx/gitlab origin/8-8-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.6) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.6 to 8.7](8.6-to-8.7.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. From b523dec83dc832029c6e40daaeb121920bbcd394 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 14:11:28 -0400 Subject: [PATCH 110/449] Remove unused `icon_for_event` helper --- app/helpers/events_helper.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 592bad8ba2..0bf328e7d1 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -39,15 +39,6 @@ module EventsHelper end end - def icon_for_event - { - EventFilter.push => 'upload', - EventFilter.merged => 'check-square-o', - EventFilter.comments => 'comments', - EventFilter.team => 'user', - } - end - def event_preposition(event) if event.push? || event.commented? || event.target "at" From c8f23bd2edc19f968446b149120df1f7798eb4b1 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 11 May 2016 17:27:08 -0700 Subject: [PATCH 111/449] Support token header for health check token, and general cleanup of the health_check feature. --- app/controllers/health_check_controller.rb | 15 ++++++-- app/views/admin/health_check/show.html.haml | 36 +++++++++++-------- config/initializers/health_check.rb | 2 +- config/routes.rb | 2 +- .../health_check_controller_spec.rb | 15 ++++++++ 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb index b974489836..037da7d2bc 100644 --- a/app/controllers/health_check_controller.rb +++ b/app/controllers/health_check_controller.rb @@ -1,13 +1,22 @@ class HealthCheckController < HealthCheck::HealthCheckController before_action :validate_health_check_access! - protected + private def validate_health_check_access! - return render_404 unless params[:token].presence && params[:token] == current_application_settings.health_check_access_token + render_404 unless token_valid? + end + + def token_valid? + token = params[:token].presence || request.headers['TOKEN'] + token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare( + token, + current_application_settings.health_check_access_token + ) end def render_404 - render file: Rails.root.join("public", "404"), layout: false, status: "404" + render file: Rails.root.join('public', '404'), layout: false, status: '404' end end diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 23a931995a..ed7025f7a0 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -2,29 +2,35 @@ %h3.page-title Health Check -%p.light +.bs-callout.clearfix + .pull-left + %p Access token is - %code{ id:'health-check-token' }= "#{current_application_settings.health_check_access_token}" + %code#health-check-token= current_application_settings.health_check_access_token + = button_to reset_health_check_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset the health check token?' } do + = icon('refresh') + Reset health check access token %p.light Health information can be reteived as plain text, json, or xml using: %ul %li - %code= "/health_check?token=#{current_application_settings.health_check_access_token}" + %code= health_check_url(token:current_application_settings.health_check_access_token) %li - %code= "/health_check.json?token=#{current_application_settings.health_check_access_token}" + %code= health_check_url(token:current_application_settings.health_check_access_token, format: :json) %li - %code= "/health_check.xml?token=#{current_application_settings.health_check_access_token}" + %code= health_check_url(token:current_application_settings.health_check_access_token, format: :xml) -.bs-callout.clearfix - .pull-left - %p - You can reset the health check access token by pressing the button below. - %p - = button_to reset_health_check_token_admin_application_settings_path, - method: :put, class: 'btn btn-default', - data: { confirm: 'Are you sure you want to reset the health check token?' } do - = icon('refresh') - Reset health check access token +%p.light + You can also ask for the status of specific services: + %ul + %li + %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :cache) + %li + %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :database) + %li + %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :migrations) %hr .panel.panel-default diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb index a93e427303..79e2d23ab2 100644 --- a/config/initializers/health_check.rb +++ b/config/initializers/health_check.rb @@ -1,3 +1,3 @@ HealthCheck.setup do |config| - config.standard_checks = [ 'database', 'migrations', 'cache' ] + config.standard_checks = ['database', 'migrations', 'cache'] end diff --git a/config/routes.rb b/config/routes.rb index c81bf294a5..f794a881f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,7 +74,7 @@ Rails.application.routes.draw do end # Health check - get 'health_check(/:checks)(.:format)' => 'health_check#index' + get 'health_check(/:checks)' => 'health_check#index', as: :health_check # Enable Grack support mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb index 3b9cc5c98f..0d8a68bb51 100644 --- a/spec/controllers/health_check_controller_spec.rb +++ b/spec/controllers/health_check_controller_spec.rb @@ -14,6 +14,13 @@ describe HealthCheckController do end context 'when services are up and an access token is provided' do + it 'supports passing the token in the header' do + request.headers['TOKEN'] = token + get :index + expect(response).to be_success + expect(response.content_type).to eq 'text/plain' + end + it 'supports successful plaintest response' do get :index, token: token expect(response).to be_success @@ -55,6 +62,14 @@ describe HealthCheckController do allow(HealthCheck::Utils).to receive(:process_checks).with('email').and_return('Email is on fire') end + it 'supports passing the token in the header' do + request.headers['TOKEN'] = token + get :index + expect(response.status).to eq(500) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to include('The server is on fire') + end + it 'supports failure plaintest response' do get :index, token: token expect(response.status).to eq(500) From a59ad3936a0bdbfd64d9c54af631a272317fe680 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 5 May 2016 15:38:01 +0530 Subject: [PATCH 112/449] Add a spec for `WikiLinkFilter` - And fix behavior for non-file hierarchical links. --- lib/banzai/filter/wiki_link_filter.rb | 11 ++- .../banzai/filter/wiki_link_filter_spec.rb | 77 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 spec/lib/banzai/filter/wiki_link_filter_spec.rb diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb index 06d10c9850..7dc771afd7 100644 --- a/lib/banzai/filter/wiki_link_filter.rb +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -25,7 +25,7 @@ module Banzai end def process_link_attr(html_attr) - return if html_attr.blank? || file_reference?(html_attr) + return if html_attr.blank? || file_reference?(html_attr) || hierarchical_link?(html_attr) uri = URI(html_attr.value) if uri.relative? && uri.path.present? @@ -40,12 +40,17 @@ module Banzai uri end + def project_wiki + context[:project_wiki] + end + def file_reference?(html_attr) !File.extname(html_attr.value).blank? end - def project_wiki - context[:project_wiki] + # Of the form `./link`, `../link`, or similar + def hierarchical_link?(html_attr) + html_attr.value[0] == '.' end def project_wiki_base_path diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb new file mode 100644 index 0000000000..56b1a26739 --- /dev/null +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Banzai::Filter::WikiLinkFilter, lib: true do + include FilterSpecHelper + + let(:namespace) { build(:namespace, name: "wiki_link_ns") } + let(:project) { build(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } + let(:user) { double } + let(:project_wiki) { ProjectWiki.new(project, user) } + + describe "links within the wiki (relative)" do + describe "hierarchical links to the current directory" do + it "doesn't rewrite non-file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./page.md') + end + end + + describe "hierarchical links to the parent directory" do + it "doesn't rewrite non-file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('../page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('../page.md') + end + end + + describe "hierarchical links to a sub-directory" do + it "doesn't rewrite non-file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page.md') + end + end + + describe "non-hierarchical links" do + it 'rewrites non-file links to be at the scope of the wiki root' do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to match('/wiki_link_ns/wiki_link_project/wikis/page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('page.md') + end + end + end + + describe "links outside the wiki (absolute)" do + it "doesn't rewrite links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('http://example.com/page') + end + end +end From a4ee7d25e398f9d2e2311703fbbcf5f6b9bdf728 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 9 May 2016 16:04:05 +0530 Subject: [PATCH 113/449] Implement @rymai's feedback after review. - Separate 'exercise' and 'verify' steps of tests. - Use `build_stubbed` instead of `build` --- spec/lib/banzai/filter/wiki_link_filter_spec.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb index 56b1a26739..185abbb210 100644 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Banzai::Filter::WikiLinkFilter, lib: true do include FilterSpecHelper - let(:namespace) { build(:namespace, name: "wiki_link_ns") } - let(:project) { build(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } + let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") } + let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } let(:user) { double } let(:project_wiki) { ProjectWiki.new(project, user) } @@ -13,12 +13,14 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite non-file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./page') end it "doesn't rewrite file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./page.md') end end @@ -27,12 +29,14 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite non-file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('../page') end it "doesn't rewrite file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('../page.md') end end @@ -41,12 +45,14 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite non-file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page') end it "doesn't rewrite file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page.md') end end @@ -62,6 +68,7 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite file links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('page.md') end end @@ -71,6 +78,7 @@ describe Banzai::Filter::WikiLinkFilter, lib: true do it "doesn't rewrite links" do link = "Link to Page" filtered_link = filter(link, project_wiki: project_wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('http://example.com/page') end end From 555610b619b552ca058a7656f373743db4d34765 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 10 May 2016 09:04:14 +0530 Subject: [PATCH 114/449] Add CHANGELOG entry. --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d5ab79602f..187011c601 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,9 @@ v 8.8.0 (unreleased) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 +v 8.7.5 + - Fix relative links in wiki pages. !4050 + v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) - Fix setting trusted proxies !3970 From ef7f793dc4f0425e1fd5e358c162f2b08a160142 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 10:12:29 +0200 Subject: [PATCH 115/449] fix silly typo --- app/services/projects/create_service.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 1693ae609c..c61a515a6b 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -56,6 +56,8 @@ module Projects after_create_actions if @project.persisted? + @project.add_import_job if @project.import? + @project rescue => e message = "Unable to save project: #{e.message}" From 7db3fb5a0b6c07b65c61c920e18f056f6c127932 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:05:34 +0300 Subject: [PATCH 116/449] disabled_oauth_sign_in_sources column --- ...disabled_oauth_sign_in_sources_to_application_settings.rb | 5 +++++ db/schema.rb | 1 + 2 files changed, 6 insertions(+) create mode 100644 db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb diff --git a/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb new file mode 100644 index 0000000000..facd33875b --- /dev/null +++ b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddDisabledOauthSignInSourcesToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :disabled_oauth_sign_in_sources, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 71d953afe3..74facd1208 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -80,6 +80,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.boolean "repository_checks_enabled", default: false t.text "shared_runners_text" t.integer "metrics_packet_size", default: 1 + t.text "disabled_oauth_sign_in_sources" end create_table "audit_events", force: :cascade do |t| From ca288587256b8995105b601ef02850f388d59ff9 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:06:18 +0300 Subject: [PATCH 117/449] serialize the disabled_oauth_sign_in_sources --- app/models/application_setting.rb | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 7039db2d41..39f74b2082 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,3 +1,52 @@ +# == Schema Information +# +# Table name: application_settings +# +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) +# max_attachment_size :integer default(10), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# default_group_visibility :integer +# restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null +# import_sources :text +# disabled_oauth_sign_in_sources :text +# help_page_text :text +# admin_notification_email :string(255) +# shared_runners_enabled :boolean default(TRUE), not null +# max_artifacts_size :integer default(100), not null +# runners_registration_token :string +# require_two_factor_authentication :boolean default(FALSE) +# two_factor_grace_period :integer default(48) +# metrics_enabled :boolean default(FALSE) +# metrics_host :string default("localhost") +# metrics_username :string +# metrics_password :string +# metrics_pool_size :integer default(16) +# metrics_timeout :integer default(10) +# metrics_method_call_threshold :integer default(10) +# recaptcha_enabled :boolean default(FALSE) +# recaptcha_site_key :string +# recaptcha_private_key :string +# metrics_port :integer default(8089) +# sentry_enabled :boolean default(FALSE) +# sentry_dsn :string +# email_author_in_body :boolean default(FALSE) +# + class ApplicationSetting < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :runners_registration_token @@ -6,6 +55,7 @@ class ApplicationSetting < ActiveRecord::Base serialize :restricted_visibility_levels serialize :import_sources + serialize :disabled_oauth_sign_in_sources serialize :restricted_signup_domains, Array attr_accessor :restricted_signup_domains_raw From e5ddd6d21eb6125605d3f47ea5db884b54514796 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:08:06 +0300 Subject: [PATCH 118/449] disabled_oauth_sign_in_sources parameter handling in ApplicationSettingsController --- .../admin/application_settings_controller.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ec22548dde..cebc01a2af 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -53,6 +53,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end + disabled_oauth_sign_in_sources = params[:application_setting][:disabled_oauth_sign_in_sources] + if disabled_oauth_sign_in_sources.nil? + params[:application_setting][:disabled_oauth_sign_in_sources] = [] + else + disabled_oauth_sign_in_sources.map! do |source| + source.to_str + end + end + params.require(:application_setting).permit( :default_projects_limit, :default_branch_protection, @@ -95,7 +104,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :repository_checks_enabled, :metrics_packet_size, restricted_visibility_levels: [], - import_sources: [] + import_sources: [], + disabled_oauth_sign_in_sources: [] ) end end From d8085d8e128e4ef50443f7a58bfbe7bf21565ab0 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:32:33 +0300 Subject: [PATCH 119/449] default value for disabled_oauth_sign_in_sources in create_from_defaults @ ApplicationSetting --- app/models/application_setting.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 39f74b2082..a48deccb02 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -157,6 +157,7 @@ class ApplicationSetting < ActiveRecord::Base recaptcha_enabled: false, akismet_enabled: false, repository_checks_enabled: true, + disabled_oauth_sign_in_sources: [] ) end From cf300443147f1a9ba3acf83ce839afdffa5173bd Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:33:33 +0300 Subject: [PATCH 120/449] OAuth Providers disable from the ApplicationSettings page --- app/helpers/application_settings_helper.rb | 15 +++++++++++++++ .../admin/application_settings/_form.html.haml | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 914b0ef604..17d7389987 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -60,4 +60,19 @@ module ApplicationSettingsHelper end end end + + def oauth_providers_checkboxes(help_block_id) + button_based_providers.map do |source| + checked = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) + css_class = 'btn' + css_class += ' active' if checked + checkbox_name = 'application_setting[disabled_oauth_sign_in_sources][]' + + label_tag(checkbox_name, class: css_class) do + check_box_tag(checkbox_name, source, checked, + autocomplete: 'off', + 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) + end + end + end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index e0d8d16a95..057b237c06 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -109,6 +109,16 @@ = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled + .form-group + = f.label :disable_oauth_signin_sources, class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - oauth_providers_checkboxes('oauth-providers-help').each do |source| + = source + %span.help-block#oauth-providers-help + Enabled OmniAuth must be configured for GitHub + = link_to "(?)", help_page_path("integration", "github") .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From 8d26836e945f3e66a46370905b9fb34244a0ff91 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:35:03 +0300 Subject: [PATCH 121/449] method to get the enabled_button_based_providers --- app/helpers/auth_helper.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index b4f80fd9b3..24eb813d0b 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -38,6 +38,12 @@ module AuthHelper auth_providers.reject { |provider| form_based_provider?(provider) } end + def enabled_button_based_providers + disabled_providers = current_application_settings.disabled_oauth_sign_in_sources || [] + + button_based_providers.map(&:to_s) - disabled_providers + end + def provider_image_tag(provider, size = 64) label = label_for_provider(provider) From d943e5f691ad9c59e349eab95f1a21bd0f85ab1b Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:36:15 +0300 Subject: [PATCH 122/449] method to check if oauth button based providers are enabled --- app/helpers/auth_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 24eb813d0b..3e1f420925 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -44,6 +44,10 @@ module AuthHelper button_based_providers.map(&:to_s) - disabled_providers end + def button_based_providers_enabled? + !enabled_button_based_providers.empty? + end + def provider_image_tag(provider, size = 64) label = label_for_provider(provider) From 284eccb5e68c863ea8e1001a24af33dd0da5e201 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:36:57 +0300 Subject: [PATCH 123/449] render the omniauth_box if there are any button_based_providers --- app/views/devise/sessions/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index d65fa60025..28194506ac 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -4,7 +4,7 @@ = render 'devise/shared/signin_box' -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box - - if omniauth_enabled? && devise_mapping.omniauthable? + - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled? .clearfix.prepend-top-20 = render 'devise/shared/omniauth_box' From 28da3a88de339a95d49e171da87494f65219cb21 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 13:37:18 +0300 Subject: [PATCH 124/449] show only the eanbled oauth providers --- app/views/devise/shared/_omniauth_box.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index ecf680e7b2..de18bc2d84 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,7 +1,7 @@ %p %span.light Sign in with   - - providers = button_based_providers + - providers = enabled_button_based_providers - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) From 7c7c5b7e40ff4fed64a9f62beed027064c292eaf Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 15:22:24 +0300 Subject: [PATCH 125/449] text for the disable_oauth_signin_sources label --- app/views/admin/application_settings/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 057b237c06..729c2ff563 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -110,7 +110,7 @@ = f.check_box :signin_enabled Sign-in enabled .form-group - = f.label :disable_oauth_signin_sources, class: 'control-label col-sm-2' + = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 - data_attrs = { toggle: 'buttons' } .btn-group{ data: data_attrs } From 47ee5125e881694b7713f187b48589a2f4bbd747 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 15:57:00 +0300 Subject: [PATCH 126/449] validate disabled_oauth_sign_in_sources in ApplicationSe --- app/models/application_setting.rb | 10 ++++++++++ spec/models/application_setting_spec.rb | 3 +++ 2 files changed, 13 insertions(+) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a48deccb02..d565dbc71d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -119,6 +119,16 @@ class ApplicationSetting < ActiveRecord::Base end end + validates_each :disabled_oauth_sign_in_sources do |record, attr, value| + unless value.nil? + value.each do |source| + unless Devise.omniauth_providers.include?(source.to_sym) + record.errors.add(attr, "'#{source}' is not an ouath sign-in source") + end + end + end + end + before_save :ensure_runners_registration_token after_commit do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 1ce22feed5..fb3ea491df 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -20,6 +20,9 @@ describe ApplicationSetting, models: true do it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } + it { is_expected.to allow_value([:github]).for(:disabled_oauth_sign_in_sources) } + it { is_expected.not_to allow_value([:test]).for(:disabled_oauth_sign_in_sources) } + it { is_expected.to validate_presence_of(:max_attachment_size) } it do From 8c2b72b1c80e8bbd082ac8e1aedb21a1aad07235 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 17:04:54 +0300 Subject: [PATCH 127/449] tests for enabled_button_based_providers helper method of AuthHelper --- spec/helpers/auth_helper_spec.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index e47a54fdac..5d66c92d26 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -2,7 +2,9 @@ require "spec_helper" describe AuthHelper do describe "button_based_providers" do - it 'returns all enabled providers' do + let(:settings) { ApplicationSetting.create_from_defaults } + + it 'returns all enabled providers from devise' do allow(helper).to receive(:auth_providers) { [:twitter, :github] } expect(helper.button_based_providers).to include(*[:twitter, :github]) end @@ -16,5 +18,23 @@ describe AuthHelper do allow(helper).to receive(:auth_providers) { [] } expect(helper.button_based_providers).to eq([]) end + + it 'returns all the enabled providers from settings' do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) + end + + it 'should not return github as provider because it\'s disabled from settings' do + settings.update_attribute( + :disabled_oauth_sign_in_sources, + ['github'] + ) + + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + allow(helper).to receive(:current_application_settings) { settings } + + expect(helper.enabled_button_based_providers).to include('twitter') + expect(helper.enabled_button_based_providers).to_not include('github') + end end end From 6f69f6a1ab70bd2252803a64927d0c8df1d03612 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 4 May 2016 17:06:07 +0300 Subject: [PATCH 128/449] tests for button_based_providers_enabled? helper method of AuthHelper --- spec/helpers/auth_helper_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 5d66c92d26..a6a366bc19 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -36,5 +36,23 @@ describe AuthHelper do expect(helper.enabled_button_based_providers).to include('twitter') expect(helper.enabled_button_based_providers).to_not include('github') end + + it 'returns true for button_based_providers_enabled? because there providers' do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + + expect(helper.button_based_providers_enabled?).to be true + end + + it 'returns false for button_based_providers_enabled? because there providers' do + settings.update_attribute( + :disabled_oauth_sign_in_sources, + ['github', 'twitter'] + ) + + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + allow(helper).to receive(:current_application_settings) { settings } + + expect(helper.button_based_providers_enabled?).to be false + end end end From deca3da1a7f4e59c97c27bedf7aa26185a4d883b Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 5 May 2016 10:34:51 +0300 Subject: [PATCH 129/449] stub Devise.omniauth_providers to return GitHub even if the gitlab.yml has no omniauth provider enabled This will fix failing tests in case gitlab.yml file has no omniauth providers enabled --- spec/models/application_setting_spec.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index fb3ea491df..d84f3e998f 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -20,8 +20,14 @@ describe ApplicationSetting, models: true do it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } - it { is_expected.to allow_value([:github]).for(:disabled_oauth_sign_in_sources) } - it { is_expected.not_to allow_value([:test]).for(:disabled_oauth_sign_in_sources) } + describe 'disabled_oauth_sign_in_sources validations' do + before do + allow(Devise).to receive(:omniauth_providers).and_return([:github]) + end + + it { is_expected.to allow_value(['github']).for(:disabled_oauth_sign_in_sources) } + it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) } + end it { is_expected.to validate_presence_of(:max_attachment_size) } From 2e4c914ae88b77c8a3871f8415163a51e23254e5 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 10:31:25 +0300 Subject: [PATCH 130/449] between "" --- spec/helpers/auth_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index a6a366bc19..6dc1135927 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -24,7 +24,7 @@ describe AuthHelper do expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) end - it 'should not return github as provider because it\'s disabled from settings' do + it "should not return github as provider because it's disabled from settings" do settings.update_attribute( :disabled_oauth_sign_in_sources, ['github'] From fc88527c9e21f99a9411423f1c8a6475957c02f3 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 10:40:27 +0300 Subject: [PATCH 131/449] use stub_application_setting instead --- spec/helpers/auth_helper_spec.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 6dc1135927..e6af074a78 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -2,8 +2,6 @@ require "spec_helper" describe AuthHelper do describe "button_based_providers" do - let(:settings) { ApplicationSetting.create_from_defaults } - it 'returns all enabled providers from devise' do allow(helper).to receive(:auth_providers) { [:twitter, :github] } expect(helper.button_based_providers).to include(*[:twitter, :github]) @@ -25,13 +23,11 @@ describe AuthHelper do end it "should not return github as provider because it's disabled from settings" do - settings.update_attribute( - :disabled_oauth_sign_in_sources, - ['github'] + stub_application_setting( + disabled_oauth_sign_in_sources: ['github'] ) allow(helper).to receive(:auth_providers) { [:twitter, :github] } - allow(helper).to receive(:current_application_settings) { settings } expect(helper.enabled_button_based_providers).to include('twitter') expect(helper.enabled_button_based_providers).to_not include('github') @@ -44,13 +40,11 @@ describe AuthHelper do end it 'returns false for button_based_providers_enabled? because there providers' do - settings.update_attribute( - :disabled_oauth_sign_in_sources, - ['github', 'twitter'] + stub_application_setting( + disabled_oauth_sign_in_sources: ['github', 'twitter'] ) allow(helper).to receive(:auth_providers) { [:twitter, :github] } - allow(helper).to receive(:current_application_settings) { settings } expect(helper.button_based_providers_enabled?).to be false end From 3922e7ee2249bb5a683b7474b281d8e83d465740 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 10:42:57 +0300 Subject: [PATCH 132/449] enabled_button_based_providers into their own describe section --- spec/helpers/auth_helper_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index e6af074a78..04b5128279 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -16,7 +16,9 @@ describe AuthHelper do allow(helper).to receive(:auth_providers) { [] } expect(helper.button_based_providers).to eq([]) end + end + describe 'enabled_button_based_providers' do it 'returns all the enabled providers from settings' do allow(helper).to receive(:auth_providers) { [:twitter, :github] } expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) From 038dbb6803948e050484ed3f34c99b2f3bf7a6c5 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 10:51:24 +0300 Subject: [PATCH 133/449] DRYing enabled_button_based_providers tests --- spec/helpers/auth_helper_spec.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 04b5128279..a6df820421 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -19,8 +19,11 @@ describe AuthHelper do end describe 'enabled_button_based_providers' do - it 'returns all the enabled providers from settings' do + before do allow(helper).to receive(:auth_providers) { [:twitter, :github] } + end + + it 'returns all the enabled providers from settings' do expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) end @@ -29,15 +32,11 @@ describe AuthHelper do disabled_oauth_sign_in_sources: ['github'] ) - allow(helper).to receive(:auth_providers) { [:twitter, :github] } - expect(helper.enabled_button_based_providers).to include('twitter') expect(helper.enabled_button_based_providers).to_not include('github') end it 'returns true for button_based_providers_enabled? because there providers' do - allow(helper).to receive(:auth_providers) { [:twitter, :github] } - expect(helper.button_based_providers_enabled?).to be true end @@ -46,8 +45,6 @@ describe AuthHelper do disabled_oauth_sign_in_sources: ['github', 'twitter'] ) - allow(helper).to receive(:auth_providers) { [:twitter, :github] } - expect(helper.button_based_providers_enabled?).to be false end end From 3c3bc93747ebe5864a50d7b8203b357a40012ed4 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 19:32:01 +0300 Subject: [PATCH 134/449] if omniauth is enabled and if there are button_based_providers available show the disable OAuth sources --- .../application_settings/_form.html.haml | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 729c2ff563..81c232b7e9 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -109,16 +109,17 @@ = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled - .form-group - = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' - .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } - - oauth_providers_checkboxes('oauth-providers-help').each do |source| - = source - %span.help-block#oauth-providers-help - Enabled OmniAuth must be configured for GitHub - = link_to "(?)", help_page_path("integration", "github") + - if omniauth_enabled? && !button_based_providers.empty? + .form-group + = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - oauth_providers_checkboxes('oauth-providers-help').each do |source| + = source + %span.help-block#oauth-providers-help + Enabled OmniAuth must be configured for GitHub + = link_to "(?)", help_page_path("integration", "github") .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From dad501a2367a35f6d791efac9002e487176f2c9e Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 19:34:44 +0300 Subject: [PATCH 135/449] no need to show the help about Github or the other authentication sources --- app/views/admin/application_settings/_form.html.haml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 81c232b7e9..99f86e5244 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -117,9 +117,6 @@ .btn-group{ data: data_attrs } - oauth_providers_checkboxes('oauth-providers-help').each do |source| = source - %span.help-block#oauth-providers-help - Enabled OmniAuth must be configured for GitHub - = link_to "(?)", help_page_path("integration", "github") .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From a2d4c349b5772231d7428cf32ac8a66d70beb627 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Mon, 9 May 2016 19:38:07 +0300 Subject: [PATCH 136/449] unindent 1 level the Disable OAuth form --- .../admin/application_settings/_form.html.haml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 99f86e5244..383f206079 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -109,14 +109,14 @@ = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled - - if omniauth_enabled? && !button_based_providers.empty? - .form-group - = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' - .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } - - oauth_providers_checkboxes('oauth-providers-help').each do |source| - = source + - if omniauth_enabled? && !button_based_providers.empty? + .form-group + = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - oauth_providers_checkboxes('oauth-providers-help').each do |source| + = source .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From 81d0a4a47390f3810b03528182d3eea68ab46112 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 10:21:14 +0300 Subject: [PATCH 137/449] typo --- app/models/application_setting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index d565dbc71d..667b45335b 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -123,7 +123,7 @@ class ApplicationSetting < ActiveRecord::Base unless value.nil? value.each do |source| unless Devise.omniauth_providers.include?(source.to_sym) - record.errors.add(attr, "'#{source}' is not an ouath sign-in source") + record.errors.add(attr, "'#{source}' is not an OAuth sign-in source") end end end From 96122034cfc2eb7039ae75b20b729f35e9aa832e Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 11:17:37 +0300 Subject: [PATCH 138/449] more readable specs for enabled_button_based_providers and button_based_providers_enabled? --- spec/helpers/auth_helper_spec.rb | 44 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index a6df820421..16fbb5dcec 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -23,29 +23,43 @@ describe AuthHelper do allow(helper).to receive(:auth_providers) { [:twitter, :github] } end - it 'returns all the enabled providers from settings' do - expect(helper.enabled_button_based_providers).to include(*['twitter', 'github']) + context 'all providers are enabled to sign in' do + it 'returns all the enabled providers from settings' do + expect(helper.enabled_button_based_providers).to include('twitter', 'github') + end end - it "should not return github as provider because it's disabled from settings" do - stub_application_setting( - disabled_oauth_sign_in_sources: ['github'] - ) + context 'GitHub OAuth sign in is disabled from application setting' do + it "doesn't return github as provider" do + stub_application_setting( + disabled_oauth_sign_in_sources: ['github'] + ) - expect(helper.enabled_button_based_providers).to include('twitter') - expect(helper.enabled_button_based_providers).to_not include('github') + expect(helper.enabled_button_based_providers).to include('twitter') + expect(helper.enabled_button_based_providers).to_not include('github') + end + end + end + + describe 'button_based_providers_enabled?' do + before do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } end - it 'returns true for button_based_providers_enabled? because there providers' do - expect(helper.button_based_providers_enabled?).to be true + context 'button based providers enabled' do + it 'returns true' do + expect(helper.button_based_providers_enabled?).to be true + end end - it 'returns false for button_based_providers_enabled? because there providers' do - stub_application_setting( - disabled_oauth_sign_in_sources: ['github', 'twitter'] - ) + context 'all the button based providers are disabled via application_setting' do + it 'returns false' do + stub_application_setting( + disabled_oauth_sign_in_sources: ['github', 'twitter'] + ) - expect(helper.button_based_providers_enabled?).to be false + expect(helper.button_based_providers_enabled?).to be false + end end end end From 7818f7329a436d86b30dff003b60ab8a15a4f0db Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 11:29:19 +0300 Subject: [PATCH 139/449] On Application Settings Page let the user select the enabled OAuth Sign in sources instead of the disabled ones --- .../admin/application_settings_controller.rb | 16 ++++++++-------- app/helpers/application_settings_helper.rb | 8 ++++---- .../admin/application_settings/_form.html.haml | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index cebc01a2af..d7a052700a 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -53,14 +53,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end - disabled_oauth_sign_in_sources = params[:application_setting][:disabled_oauth_sign_in_sources] - if disabled_oauth_sign_in_sources.nil? - params[:application_setting][:disabled_oauth_sign_in_sources] = [] - else - disabled_oauth_sign_in_sources.map! do |source| - source.to_str - end - end + enabled_oauth_sign_in_sources = params[:application_setting][:enabled_oauth_sign_in_sources] + + params[:application_setting][:disabled_oauth_sign_in_sources] = + AuthHelper.button_based_providers.map(&:to_s) - + (enabled_oauth_sign_in_sources.nil? ? [] : enabled_oauth_sign_in_sources) + + params[:application_setting].delete(:enabled_oauth_sign_in_sources) params.require(:application_setting).permit( :default_projects_limit, @@ -105,6 +104,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_packet_size, restricted_visibility_levels: [], import_sources: [], + enabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [] ) end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 17d7389987..ced8e16de7 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -63,13 +63,13 @@ module ApplicationSettingsHelper def oauth_providers_checkboxes(help_block_id) button_based_providers.map do |source| - checked = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) + disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) css_class = 'btn' - css_class += ' active' if checked - checkbox_name = 'application_setting[disabled_oauth_sign_in_sources][]' + css_class += ' active' unless disabled + checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' label_tag(checkbox_name, class: css_class) do - check_box_tag(checkbox_name, source, checked, + check_box_tag(checkbox_name, source, !disabled, autocomplete: 'off', 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 383f206079..ecd032acfd 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -111,7 +111,7 @@ Sign-in enabled - if omniauth_enabled? && !button_based_providers.empty? .form-group - = f.label :disable_oauth_signin_sources, 'Disable OAuth Sign-In sources', class: 'control-label col-sm-2' + = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 - data_attrs = { toggle: 'buttons' } .btn-group{ data: data_attrs } From bc099d799ea502c9fd8f1a09f07af70cdcf51156 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 11:50:53 +0300 Subject: [PATCH 140/449] Allow Admins to remove the Login with buttons for OAuth services and still be able to import on the changelog --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 187011c601..8d70ef9944 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -55,6 +55,8 @@ v 8.7.4 - Running rake gitlab:db:drop_tables now drops tables with cascade !4020 - Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100 - Use a case-insensitive comparison in sanitizing URI schemes + - Merge request widget displays TeamCity build state and code coverage correctly again. + - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034 v 8.7.3 - Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented From 3a91f5081eeeef8dca52a418aba2d786cbc8ddf4 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 11:53:22 +0300 Subject: [PATCH 141/449] documentation for enabling or disabling OAuth sign in --- .../img/enabled-oauth-sign-in-sources.png | Bin 0 -> 49081 bytes doc/integration/omniauth.md | 14 ++++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 doc/integration/img/enabled-oauth-sign-in-sources.png diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..95f8bbdcd2489c4eb14c100c92dffadf3a1a65fc GIT binary patch literal 49081 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 T3O4XXNYA9j6~w-Z7zO**Note:** +This setting was introduced with version 8.8 of GitLab + +Administrators are able to enable or disable Sign In via some OmniAuth providers. + +>**Note:** +By default Sign In is enabled via all the OAuth Providers that have been configured in config/gitlab.yml. + +In order to enable/disable an OmniAuth provider go to Admin Area -> Settings-> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. + +![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png) From 2a99c51590da3c430b27538a1f570d34209d51c7 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:39:27 +0300 Subject: [PATCH 142/449] corrections to the Enable or disable Sign In with OmniAuth --- doc/integration/omniauth.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index f0d7394005..f3e891fa45 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -194,14 +194,15 @@ While we can't officially support every possible authentication mechanism out th we'd like to at least help those with specific needs. ## Enable or disable Sign In with an OmniAuth provider without disabling import sources + >**Note:** -This setting was introduced with version 8.8 of GitLab +This setting was introduced with version 8.8 of GitLab. Administrators are able to enable or disable Sign In via some OmniAuth providers. >**Note:** By default Sign In is enabled via all the OAuth Providers that have been configured in config/gitlab.yml. -In order to enable/disable an OmniAuth provider go to Admin Area -> Settings-> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. +In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. ![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png) From 4372043392d735db506f1e696c213c3ece47bc1a Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:40:20 +0300 Subject: [PATCH 143/449] credits for !4034 --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8d70ef9944..7cd8f63321 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,7 @@ v 8.8.0 (unreleased) - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 + - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034. (Andrei Gliga) v 8.7.5 - Fix relative links in wiki pages. !4050 @@ -55,8 +56,6 @@ v 8.7.4 - Running rake gitlab:db:drop_tables now drops tables with cascade !4020 - Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100 - Use a case-insensitive comparison in sanitizing URI schemes - - Merge request widget displays TeamCity build state and code coverage correctly again. - - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034 v 8.7.3 - Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented From 9bfa74fcfe79c244fe4fd7d074b87bb22ba7971f Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:43:54 +0300 Subject: [PATCH 144/449] enabled_oauth_sign_in_sources param doesn't need permit --- app/controllers/admin/application_settings_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index d7a052700a..a2dfed0b7a 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -104,7 +104,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_packet_size, restricted_visibility_levels: [], import_sources: [], - enabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [] ) end From ab7f9c9ae1281857bef655a1af64774c628befa1 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:45:39 +0300 Subject: [PATCH 145/449] Array(enabled_oauth_sign_in_sources) instead of the if --- app/controllers/admin/application_settings_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index a2dfed0b7a..c300560236 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -57,7 +57,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController params[:application_setting][:disabled_oauth_sign_in_sources] = AuthHelper.button_based_providers.map(&:to_s) - - (enabled_oauth_sign_in_sources.nil? ? [] : enabled_oauth_sign_in_sources) + Array(enabled_oauth_sign_in_sources) params[:application_setting].delete(:enabled_oauth_sign_in_sources) From 25ff3fd5bdd8645f45254aa52397f8d6b49386b5 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:46:40 +0300 Subject: [PATCH 146/449] button_based_providers.any? instead of !button_based_providers.empty? --- app/views/admin/application_settings/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index ecd032acfd..909339f7bc 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -109,7 +109,7 @@ = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled - - if omniauth_enabled? && !button_based_providers.empty? + - if omniauth_enabled? && button_based_providers.any? .form-group = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 From e87c96eef6d8d837bf4475e2681f4c5c5e40488d Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:48:08 +0300 Subject: [PATCH 147/449] enabled_button_based_providers.any? instead of ! empty? for button_based_providers_enabled? --- app/helpers/auth_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 3e1f420925..b05fa0a14d 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -45,7 +45,7 @@ module AuthHelper end def button_based_providers_enabled? - !enabled_button_based_providers.empty? + enabled_button_based_providers.any? end def provider_image_tag(provider, size = 64) From 46064cb84c579bd1657baffe24f5a515759bff67 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:50:03 +0300 Subject: [PATCH 148/449] << instead of += --- app/helpers/application_settings_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index ced8e16de7..878b1b254c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -65,7 +65,7 @@ module ApplicationSettingsHelper button_based_providers.map do |source| disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) css_class = 'btn' - css_class += ' active' unless disabled + css_class << ' active' unless disabled checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' label_tag(checkbox_name, class: css_class) do From 6bd8d3ad48b9151c0bcb69bfbce0bc2551cdf184 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Tue, 10 May 2016 18:53:02 +0300 Subject: [PATCH 149/449] inline btn-group{ data: { toggle: 'buttons' } } --- app/views/admin/application_settings/_form.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 909339f7bc..576509e3f2 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -113,8 +113,7 @@ .form-group = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } + .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes('oauth-providers-help').each do |source| = source .form-group From 9079b56bf4f149054e347aa09ad151ab617ef123 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 11 May 2016 10:35:18 +0300 Subject: [PATCH 150/449] Revert "no need to show the help about Github or the other authentication sources" This reverts commit 78a832d0fd7ff445c707809134dbb2a42d2a9e03. Show help only for the enabled OAuth Providers on oauth-providers-help block aria: { describedby: help_block_id } instead of 'aria-describedby' => help_block_id provider and not p a more simple approach to the OmniAuth helper links for oauth-providers-help --- app/helpers/application_settings_helper.rb | 9 ++++++++- app/views/admin/application_settings/_form.html.haml | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 878b1b254c..82056a4c97 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -71,8 +71,15 @@ module ApplicationSettingsHelper label_tag(checkbox_name, class: css_class) do check_box_tag(checkbox_name, source, !disabled, autocomplete: 'off', - 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) + aria: { describedby: help_block_id }) + Gitlab::OAuth::Provider.label_for(source) end end end + + def oauth_providers_with_help_links + button_based_providers.map do |provider| + Gitlab::OAuth::Provider.label_for(provider) + ' ' + + link_to("(?)", help_page_path("integration", provider)) + end + end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 576509e3f2..f3681bc96e 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -116,6 +116,9 @@ .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes('oauth-providers-help').each do |source| = source + %span.help-block#oauth-providers-help + Enabled OmniAuth must be configured for + = oauth_providers_with_help_links.to_sentence.html_safe .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From 2576a55e35f06d2d59783867be5e7e2ccbed982e Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 11 May 2016 19:52:06 +0300 Subject: [PATCH 151/449] config/gitlab.yml between `` --- doc/integration/omniauth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index f3e891fa45..820f40f81a 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -201,7 +201,7 @@ This setting was introduced with version 8.8 of GitLab. Administrators are able to enable or disable Sign In via some OmniAuth providers. >**Note:** -By default Sign In is enabled via all the OAuth Providers that have been configured in config/gitlab.yml. +By default Sign In is enabled via all the OAuth Providers that have been configured in `config/gitlab.yml`. In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. From c0c2cd4ed72cda88e182f3eafb472687d9d075f1 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 11 May 2016 19:55:24 +0300 Subject: [PATCH 152/449] new screenshot for the docs on how to enabled/disable OmniAuth Sign In --- .../img/enabled-oauth-sign-in-sources.png | Bin 49081 -> 92674 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png index 95f8bbdcd2489c4eb14c100c92dffadf3a1a65fc..08155a4fade75af85f36fdbe8cacff1af936280c 100644 GIT binary patch literal 92674 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 n{ErgGcytqE7Zr6^h`m+mD8d?#n7A>v#VTY~gf5 literal 49081 zcmeEtg;!K<*Ec0dE2%V+N=kP~cXvv6cZU+v-67r0&?QnbFcL#|cL)p(-*9_B&+Yr( zf5Erb%UUyYI9Hrq*WSOquj_;>%1fZ55TL-oz@SS>iYmjvAaKFJz@|S(gx=Y^I~#$4 zL3?E>BBCfIB0{F8$L>MveWTDuocd)U&OfSTdZkVwX;?pSK3$4T;`b*Qhb$0WkTD02&RvdI8f+twTl+6Y%rd1tKdd0@fX#ks3MWbC@{# zo?Vs-s-9978-~X4D9Gm-Oc<7=KCB}6T(WQwy-9H|7YbZhSY7FjhDnC0kqHc}OCC~P$%oL=+I{cL$vQ^0MtlX68_@d;_4BJK zEYq;MwRVXb1{vCm&kzfQYBDZ2gkluue2KOE&vyr5HE~=rtj#_7&B+7Gl(D2#edq0`X-wwrE14oK_s}?@Wo9F;Yl5zo7@057T&kZ&HmVW;cof zJJrMIo4WQxw(gRuLY6!Jdz`5p30{Ee9FYe|Wg+!uRYk(Q<%RFVnN zc+&iJ-6~^*!N4wbrsq8RWPy6{l+F>Z(@{5eUYO-&k30L%QDdlpV&1+hIq@HuQ886IWeZq43nGNig};3499})B%rHxVv=;q`wHRKN->Ds);o! z8N0mMi}cF>YC+F~E+=vJ^GySSUZDC{*B!eCWd3k{(dvAo^JhF^u(Bk^hF+;uTanjB zahfd2A;`7Yny=Nv!5_VA)vv0mjf^TtZ^N)1c}Bl8tlpq~TX*2d4h0RQtb?!Q+mVmE ztJg43VejYy#ecrhe4&fn^GWmr&qwO7iHcG)!d$Xk=$GBF#8l06{6X z>Wa^duZ^$9lCkVmjv9Yim?0hf!^MV#m4l_5sHM2ErzHT~PDo-a5ErvSrV7?{>1f+<)BA>dM;0vSP|&s<}Gc zI^9}+Dzj9p+*7;WzkSc6>Ga^#bR9FYEB~`qnzfkq$k<$wtCY2(js&lwPHunjf#2YsAHCZUTak-qk?*&d>w6XU4k*t@pi{Yk=c&as#Y zrm5v(|K$Lq6`NSInyK7H!*0cC<^_kUkNK;_6x$mr zw6c)3aNWsgK4bo0{N<(n1w`S=KZT>$&UQ^ojan(u}WM!R+gXDCAV+rW!xY)hWxP z*;5HJTi?3|91iP){CZB;DmKp@7h7-_%q@>BaV@i6&z!3NQ4`APj1aSKmNy;IvWyddR3E)cuD-ckUwYBL4tP)_o) zaxO{?Pl{+!UXCaxDnE)fiXbpD$I~JC7yUzpO#O&CQ^tJ-ZSGDA_Z1N&6Vl@4j6dSb z1@c~+x*oinygo!l!%L*!(RIyoUWHgnL}dismt{tI%?0f|y}Fu>Q-wf5_T+bo zC^GhpjyiRoZ#xs48l2M>>(GZglO2CBChDYUB>U@+>ieylXXKRok?6+(@|+9Tf6S?U zSG!Z=RQIl0()y~aqh$2y8U9UarchJ@P5^-Q2oJyvVy)E2}9>Mh_uWx4qKM zQu6KSFE(2iuZ$Yz=v@9jJE^j-*5+Oh+4?n-12_RJ%n>vs<$h|_t0xciiDPe-_mA0JpaSXs;zHd>of%+XpM%=5dtPm^@G$(`Wl2Oll!K&*l+pS_x)~=rn>kyAPP}$&%qJpCdsl1zy9}`X zg~Ubl6p;!q;1}1X$I$62wipfKJc8{#&4hx0UCx-_rRrL#HE{wjk3wj+fIIaHu(W>3 zTp`n0(EYr9|C$0nlZnm1rG3U_Xmxyb7$XJKoyUzs&#`rUd~+N;$6tEu5n~P-D)-X> z>7sVJoqB!0I3jf9v|G&dQt^p!-oHLPn6EyT5y}#;9xTDGOmdPH^Ru`D?ojO^7T#t1IbXEx#-HqLTI58^eI#<{f zLl>%xQpt~xM_me&m-j{lC}?fa-Me{1oww#k!A^I-zT2z z&i({~3P5TGz|ICg^m{LRE^`mfZmhZj30O1^HU{*7=?j>j@hh{iOY5{Kb+;+1FJ`lp zzI6M4tK&^qT^TA|LJesC4D+dnRhq#@{DsP@iql>61zbtiq9$S79UHHX7X7>Sz83;$ zm|bIt0h1`*_^T%;KZ{mZndZr7=V2N)hmL%)$iJ~Z;0-OJ=yGUgKKG0DhxN#?(Z1RH z!3<3mkR2qooMB+_sGdHsQp%Jk&@3X`QdQGMQ%;u0*xr`$qlvwdDWiw21GF~`44(%N z^wQSUCj|cyozk2XMub*x+y&?Oni;Fe?8%;SyG7)T5u!o|gbhl$DE-JQ{$mC@eGoasF`H#ZYA3lj?q1GEQ&v!|WQ zM-K)&XNupG{AV6fQ)go*O9vNAdpokHc|RK2ySnhddGqw3|Ni}^)6~QApC{Ql|IHSZ zL8hk`ruU4@O#hu5I+X9}E{~$6hpCOGsHLr`oip?q0`Hl*nEC!1@UNzS9{I;mt$&8H zz5gF0|7iJpBp=fg2Y+z%TU>wLh003+g^%gKvKK(Xs`q#YRR@u!sDdi=7w$U|#5BuH4~dq3!sVb{(U}`a7k` z8paIcSzSj8ySYYgRYSth=byg4&DBEo$~Q}Dh`?%Z7r8MA%@3N;kOM(>35Rh|w(v$V96Lia$vyDEmEeJaE zukxC|3nN8%Ap`^aKfVm%P<6!ujNvXx)Fl2_6HH(Y3@io-!vE<1)F<@>4uOl>2Y(#q zUrl7)J*)vRaL>v9#}~GK0L;Lr;|~P?b~k?a^c=am{)C0U=_XBVg2WsCLIdcfH*c%fF_RFvC> znroJsOIFA5ji4ZQ%aNr!4*s9q3Vng2z*>2J;l96v`55YVm7mY!y>vS&;9n}xnXB!- zbo~*R3e^pPx6w^1;$RiM{QKITnXR_=mY#<|CvRLCgSLfYl3P(s; zT=j>MWvzS`_e6@x%4Bas@NR2oU9BG7)|KAz)@e2N>=2Md)81^CFU9f_SHseHpCu{M ziJol23d7Ym*Ew`g8>3(TOpSfF)U@8XE9OTJm#1F{7~-gWskDoaBKC)c27W|E%E(0K|m04)8?6|G5NS;luT!I-C=0i1eCWw4^I4&6gB{oj2T{ z(c_@(0K4v(_-j2=)^)u7H?3^7-g9y$ziy_#Vh<0H2*@ug6p1kNG>>bhxtl#DvP75$tE)kY*Xg=dI;smY<3pDXZ94rw0qmsZ(oLA}r z>%h(ntGRt$Lq*f%Sb9PyWT~!bTYt?MB97_gDXaOAHkxzJWA1CwNU?lAI3n=Kd;^Q!VB;{D)vq)>X zz0Ie|>4{2wgClUu!x;U7##{F4VFYx3rn<^)M z(hjCMULjGK3qQ$HSNf)T2S3wpU-t4&kmp11Ayd1&Skv-Xtop2DMZtV9{B-s3nfBI>nReWLsb6q<#Bu*Z=eq@P zj=+1BBU9SdJi}V_ZEd|jW}CL>xAx_!yPu!4VtD3!ugY#aQ8ydc9ejWDHJeo9J<56J zRA6ye`900r@k`!mgQC8iTWf|)n887Y^?yW9~7Q1TEviaQ$uZjOyN<>&shF2oOa9N%|AwWr|rZRvyLh=qS?oH;c zj^4HPS^H25T*4ZM*DM;&xtRA(ai$3fb04D|m^Q7CoEOV}q`IY`Uv~wl3?KwvoTFRmWBO(lho8-Tnm__be2M zzX2gi>Z+B^>FBXreg20KGO?jT=miovY@$6czoG-*RN|&{!wF+t3Xxy0P!+7lRE}@u zlfY}%Us#+*sklOHtVD)X;fRu4FUA8af3@m;E6$$*3eB>eqa}Wtrnw>`rnzj?N%x*mvR7Y9Q5Q67|i4%{m50~YwI$gJ{W|6ixLZ5J5j|c%jzt}&=%u(I1 z!mc`dr)nT5vkfxfI5tQ=2ESo6-W3oUK zC65JcZly}w#d5D|`(ivBjRsG2l*L(5G-I?-X`yX(RZWc zcpRO2;6uq{Mf9Qn2jZhmBIF?Dj)A#`y_eI@s=8mnm<#2Y$-tlNDDQq=mOU&hJced3 zc(Vb{zZ-xpk{w6FRr|Gz#sCt&)7ls4d$ZWZvOC?^DoR?G0x11UXo%I|cB&kAApPq) zg%OALJg?3r_i_a$!83)u)+%j=VZJ(Zn0ea}cEclV?^9fUnWfnq6Q`4S^{y*MXH?FLcjvpSgH9j6j@c3DD zU5;n`@;cP#6kG*NIc6*6>{y5R8&mu`5ZnJo_XnKhre&^x3Ehmndj#I9HF|^#xNe8C z8@~#_S^FW+<*^Hf+^X-^_%=pXe;{nCR+19-vA@V*5Kn=Aj0LwA0L4=!v=H$-VO>3& zV%4dp62Axf>3{**)#asvkAe9{&-nKC^WguzY$9ZiXUA1baWln?DL?XcDVW{IY zji=v@!tu{vQ^Ano-T^f_PODiyo#?2U*Rb`8n;B+rhn>r8cYiL{w@*sc97^rksg)jI z8C%Wxu&K8V3a@QS1|AvTR7b^pXP_wK;Fe9s*kE?VO+H`#4t(qvU!Qeu3Rr&mPa=>y z1J{e;pt&4IV(BfU6DldHsI8RmeSBT;<4Z-gy~?eW=gQ{$Mc)#Wgn@a2hU@Pumz$;$ zI^cF@#})D_##L&Za=-OuE5GpR+!3Cy61JBWaqHd>2&{0**rx(?Rd!N{o{rlg5kO^L zDAr)7ZW_wdf*1ysk4({LlKN{b?{X1k;J)O~pFCehlpys&==`zQQ12-`PAs~>D&m7% zeleNeGB%$&eMS7KEQ7b9fqu-{u#Ii?V>bfV9uX{tdsUQZiq~c7&}*f|0y&uxr&w~o zwJL?HyYn<5qhQupiyu{1Hq&+F@xur=@agKQgXReH*)>OaAF4@M1Apd2gi4U5Dwm9Y z#c|QtaDV^6FVSE#qP~4*2B$qQD>S@sP|pQ&u%x))C||nUz|o#&za|r zoIP7iB^O-J6+$4}5ALHx*XtNkFI@Ck1i`8WlLS$uXrVVc|>n!*(YD$@CMI4oYX|U%5M6L#nh-p*0>jOk8r1} zfkDz-j`kLR{&%(NX7e{aXV}cyt{4hK2A0VS6|Xd%*57en7(nd~dCv~qXAP0kS`=qz zI!X6jtGG||-3K>Jr%e|010}UK-cwMGI*a}`IY83#AGuuW&QqrHW&{(OCWS8Fdmr)~ z$a}2xtTRpTxt6nWt{x!8^mqjI$mj-8eAdG99he8Iy6On^Q(_)455Z&m4iIr#yuX~W z#0u%}Zzqv!SlZ(rFJjlRy_EVHf{;>t?xwpKzd&)cU<2r-=#EnkUYCETAv3_!GLTL~ zYW-7O2T#-8X|&nM23i-o!pVRcpwKu-lQL$9Zq%4`^CrDr3DN{ZM9l#Sc!NqaMdRoGfBcX8>mmps}6 zxs8$L6({Rf($*?Q+Q+B$dn0%EEAdK~0D{L8(>B5hi2~RkEDp)EVK$b$XLK(0%(hnF z%l_=FmgqK`_L69$_`P?pm8%s2qv_rff>*XMREXi%aQ17~4a4*1>kY@|+0BgcekV97WpO^-Rce=in- zmGlX|R5NZhxLN4Sfa}+#iJN0VU;QcNNE5-Sx+q=&uVOQN?(MXKJ(Hv?s2Exc#xZWM z-x8a@SgA>_^8xHBuutR|Vnj*k+K}nF3ywHfozWu&Mi5TOZy5*y0La2`?`= zsXm3pICCqJ;7y+%w0RM~F;M+uPVDBKW-CpLSW2F72qtuZHU41qEk8C%Ham6gUS1?t zxwthuA}mJr4b9-!mz7Dk?0!hevX6^SOXQVZwJ&`knHMXZfAB=z+D(hZmB&Y(#SPGr ziKWG?j=)i0Ga>t7nU5V=tKE8L9xeG_g^~2~fkyTGhgb>NtC&F90Ozjj&kw6ZS6Y-J zBTF@9oaw>IhL_}(b_YM7<$zk%;z{dG)~2-iTUYHQSSEij-Y=1A0Tg4ov)1?#1@~L; zbCYI!iq`-LGTH}GuEaSti;F5~%PWQ^Bozd2i^JEH>-7r_7Krd&5hda9PA3Je+%%ATNOGZ&cstM*tsd)KnSMDR ztg%kj{a$l@mESvf+WFHzL0E|Kd8B7R{`R1V+3lnXN844;6ZtyG9W?G1;Qp&kCV+}f zsDK){dZO-=45CeOdauhqR)?1=9Q@cfB>jm}9iHtud)9Y{l62i4w{|lB0(FOA<+Utk z+3cj-sln`4ibdGUb@AToKFV2s$YihAMh%@*=zvY(OF5Q3Ex$Fy0*LH)?;AQoaOXE` z4(T;-Pk$EZ%JH0B1{PUfX3DMW4JXWx0D41hRleVW=cYPUKUYntoVnwG2~XqhsNE;N)aDoCO1=FTaKYIWizH5-l7@`ZTxr2fJuEP@Lj&oL(91 zPFJbEZvK$)upAS;x4l@s^X)TgI?sjpqOx)sCNQg5@LDnr%N8oR!4)MQ*VpY>9m)9K0E?C1*quyGb^Fxxx$*$?zmO zc*&Gh4!+A^WlfUeCjnXI!GJt&2xSguWx58M;r=kfQ+g<-m)Op}u$q1J2HW2yZ>3N! zzIju1_yjj+vv(%eb+#vwP>y=0CDGdv%d@=n5UXpW#m{xL%*Se%$DefAZ^x{Cc=C#k zT0ak~RIcPU;K+8tv^rg@R5{-^1Fz3Ffuw8yGn?x5XpqEC(?f!lX6G0{m&y+Sbr<{wXtnx*H=FL0Y8|KO9m1Pe^A%OxkN9iTn|aF7=9qJoYoJ zqOzzgLef}vkn#iv!^NO%W|?`#Nno(jb<>+dbC4dk-yhXs*4pDUnN#9|8)J?^I+Y>dgxAW1Pb?w9m6Ak8-5l zm-z?cPS}q`_oJ4KTkXLV1er3*Wyx8DY{RDkt=S)))2e--kv^mNEA=b1CWU-gxoa;F z5vKsy)<+k0&FrVB45`QAvHJEtXGz8Pj@;fGpNcuVY@~#NzH7e3bmBw~%0*!*-vRK? znpwPD7B%_02!$Wn@N8o#s*jbguFaXW;r0JWGX~KE8{mk9&lqm7E}M3eMI1k?PcN5a zg3{v*74g_~td77UW-=o^J^Ba^j^$cr%oFd|WQ(T3jSVyv|dH zjvXhx!XoIv=Y?HU2d|HuD{eIfg^9g({9I>J+&qBlb)$S8zLnaTIgJ=deqX>A0H*A9 zT~@T ze2L{P_Yv>T214U248Mh_5w2t5dm~OF3Qe3wLIN++wF8V@pd|q~Eje}Y@SX6$_4>7Fv^zkZSKa*f_Gc~N~+I~3|TMLf$qeT_|dE90B{VlwbT zreQc~xE5M>bN4&%Xm#A9)a&$_)faK*8`{`N&3ijhydU!IgC3zEz%et&OOGg}kjuhR zKjFy!FCTDtA!&PJ+V$r1`YIxq?3mu|RJmt$eQ!j0Mx&)=+J65%wf(@a=62#Ksluej z)=Q>$7onW^d+F9o%FHLN`d$wjH@h)F_3C0olB4ELCSlfKlg6PZu3f)RNha+0ug6t0(8%$5-!^8KiVx_{Gv<>B~EU`_zW6TN+((ns# zDO1%|FYLOpiP{=HFO%|etzCBC5stV&SS2oAPlV#oIcQ*nRt^T8ufrASn)nKv6H$6t zt=hVVYV*Hz*oqKqaJpMqYPp1nSG!f{C2=+5GXl*Yre9BnEm91PBc@T2?T>H_jMm5< z{OAc@>oY^1`Zj}A@#LYp8>)|aOE1s~L#xcs5u-Kg8}~-#-Z_KTUJV1xfAL+>H98KX zNU)eloB^3Ey)kf@&zEOt1-y^MPk8?fGn$64V523iy@BUv_8Gg|y~xgq>XnlzDI+ph zau(asjaIb^*zZ+tn;Hjb5+Yl3mi%(k)^ZFDU-MgrO&hUhK#KuWal2DgUWC=cGIboH z)iT@cS*?Z6RUH`l7%Qq=tOY^g!nH9mQ5XEW?pMC2&*B{(xZy=sQ@HtvR zbw}kF>1q}^&M%a=zgAMG42&UYP;q-}VkH1r|oP)RNTNKj>XGh@8ynb6jk$2$<67O!q10y%5HE&Jn#j(R?2SRar*)UDo75!5Sb253 zI%aj7PL(W^V=?CNL}L8h^2`puw?ey6!@5;iQdP>qnEOcXGvcyP4M3;&&_7lyfifS57isztRDsp?Nd+=%Zdbc0no) z7`LTyt?>pRpG?o)NG6UJy@7S%8x&`2POSOctl{S8pRBPCz^B22KQH|pW}J7yhPJ+C zC6&|LN7;l;6T>Uq!4e(CmuZ=}-dR!%NP-*^o75?Nt(uGxPx2aEHL%~DysXxCcuZyWoPI$dk$eK`bwb4$}@zn8|y&_|Y?YFqZr(!aV)VcQMSQPp)Rx1(cZ z;YL9Ok)~1TtaSv!S*OpP56u@&>kAg6;(v&q$HBp&by;=yTH1KV-MYN-%+bfnPl^q- z+n1zEK{HS>S7DKfuR1$LU&T=SYtgeC+*CF>PW+UeXm{LF=1d6+@K%}+VF$n0uGrZ! z-Tk}vL+UKfP5I7UYTTp+yX@Q7A{Ynj$2h8>G=qI$J=3o8s$9Cy1AD#mz{tBa&roSI z$}~7EgP*DG*gN}Us33Ptz@u9EeNksq*Zd@BYda0NEYf%0_`-kW++daM;u1;K04kr1?#qMk+7b`0ogSd1fcz`qITL75e zg}EMvAhRAOL=z<;io;3a4;%a~+gI9#YfxwG^bIc|wU%{G+`n;XqL!_&5N1p!$Q)uJ zHSw@hZKjj5(ISATu<=K&{%=UtpV0js-2s885l}@10Dk{5QMn>Kv$qe%;Y|H(d@GdxPuI{nI3$ySa&Kx@ao6@%W@9hV_M}K`_wAI^Q+8aOkHfH9w zN-J%S5q7vm^v6d0?@er>DFkY}01;tgE3Ld}O{{2G5rZ@fb=u{}lfJbJgF}{T*5l1(>xAL8>|TPiD`*`W7a6tps*WW#(D# z8US(s2p7M#&TNN3?H6DiP}j8I?B}u*=I4~hI;4v}w9N#*r4uOoA$W^D!gv~!5uN+y z-(!6OGm#x;kb?sKIv#E^T3^K%$;L6J2ftM*vc5t8obJXTC3>h%^v6E`U&4w-h2ws` zGWa?AEBe2tjBSO1jh?LJ!u|(k|Hn8=p)HPc?^RL%)ufD1)*aW1YyK~-baUa7b-$PA z4Y4Bs=d8is3fqHwi^QexcfC0&vnl&8gAg)Ad-mKRlANRI!+(VPANLVB_EM6&`#n!n zvFg8vkvYS{5v36-!~I*YS^+Q;$-Dvc|63dZLR-*7DJ+}?|30oVSO?{q-W{gt4@mx- zoIoIyJQF)fx_=vv+nEXUR3w$&GzgcAz3ZQTnk^ZMp|MyiO{SyUFNDkirlH8y!R6JWD ziT_(qFmPIUPzp9NRv`aowFN33Ao^R_e^FgW52aw3xcl%A6#tus|5Fgu7)<_8K`{Tf zf}ra*_WxE8EW-c)E-ugq$73^!?j{GMKlbQ*`o(XNro&OH^_^H)(}?G^Lh{2;Cf{>N z`9-+9L8VSr7f)X~H~N2lCYc7*k#AUm#u=eb*)S6&^Qa|{#{VH%9@1VpG%cx-F&J`T zz$2(3o7j7EJgzgInOA<^f~lWBD~7;5{(eLEKdzi?8UZ?wcFZ`*(>z3|nyj20M7}b` z27WpTD9SmFaV1hyHqpc6mXJyfOW#fQbu(%SgUi?Vkf8HX_mRZMInl=L`@5IVEtYSP z8ZJ#I*3R5ZTJp6_C&sOPbFFirXV`w2GqyDri4o$BNLL4-fsW>;^R_xb8OnC5zB=Vw z_@{>El0P>8iW2!`LIK7&if^%=xa`)+T{kD^_xtswNM2g7`4lp*&eePE3|7+^^1A$H zy8FPbQGO&sZN-u&D16~^|DnqV;@NTM)t$&E#o1mj>E6B_!&Yt*(thOj*O5I}jLAIR z%C>3CqUmr7Z3PsJ@>1gjvtr_v8Dlu2+ujlSD!ZwoLQ~_pjVk_q zaQkD+34eJ;lclBBYU)x>#&1v(`vn@sR1>+7o@g}`ML2qVpt}FXUP?9src`(6eaOL` zX};N$B%gBJ`Es6PA5YRXOu7B)Yfk5=$c>py=r?2KY8Kx@?Yk9tZLYRKNU1NGbq9ZY zPer8=hVzGR;0iNrbO$_av>En_q(34OY;>jvjs?O~%zHJBKxO{WbQ@<^U1`*RT8>ORA?Q3jv<6 zLmN`F;FAxMfFt!fkFQcNt|8S+HhRWsm*uC$i>GIgm7rf}uHRXJ`*>f6f`Vf5jH@Dt zGiV${@+uHU;?c5FZ;6M?xmTm873pX!blEop<%9iR(ss_+SAHGWH$08K8ifoB3=@%> zf+4`V981d zgQw~{bpZOz3QQacM!SdAZU!4luP5Q$BRqUnxHS(MNJ3yAbXLC!#Gg0l$psbv1o#*rOFQ;I%)Aotyi zS(-{YYG*{doG|DrMe3Zc-bX=Yw;V5+L5q$^{b;JkvAN(fK)T;+Dwd6`sN((W1IjrP@DBSdq2Qsm)gu%87C8Av z8}yLPdUMIj^eSUGovl)_k5w*j>W#mZ+Alu3g3hDu^Kye6wJuAKW?;OavSs*k+abX3 zE%RlJ%dimEEWSRw8zRU(;h}l0_nq{NIW%YYzI54#9&lVId2Yb_lxRQnaHqf2E63qy zzuQA`IAG)El1(Wx#@dmlL6S)40JI|BdY?!53+8LcwV{>SF=m%uj=WQj(eeUw)trB! z%;#Xn_$}O|rvw2^tcv)gMcY7Ccp^yQ+v8?PotrV$#)tqr_#_;Xs(v-)DK(tM-q13PLk@aYiOzx(D5( z8#Ds=8&yBEAj=X0<%xW^MEon=0RA5y8}2jayk_zf#|dmLwF+JzX@++!APhi=-h}Uw z9th-8MYVmS`krH4U0gcAX||8F>?awT!%ONvuqNH;qc#8<3-89 znW}Epczv;vIszAB&hzNMa)bU9s3=dtq?3P%(h}A^fP!G~#H!&U?V|FgHz&0qW+W3! zU~?bIV64;MTV`6%zk^yXn`mCD@5Z43jtCN1qh{x6o6zZTX>+UvJB3`8^A#~Ku*%o0 z4b%}`e&^S9bJH%qzs05#VC+DHk@Mw#+m)k-YW16(!`3caFzb{n zW?}Gw9P$%9D!BG zqd671?|MR<(jKS{zSVx)p8c@(gC4Kxm#UU60;DO)XIwaY>hm2@JRg}B?XjZ#j_Pl& zGSdjGh_Zdi=E?_-vc+s_pPn*tU}(h%}kflbdKlc!waMOgROi`h2t1QipnY5 z58m)d0yKaKhF0oUV*|_8yI;2(FXtya_DCj+w>ouH7(2_> z1wB3x1JY<7_RJcY7@RqP`%M8-Fgl#6FjE@p`L37#%JpVA1YL|14`!@%Ml0WbCCzo% z>C-ct9;axInE?iVAmQ!aH_1)Q3|y8Ku3H@BUy*$f3f1159J9<*-<4I}W**(0w8<@Z*vXP8SQ%CO`z!+aS!=jE zj4QQm$I$&fD@k;f-}dIEWZ&cS-&=(8*gC$3BT}b13w}ylnMt6YI&wh&tNc^kh(>9K zC(@|W9gD|ouLSvjFYfJXEwR6=9&X*+vo?7qScI!PMN-eUN!(fPx?9<#cE96aay-5= zo@hZvDa~#Ki=kpza~pePe{gbD#m`c6Kbhr>y`jy#4y0_sI*{STX}bN=;`k_4A3WSY zh{4!r_WDOrTLM~qgxc~=Q@Xvuu%X%OhEy%Yu|S9eVjlGU3);%5ekLZhaNiHXmR5i5 z1CmCFr@ltrCt}6RySMV^Z|M*w#FTg=)cMPcztgUBWPqn$VV;lxhr6#DtOKw(>ZV8Y z?$R{Bfu3fq#62^fztuj@;{KUeqzCWyKwxWaV3Lf8{_x35!Rk_hafeBORpBA1=dxD< zx{11+UI`D10$p0q&^)KM04|fC;nk007nl9x98i_m9BNcoo za?5PR!f?2o=B2Uj@h%F9xSsmR`uV{;g+HZ8^q4gs7NZ;(o2FEIJ0{^;!^(3WvGn$r zHuMD!*UQm}Vdpp-vb`%HYakSt9OxGrJ_3tx0Eg)Y8ziO~StcZ`!U2JnWWBG$qHO~5 zHRY(kZrMx=3DCdVW9dmX(_to_^@)wR}1-S#xc1 zCGCBC2}1a@xv( zQ2bc4QGDvmJ}LgnV0VAEvsjtp{Dk$!D47xXEK-VPP88aKas96%d*CS*6ifPCATr#Sw9kv08t6yDU34fQw0vWq*g)dnq zeYcWTa*j=`246QxZVl$iVWYg8P1jOe^pJTe7q(TaIm^s35u1!aG8+ALY?<6R*aS9O z5k$_Q!^&J<%R=bfc2Petxmjp=ACP3gtB?-z0whq%kt+N17=5LUuSoC|yv ziisxm(2h9}eCcbw(x|MHt^ai=n{Ws*Z;!v|Rvo&6BYW28oo=@s;SVMehr$u_AcyvP z=*;`HQXZ}|g@StB4d(I|Mm;O& z*Wf7M*3#*a5```D^f3KfD-uoR4V(EDZlb?|y)ATQ%^txXJyqg1;sW@)KeEqDQxdd$ zC~Yr!h+H@q@AUBIe++sqeJl7N9&^uDvX6?o?k4A0ALSN6;nrms18MXX@wW9&oI~1% z@~j>mMoca7y(dtONgx4uy2|b@5g!T3R-aGNcC{i)Gq$vi;8@_NbDLK+7oUO0fdpEN z!${Z^C&w*`QO(*s+cxa9aV$^AA8lxy_Ox(!^^3pa^!}xMdl3%DN$>`GDogoS?rcwA zvi-!uLh*Rr<$f^f$v%?V?aYq3+pX&Ds8u+W0Zu7(|6B8L_RJhno29~@EYAupws{~u zf%jBgk2My*MzFcV?=vn!kSB=Wg;@go1o1h4Lwxs5(H|J3|=SX(X zz1vT;71?zR#tPm}^wx7Ov=udvEj^bO^d^fR?!BKKe6;I~GcYI)972+|CR?<3gVPdd z#tvf_Vx((R%F3!eQ~*YPd1q^BfR}-bRqAh54}*m+DssPglj46I$Bx>6YMhL`+n<)s zN{6LeXOKht!jM#PASXeaUoIJ&*wZ2nfFbG{4uN zU^_W*MG``P`Wni5K==0-(B$I%O}O?`d77L25Fo2dXm== zTHZnTVl1U5mK?|-010Wi%53GBxg#y?`Dpi&)({{g)o@;0;#K(w28|3cZziSPm{4W> zfgN@mhg3kX0DQkW;L_J+@q>mRhIiXyJ^4WnRR%bIhH}=K-G`74^T$0krnJeDl$dYE z6!22|{a)rQtExdCANTL-t6}6*G=I<4Bm(uNzqD2!jt$0{3=&W(EIu6$pg3tFOE6^$ zl0Jaus3CK2RoHxQ&dF1XH2XfuPHqS;Q}X+1Y{6n!zy8)r<6rGsIjp*tZfjItcQEfX z2M-HfAgi{r$E@?I+Ur9`^}c%=f|Wtj`eVmTpH!JIcov0hY%MY`mW8JHJ5p3;Zr^%kwi_UhN)xQS0tzcl)Tu<6wU9>VY8};z zdLvP-BuT=l%Spyj@kqo1q>w45bbm$YWtlaEmOzG z%yP9$@|ZXmg4UiR3hl*aS3{aeWiY#7$urZ@b~1HG=uWGJ+P>j8nOh7pDWp8udcH!& zI=(&wQ!GQq2+D5R$}bRMr!_4Y2<$!-<3Mg?Os!^TD|%{uB#SR?6!#xSHIovMxMY#p zqi)9YxZ#Jn5E)sAU8XWjdBzMsR~t2EYqN5yo8`gZ9$;A`Wq@@$%UW0$0ud(!&0E(5;*_sNq?|)MSm_AR$GG`>pCYj0X=A|)U-1dHlldd)d#-=^ zZ}ADZGC?=^ab&9|h*-nG*Y&E)gh$}ga<-2o=Yqsvww#01 z)o+bS?cz>QZTFDVdVEvE_17RQ@gdP5mQ|LPjT_l)11&)J&92bog`BtNh0&sxUuaRf zQHAQOt^dc~R|dt^ZEYq5f)m_bgF|p9f#4e4-Q8U}xCeK4clY29!QI{6VYv z%)j|FRk!L?S9MpNv-e(m$+Mod&TcQs@X*2}(>P#KeF=xAX@-*-F@cPhsEz_Z#MWEK z6UWGbP6c#CjCZ3x%@~t#}eMre-9SsnS7NM61%IgKWFUc|2#05%66(p7I~j zHTFKVCMJJ8gI1_ay!A@wzi%CNBzjJlxbxeIm&xs^*CTM>2R0hj{OUKWdMA7brAP}5vHY8Hc#f+llDtzk==)0arp_Ywn*ztQ?ludi^uND%`imYXfwqkq$d9L z?<=j^+)%(A=~F8|9wXph` zs0$*wLZ5Ld^_3!M_Ic{waO?_ecyats!oY?z(KqnXA(@?*zj&S?WSHtS-VczcplUGXq00GKHroXQVfpbM{kR>X?f*~8#g3MEsMW;=H;;lJ&$KI>&lC}u18uE%QF}9==V?NF{W*$^_=<5$M}#b$*|WdJgF5uCbjnGQya9d8X_`KS^gMMX=};QC^t?+| zwGFgat=~!kqkEZd$*3o(e|iq))+9)=_jt>!yd0`O+S&?PeNLa3&3%ETzFHrTus`jv zjliznKFCtu8k%v+tzB?_J$`s`!iOJY4not#BV~c!zq@Il*y-~5Ra?p>`MN-itBBQA zQAt-_W@qJv>$y5yQCOmuKQ;!te@K3(M@wy6M{A4z8uC``>~ID}U3^yn=Ud@2R1Ltd zRj&Dbe5#-w7v?*fD%Z)iQkZOXm&!r*qr~GckIC*{*Pf-hTEGU)z1m{TDuMUZc=w2L z9r1dWgOCSqx^*cnS-Jcjh8m^PPdZO5WDIb0-vaZ$X!pnv(EdQ*~T2)p1|4RPF^*s8-a!YbE8 zie+)az`?#Gi<|uJfA-0Bh=4d9O3^`3HV2Fq5P&k^X+>XOCx}L`3bStBPp{(fb?F7d z_I37s#Zzt78I8!V7o32986@oVei)%6pi0R4ZBk|-A$&iB`maL$ zJvGpQcs-`Y{qG$DznFmKdD}c{6DSa%eADlLKK^sdePzUuFF`{MokVLAsa&$IwA|vSJ8!51P@;q} zMzxl00AFZfG54Ccs3WuA|M~dS7J8155LH#*z-0c%QA80eoqPwYDb2^EZ@z@G-!LWm z!M$!1IgEBVj7FctzeEq5r5^sYGaS9#_2eCk?f}*p8q@L9hhW_!-$DQS_luv6eB1|2 zm@SPbNeYaV|KF}Pa(;jhEC5`@``@mNMU;$=iR1iV9|+$g|NE8E(aG}f&jbE4ehe+M zws#}f{~k0S{#z33lM9EZ+4Qo_(suQ}fsbn!EE&QHUE^Q$KJ9%7wSRZ(o=MT>;wG&R zw})kDfll(@1{0A7LuE&HFyx>EO(0MXtnaXFGvBIy-FSI-ZxVKoa?t;VB5Kw_mZOGm zcEP(ck>_kqqm_X?npl_=P9^TYmV_Mk4kBDISvEWX^xH;)H+o~`lQvGc*A<5=g&#pJ zlCg*?vv4sirk(gF8tNl7wn$v~T9&&zrnQ~Ls`Wjb{)fBgmgYqQwJND2)f-etchXi( zJV4dpm#thOLqN4FM(~g$;rF8DFSWQsMms_?{E*^Gr&Pl8;c|Zl35T^b0MLoc))S7u z-WNx|?Gfq6_b|9Yj1jmHpiQ%f1&eKWTKCJIja=fgwGjwsxy8|ozz3eR`Jh65);*W@ z`6al3JoUl!tH!mP^Q(0J*#udB%@WQBtI_U2`W3*^x`58C+DMvhjAiMc(thEdI-_3} zN)>X)M=iZg&CjXA#7l80b8R)4&10t9)iSD$FnYx3>~$d>m5a+din3UWBLlGYmr|5U ztsYo|j$V6~#RcXccD8bz#G1{0{}H0Dq&JvwNl-N(XeSx~(A?*PEG=G0BEi;dLFASN zUd%e#M$*zG3$2HoXR|&4j66}>a4A<7`QxsLLF)Azwc`befzFcr^xs>IiGy;}^V7hC z^16zdF*g@aEWJW)8k?h`5Rlj%_rQb`p>QLGt$`{OoT!)nS7n2QfrD?1Pznoz6xc?5 z?GUHzYfWIFttIAfAvHCeHeN92wIPp+<8GFxTP@e7?+B7>;}nZo(Er-JRVC$Y&AL?<-EGvjt_0-ppHcn16h)H zbPF*2#OrF^1IZhH#YKh;Uy+_hpV&+;A4jklz~EMrkG}k4ri6$<^#Sys+5Mf86a|AK zcz6vI1cKz2v0bh1$QEw{(~w-_ky85FYw@ueGJBHX8v6n?nDFNLNP<%3EYnTQx00nh zQvXF`)al2xc;6_D#aQ{O$Htc$}+1sPvBQqdnbm;u3#8vXX^~h2FVGpWfO^!^^$~1mp7IR*B$f6(8?MO7 zl(Wiz6_B!y@MuHHN+GK9$(HBq(f;M{?;SaCz45RoLWUjpXX7>;I5KVzs0X83F)kAc zH$uGy?x7<$J$nfps^H}>o=rM@oEog7GJp5OlKenlO(CeQG+0#V zT6Ep-&73mSkb$n1Ha|=V*V$)1R~5y3nHIfb=;0nr}jme&pGircQ@8FJFyBSHD6A$5HmXtgWk zb!c2E+=Z5pV<$X2t7Yy{eA%e0sl2_K@4k^BaKGx6@EKPtR$WBe-6X`LK_IBLJpKAB zwPZw~)FjD+4WmF(BSx%%!S(;8!^V1?cr)@C(LRr(HFUB|cj42zTXFcwLIj85UF+l3 zd0I{u&k+qkz#*>I(T6Ks75b7oruv{{g-rP834?+yJFc;D#pPaZ0x?j!haz9&HmOEG z7Hn9mcPFMI1v9C(uH6)my6-NIE7!ta4vv^F=`lXX`bEJPGbnNl$k%d^QNt>YErg(^ z8UCoS9uZo~4T8LSb>fyznh$Q>|$I(9Z%uS zA_2-`pvKTnXL9pQx#Sq6%J*E`?h*x=;=Y~SkP0?T7;mg{^S#4EmOYHpYG?S z`2|ZLcOuT8s^#w47I*49Y+rZJ-Aj%HbMu2u- z_+h`HsOPUy-YVu`R(7&Rvr78LVsysyVHB^BrOf@`d8)mNybYHUzrb(T}Ot&)qL6&Gv1DI~{d zEfve;XdnDGVz@7E6gl1&yv{uStaiwCZuD|sk&xk=h#TL8w$g$L%W$-;Ze3N{za_rIj+O&(L+q zsb*;3&|>)NP^ZE2*{cB6pKAr*h$Z0}dG);(Y7pzjX!ZJDFNAXAYW7La)>t3xn1oDk z+4Zz{7%X~+TsNS{Y(_f;-!F5aB-S2vv`;!MMLW2rU|uf#Pbnu>pS-y;n_S&9COt;@ z>4Lw%?CNh{b+a@$41|(|kn28fD)07pI)+iii$0!L(p9lNY&IuSMRoK7_m<_~NMv!x zoU_^GADxId|7^qE0Kh#mlX(X0rAqSmdn!%~Iv+7FU zPtFw;;~uLp#eKa07k+CB1qNU`fw6E=AV^*7CDBG>2P!?i#Bmrs!X@tM!|zP$IrpO` zwY591zT3_mdh%wXXZ?2H`Pa5-N3-Csl}r!_rIPW2^5$+Q97m;9tB_GzHHbZ*mUrWK zi4&?#T@8~vuw@z*#~q&zz0-lcUI$P5}Fm6 zvm}}z^S5l9TdI;m?;TYwpHLtS6jzPLD1A@G2uhlW4t-$REJ8molt4x(t` zH^jn!O9l+#kE3|B6hJ5quoUi~vNq;m^}lTo9t^M&O4sBOoO>7d(nmI5W{dagv_N=! zxAM5h#C^Bnayt{_IWXX;X|F5^P+L#@2dIjH2oxEFnKZX zfH40J%9V{6Ir-6yo2~S-_4Kigu~WRS(<&YW0l*V^VT$e0&DCUJZ(_%Ua`6R!kct`# zz~a&(^IZ6{xpr6cPSSzGy`By@OXu7 zjNarfCa-u-vL>R~1!wH*U{AIRGc z3B;G@jGtB66-(7d5|MlW?fZl0B2(A?}Z&nIAMUeypE{!Bhg zNmhb|)OD;Mm**lhE50If#n?%}k|nK)HXDN#cM#!_vWnvewKh#A5ZJ# zl)Pv{T*-gxPm~Scp0sOSPRgbr#=!UqgS!%~nu9SPE?Z*NcNXyldMq(wN=NZg{v+;{ zqU2QJ%x%4j6?s9>x}_Y9|8AVxGLX&6c)(rv~QD1i}$3f{IE0S&m9346aKxl?*G# zqJ_Hlqr%Ebmd=xbc{JlJJo*UAzPq0ec*s9VGk-h?T#(JpQ-Wl$lmOm#vBUz$uqzy6 z?S@gtnQE!O_Q@Zx5J zACKl1aU9w|Zy7WBZRcJJ7Kshp#l=S^R|vkJ)rX~Oq=V+R0KsevETFg&U3jI`2OUDs zb%2!!Zm@T5NoE3EwKRNQ`#{M>FL>?ORxui15_sSl+7Sx_H4cIYEd)AL4Ej$f+?i-0 z7(<$FAC$J>VS8!FvLp@X0(ri+x@E8c4wXV|S)@5nJ8PP%mR z?E^39I)IFKjv!DZp_;mxt|cwBJbyT7RCb$-e$~_6|2T}d$@NG^hat6& zUi-zACQ&=qkCmA2TlKN?z;t54iwn8f$(j zSrzj90!$P2knYHIlsXauQ?)QD#=wqsN!|k|=JNb9*I+Sg014#wVjRh=Aq-B0_|K`} z)+WXs@Y2qiP^uiFZ|A~c!%&L#rk6irPOn9{i_Ua4z1)TWp@7b%fe_|t42hN$8%d+))YT}PeZ42${{8%B|=H(h8=w|jPt!_=ODtWFAGNN$MBg{_pe~7`LrKL^JyDi zOU9oCxSTB|K1=)q5!WQf`oxcB$ZGLA*T;eA z7CM8x7P{Y;dxkWsCQ$VJZZo0vA6EY59xy;;SPx3a$r}g}A_kFKG<-2s zo_ZO@-qaw3K^fY(=2vipRdh5C8FDYGG*@lhKT+xh&(HWJ%Qkazt9M+!9qV*)Q2|)h zFgA8#U!t&A`{D*$n~d{#Bs(`8k{L$rewQ{kxVkg^xRAZ@als^UVGf-ye?yBfi6vk( z?&e>F4B!MWVcFfl?Ny7QLgIZ;@*oN#(RIO2rQc!cw|a^JI0rQoiIN)I(= zJ~z&M3>`TF^qm~|F(%@nu!VZ!6y2;nZmXdMOgZa1jR?>C8~xgS{DW)yTf&YPLi%|# zIX+Xj{9L0=Be`v?3dV5VEreXJU~y6*TF7u;81OKN?Q~#5d2FNTvq+$UqwWxJ<`u_X zyxYYfq;IDnKGm$B z*a$kEKaFBpcKPSy9QrR|&}i9fDpAKy*g}vTdG5CNjZ41Wx7bEo>nn}jmGGGL77S7U z6MF4gfIe7*=fhF@Tj=EnQzh;5dfw+;c^Gm?iO~)mntjUV0MnVsV*Mym6c%{_f5)S> z7i6`vQr#7a z;S0VDM_iSG^$LT=jdzb!(_)Wgu8&MqRE#`a)3*$v&1*M( z9UnRToyJ1wbq~Li>2{jcThk{2<|o?$5@brEKtM8BhF#)ZfNFWcQ7zNx{>5ECg}vr} z_3TK~ES(bh`vq!R%8S91>%6)7v^{0eTwzQgo|cB6=qOx?`v$D@_G-GFaFX-Lj3w^9 z^4&5-1HGDTR||wXvGx-|v^Qh6Zpd(|(zMwuEu7_sShe$t=3H>r{{rN9(A|6>e8VSP^AE8A z3(bymVDqvm9Z9+S=AVsIslDy#WFp+@{$%~4ye3wi61(ok z;`M;>bi(Hu+}a06U-yo>p0?9kul;AavF>VPNDHQ+` z5D@`-Ul|<_xuk101=~sbf*x!+oI{OET=4klG!A)wFc%k~7Vm@x1^FHQi)99Z5GD`^ zak(+(db0@rdSaQ^CR~B$n+)CR5BGi$4Y^}zB54ZlS1U;Jg({L{ncJMA#shos5Li}! zQ4EM9C42{b$oq~ny?+pW;77oL$5?jBg?3;bPrlP|`U$~0QWi16Up5{jqz-s-Z?Hai zu4`QOzvIRu{n9foezC?SXn8f7Vq+e7U$Ib3kE_-HZ4@t?fr<@hUzVFTXQ+4NEiHW$ zo(N$u_-AWDB)Vi!;_z=*AUtqVmFvTrpc($#l?CDyP}i$W{ttH&0_K3|6bWba=r4cx zzuo+wVgE~J{}J+k>DGT#>VMkc|3d>p1)Jr2#l@gg@6H{PY}j~$D=jS@lzZc7fyK`8 zd?%}%ygY4fS8V^4BYt2NuW6OgnDH&4vIEfy_uzlrLF&3^30|gQ_WgQ5&RX-iq&ZQ_ z4dlKEX}}682l`t*z?mm}=-<>DFb70LNL({VL2rn_87y6VOKAF$g3NT2%9VmF(<+_x zs>8wEvvp+1f7R7hmb5#Jb@xEr?bN*S-coCk4c;T5fjE49sYUh;*v++aBo$hMtuyVazEhHJtCTvpL;17GXBI-%VVio&EH zEjt)5x?MImW@dZ$e)>K0Njb9{c3!idscDx1wdHtcSheV( zH;4bGk$P=j&12qJ_hYn@YL(#GXBU`Ix6K2(?sgHvaA-`k#Rk(n(_0=_^7X1}tOdnb z`iLfyaAE$*w>VG<3`rs-_gltq-^!p{t{6Ph{f zdwQK|#cXy+iAU4`GUw`MlxvosKgJy~A3T($G%K}+Es`yh7q3U{Tjr@NpG2D!@S0)&OA`<@wFy)B6vl~WwL&8gl4B+8_ z=f@;mwPHE5mI$0K6OnFk`CaK|wiT^*PrUfDnuf(@%F=Yv@d^}9;*$h45keu*%r(Pm z%QV6qqyQvBD8GTsHMj^rC{BO*8=wNQ-V3ZT;Hq_HDwHQN`giE4>lqSq9WcC~o}a{) z!{gn2i5^NL`3VUxa)Z(o%2BV$WsZqM2#_L19L1g;bYGY{I*9}mrT`c*y{#Tp*Vf_J zMf$6`qeR+JxJi18)%m-DG9!_-FB7gzxJ#q^iKX39-Usinv-};Wv)F3P1U41wIl`|{ zZ@CvsAxhuP^3xcFd3^syIF8LW(_A+LPZR+rOsfC4pwB4;jFWwLm8^fmG&ndMK)?

    xLrYfEm)%MW~Km71^$V$>A?2@HPU?nk`%QYTVA+MaP`u_}GV?8o#_E`BRXfNUaMF zmk=*LTX)ZIP2hrZU|=f*NoIb$4+um7?uda?_t!HE#OYOVFr~Eao?Y@YlO*huepI)6 zso^8=a_w1O8ezM3g2kP=-{pjgXB~1td*AF(7L2pdO0*@rwpXO0+YXco8!=EMY#SwZ z!yo~Te+SLi!F+$oaYA1(Q*C;GGHG@N=eNJjG;TX=I&-5N!I#e?z`2=yrPdBq@152N z0!|kLMP|0KvNx=_t}Tqu|CcZzJA@*183pEL%oTt3&Kl@%rTX^3R%jMYz{nzLk)y=P#98pvVX2$ zVJQjBjjz<#)_z4}2UeQ4)3hvJrdcvZ8XZBr9=zIq=LcGA{>zzM;DvkwLqSz-swmKeSs zMFA@V{y^S)(?8V}Aq6yhU1`E!9ht#6NfkQ4J!H9wtLnQ!lu6|+ZM&hZJ1?!XnI>kJ z2MC0$G3;oynqG(9vz09vxkvIVfn@Dy^(33kR^z6ZDz~pA^X3lVD~U_VrKN|} zRUb8U_fLM(Lv=Ke=IJ`lL4wrS8Dx-R|IHwwW-D_AaHLw2yDG)}H^>4DE4Q$c~z>Dp^r}@zbwAYQ{=P>3dky#Fi za2yi{Ymm@1Ks_W%WA7f{3KKYiz#+TSjzsYIfW0uS_{a|Sk!8T`3~EAV2lo2_oPnJP zB=|;ttf0;&{-3~#d1MAk@h9rHr|Xsn2;ynv9HxK?XO)46hX5ldK5))~eJcB;$tly# zGU|07DF3X`$_-Vz8(R9(a#fnm%gvof$~SX@8N?d&i-%PNg`-*(3f&)lNr`$-8lf3z z6u%VEhG#zp30 znTjJb-bZxSV;%r0B@baa)F)3`>JryT)ZnS}Lj*A4Nk2Q|mXA4~M)*}vMU3q&=9Pz? zX2UBET_fJ4-_)o(2GT5ipk@*6wB%OeW0W_1i}^!9aV@+veswn3n-Np(+6;x;yv$>O z7OOLz(gNk?p!@ho_lUPxVd$I5HdSa)VTY_=ZuQXm$qFMH&di9Q+de0b>G?`C#d&;F z#F}~R`ju1-0C=?JHU6 zcff?znCMa-p>{_#_@jYgSlY6E3Kebu8?cV`VPeSgu5|ZoFr<9r=Zks&@+1STSD?lK zVtVbyk1m56lU&zpA3g0rt&CDBW_3%)`uXD3cW@HN>Q+tnN6Ez2&YsAVwiN5@zeis= zLZ{9{ayurL?B5eir*`>;NE$m1v>%E-GpywiVZJDJt&7xZK(jktp|w2JRU&_3Fjzs= zpTmG}LRU?Vw{DzoMIHy9T~7W@lxBx(F9YRH=S_jY=szNa0Tt*3WH^ju3dtB5?p6YU zKic~KvLD@Ex*k2*uCq1)O2q7fYjc^fxCX@5-K;;eVTJ_P?qsq_!Gv>xO-oS@(*%7F zUawBLPiLn5GYI$Om(9{+UmcYXL}71elf%U7Wi7h|?^G}x*86s~DRiuNWEMr@!pEHW z1=AqQJ|3yPlE~xR(?c!(=J2aNn*zS4UkLSXlnQBKkmp-+azdcRAu;-*d2`5oz`-dX zC>MvpNkTb<%<#q`KU|l>nqBsJ=4*?~Mra^5K+~k3`srg^oEbWOlG%5tIU^BUN+j@={_{@1lQv#+y-hlX)n0#7SnhCEf`qK1WzwzXOOJ7NT`gLfj6?(I|B=mJIIuy%78H8B zpX9By{HvBVtyl$32-8y1!?{|P)z*wl7W`bcxxS4i=6ip*RN@VR?m`<|D$BcMk3&~- z&Jw%Yg@B#a9`F!27IRuK;)Ps9l9AcJbBAQA$H&2-`TbH+(}qi0pYv0`-NB=Ny5p?NP z6zN6t=@Fd}M1G7%UogFmD*YrN6xMlWpUmLVd?EW%vWZ}Ii^i_*0LkDq;96jVi)d z^QQf>w2NlIPnSS+^86mbTq_pJV>@_!*N|i1a*2}xT8S>Li{R67*TXU|onOf8+JFu3 zMBlV>$fq$6^3|MtA~v?k>Q)MCC!5>uJ^~=oTe%<9)Iqc;t{Hnwwu{OX{c$y>pu^q-EEK%aZ_sdDros7H_+B&FEvYf&M|;I}3wzjuMju z($#}cw?!mkeMKFSG+=!ssO)80?Fh$XjS=*XU%$((HCP`hwAgmnAX}TQD2KLruv=n} zf0|9E-+U_M+Zd0GPWy~gxi{lR&`~c`_zG2%xqigqLPx!xNAp@rGl&bhc(bWS{|*9S z+bAb_<;U7y9*S@^Fk=H2M^V|oNmL)qNzktTOlUa8Wb_tcg8KSx%cdri4tC9p=NK6hR0NA9>y>`;1Y@xB0KKd1NvI(w1V zGcrONt1qmL@@d*ntL@}$`cOu7=UbMR3!x9>qn?P7Czxj(C8+FhX$veslQsntmSbUU zb04qgp}c2B9cds?srxns(NcGe#;h+L)m_XAJf8Kv>S?So0SvApd(b2AABRz>@NNxQ zTr6NjegdVXx>B46b2ZGYzoWm(_LNK_;80SkLxFXj{*`?ljrffxQk`_CsRva>vLYW6M%-3hD&g2`ij~u;LzQqiTu+3KpJaUf0g)pF*PE1+7zGPK-0_K0dYBN3zWyZcM-WPCTza zM>NZ>IJT|pJihE-R%z;2=Kj-O*v}y=MEGs;2T~GuBAcyDn*j&$^u0SDOuF?B*77Qt&bhQ_7%zUMkZZEujVcTU;=qdrc#5yG|nYznz2?}q8O`$7h-U*1C3 z-MpuCTijNp-`(o%Gg`)2r0NPqU@hOz?9xVPjDxYwXmLs7$kD|GSDg&fAw0?f+hfhj z1S_j{*DtFPICMc4QF8TGFt}Zo-{VL`%8-t8vY2)aj#(PL93S%Fx+{j&)yeP<#O&mkF0VJ3tn#;FltrxIkw&)E;WG+6W8d`~C2IPzTlOmr|%K$BQCR z+LprYID^+$MypnFo6y%&bCVdwhQ_DLilUgV?%-^f7pELotJNfOrcDDJ}S+MH(B;dXZGcctMkX;Cx-2G zs2DzhS{2I4tQ2BBolUnxYOADEtnIr#H@Y=p@tK7xS}GME4!vVPMy$Hg z*x3>ReM;e&^bdtr@i8oOoM+V(xllN)BR1&;ne5_K}>mKowCU#GVs(F{nmwS$g|{n>BTaVoMnH} z==SjSyi12<+2@^dn~H-Jhx>+AicbSQ-R-o3{$Zn;*99f}j{2@HJKGjlJRp!FNnZC-ZOe{#1xL z{cKoDHmu4~y}&ATqkc-^#DWL4%nuWK+yt0l5-rN`mIsZX^|L&YuPhjTg=rV$T6Or` zdJC=@3k1eyf+WCyYze?)O$z(NToo*~ov^@oAZ*OugaxbUnj$y(p_xqw78gDW&JMta zwbLEtkiez5ENwB%F5{#P5mz$Hv3Ro`b4=Njc1*dE6K}2@P`Pia)oT=YBT4+7(ElI~ zJ2$kjELY=H4S34@aQ&_9crq#y6>G4JPOQxl6-%%T+Zf4|Nrc>m?87D*CX;+bRzS6aW~c`eo2o)U7r@{D9Ul#_B?#9qB`Vgh*y#8>e*{Nl=sdGGA`UZfi>)_9o-!aI%Lw`at7})Qnd$=u(I3I3HvajDz#L|AoLt^Jl(w~~D*NrxL zcTh<1VgMJQ^jLV?9g|4)*gEhzfgCR2&IA9*2jq~TaEnlAl4BT-3EKSnh1PL0J0%_k zmwzHt1G@Mk0>@Y>?M$!;QIX@C?ju4@utd49!G9 z3nhqd%F-m%BlBX+0I)*Rh1Lty0Mtg-`m0j%2RiL(odGTqB_>>cb1mPIZT&X`*zl3v z(x#6q=lwGmiUUOt9{SVURl$jeSkdIJe0T(cCZL^htpp^|q&#e4#!8opLgW#2o-l2< zUL6*SYgwS3mUuoQ+!K^6yLUxK_(_j+@2Nd*4rk(hF6Z7W%YTj~>#Hq;xol93ewS09 zL5A(&d@vhn(cV6=C|z;>{@VGa^E-P%yyMo`Z$A1&I7u!OlY-wNtMq+eJtEo3>3VM| z46^2@3AzHfmXKH{aI3#~LjgryD&jfnQH%UaX9^bZ{oNVgC=+tsdvFAktbKLbQLRSr z)z+IjI%HCk(-j-cg}%wQItxO(EvSa)FO{NUYYDWXLaptZ7Q;3(Z9%jT=QTx4$8<$? zWUCyD6s+*(^)vnh98-_8ZL|bRXJkvZa(wK35AUo__`sar1wJ+|R~ESTyo>*_t3{8tmq4T9YXkNo#(1ouyU8BBSoUv&|^|M=GM4A)r$9hr=={p;$*HT82 zW|!`UZ}cdwIwr?+>yx&*&c<6Gj=$d{Q~PELyxB_r(eO|z$hbim+D*oSt#>x@vNT-O zj8COkOZitoW>s9$;cMut=P}`E;L9(ra~)K%pN_{42WvxfTE`)RSW#-;<;$@??_0~W z+jcCTgu*^o1^uqViAE>;amS_=X;C_Rc4BqA=}THu*A~TOYh%@!+R!*l!CvFU*U}s> z_>lBPTB?`v@bk=p=jmH+_Jnxx;mx~YgS^{#5rX-6mdb{Ad6LQfSn)xFY3)Ca$C8fw zMPKg)ay276UwuFRw?^T^`baulWk!s{2$v0}5`^OUd-1Om8w5I|>>(e@$S+V~FN2k` z%%dXpC2LoEIh0a8b26N_VH5N@3~Sf-go6H@1>&l%1WQW063d5Mse8kmCT6RAR;h=X z;SUkIH+(g@H(cf61STcPSd~yPhMm26E$6anQXzz=XXpB|(ItbxxnW8^97htZ-D4bx^qYaKdf?}g8^ zVP4;Qzm64f@|H@u0oB!m>_LMW0--#C5RAVD9AWIKW@B#@fW=q0MLDIs(za!Fj>38AhnwRE3G+D#s&&F!|j&Gz1-_u6A=(RGj{?4#7YKY>hGwyu$)xS$8>Kzbr(0ew#FVW&rHSeF9W)LHYK>fAtjRV{R5|uHzHRC!gH&iRUW-U_Y z?~tYrf8cC!>H~vB1(_&3zrfhpT$_zhOrZX=>qN++t`OW}wnESX*p+ey1(gjC?4-=# zCN};h5)0v)6I}Xv*KF){)bHTK;@RopEmb6&7-bN8;;Vfnl+8>d95?SIcB*Q)0+U_r z$6B>o%lC?wYCqSJOLbeO9wxA=^C=ukA74X@ScP_XmoTk})%A$2Nq0Bag<)vj4wKJE z%f0+Qr6d&_c{cLe5$zsEN1i_4a5;>jtNGk&W+&p3* zHXc|x{1FwW_3eVqSgVtZTz9#t(Yip2^(j^HG*H1Ng9i-qqKC+{cMH*Nv2LZn!$cOj zN}-QS$uuW8>~n_YG~rNbzD9unXP zw5Pwh2b#5ExRg)=9zL*eab$vr%8Xs#(5|9>8f;MD+wW`lI!1}VBp&tHxIdU5OJuE` zK8HeRzd?&urSGo`RN`JFMie?h%r#wt#>GMgeCa_3{Gm|;uG0x8$?byA}QQV1N2^z+8Ub}EIkA)&4$DE1PZLO69=agQ7Z|0bMV5c^? zIWSmYA^$;)~rbeMP&N z91K_s&1n6=J#3uS*RU7{3lu2B_9RRuZyrwqH(U25&jqIo2pZPpqMAOecusL69N{EY zyWj*>wsu*pK;T<)7P9sEBL0%Y2`rN0p6i9H&1?s^0|W5zu68pPR0o(Pzk_CfYNynG z%S>~_hB0eXa9g0Zc0P8@EIUASe6goRP>VC@)D0TMG7_`}uWSc3HHq*A33ZoEbH#Vm z1c`ZA>RAQ!nyXR+^AxM8^qw`o?61AM7VtzD&YvMro>-H>0#1EVnrW#<*dGoMTI@t^ zAxK@?8l605z@b;H+lQs3KEEfKvhzSzko^|L&r2stc!wW2VH^A_MaK>;4|Eqy_lWQ1Q4{@@tv z1<6%3tQDKjZp+ZLMhEw34CzQPd0fHnt;6A^st|3L9uZ7CM!1-oz?86=#|zuj=}bo) zLHgWseC5c&(p6n56vBCK5yUvSKYPQznODhP;k?Wl3X8@@y<9k;9{D41Iyr3$?^vYI zWM2+X;5RgG+kFvN6D*&4V^_Vyci$?9DCsUAO#@@J2kZ(Jsn+>oSV78rrn615Ngh$5 z!w9|WTdE95Xd>b8HVt0j>-A(MEM=pBrj>6{xG3Um+9wJ2w}d{gp&xP5BV^B5-M5jS z>0H73R7Ce0G15*gqGj*SNT3|P>@~7&C$CURhZVr$##zlC?{F088QMI6GLq5r+l3s? zmi}K>yTyCV(S1G2kQqk8ZU zRWmN~2jGb83v_GoIGUYUPHsEKxfy&UuCMDcnH@;>Rd;VKdQG6-P@=qYJt@&Xy!1_5 zF;TzU7~}yZMm-|@lrx0^^ba^T+>9!HRH z{X&Kq$(U568Y+%tW$(%11?y5gGZ&`!k#LErEVN{fc>XX8UvSdg4jpD-Z#VL_$+Dl$ zb|K1#=N&V@Tp#f=l9?)`9)hE@Gb8M5YEFMn&^{EQV#SXa%tA{O%(Bd&>>tAMrD9PP ztz#>VJeCxa2y9MhmQGhTZ14FFQ)rhlTAEW?%;SmcP^DkGT4a_#slUq7OIu^?0rAR% zsOzr!;R0sGKYuvv%xMZ7VIbsuM?ATuulu9;ov}5-(N@7|!hGk&6#TXN=~px|n$dh6 zA6NK;ghGq&96-|PD>BcEy7ZN?-BW>S^))X(I6`c8PG$^g#is=d086M6O0nvWsKt;J zD$MoL9`Ttflh~YAX@2;U>uzhqu6TV{0t9#0pur(nAV6?;nbV#7o%_yx|Ad*f&M(lc zL)F>4s!r|tJWty^CiPO|rDQZ5BYZgtF|?T)!n!*pkir zc{;inwcS43dKv;$cvrb-*vq-ngYQc1cRdpa#54Lc&hhf8SJdgP+PKa#GWB;>2kp4@ zCFj9|AQaiE!=Y-4H&b^P0=7&i(uH2Oqb8Pv`3|L0aL@Q5-=n79i zE-^WZ39N4U5K-JTht7w%`)h}Dol|{aL!ssxw!GtJMP|lQerLCW;~+r#q17!YVOAYv zw#Y#~tR~((#^6YQ>>&#gLnuRwgKK}|r-rz?6(%?8dVzzx=Z2neX5lUJX;viP7Bgjg znKIT)MuG6>W19Z=&2wCRsj!`1ET>=#VlaymwJyxojxG#)Wb0e?wdydgm3MAjV;fpZ zdh5%~aq4Oj>MlM--&4vO9d~ZTJ!X@|M5S34cVuH3KNlux%An!ZbZ**xr#7WuQiE!9 zrPow7YM&E<-!BAx=IjobZ012nd#zXV{K`|&iFG)b!Lr1L(Y7%9oBS@~b?vd$NZYtz zZu%T72Voh5j(D9Ru0U-X>iqQ%?Eajakr}S>Z$WwCAAK&@YFJd%G({99TC_h@`ERgV zALzLXImqb82^%hoS~y)f!fa}Qp3d7+Jrls@B|Y3KF%KvN>KTvOrC>)8 zBw$DQ)NGkV-GmHV5WJH{`!(81TD6&IP{gp|Msz(-sbj=a(j^9HS~ zj9On$fbFa@NqZRn=%DFOek+vI(7Pg8I|28Xx`zCe>9d=-#(4@;;jf*~|KvNLmyF7= zAd`5C$H=NEu(?;*bR;+X*}@0b7wX*4G2J;z%)tgBlN_Ww6p>WilY@M8bay@~v6#J+ zAeLm{Rs8kkO=Z^(55tE2NLsuDVav4tf&5ye|3-;Lc)2t&H`ywg;16mc&P<-Lw`lB$@Yg#hn@k+B=~#R!*V03 zMn2z3d#hE`lCPbLkOu=BP`#AS37UYoCs zN+~FG-`&+4Ieb=xmtxS1l2-(?MGUJ`5>PjrG#F6(OCWFIMhE6sj(`8F&N28Ra(@Q< zpZc8UyQ-C0U*wuo;ob1zczZBJr+TzG`QsaI}MPKo##2E?Pt!?VQ+Cc`}v=#7a#oZ$<+*x>$0gakzL@#s6AS$fA&UqQn+zsdZ}iZ{a&TJ zdCg#lEgZVtGS@qU*~IRNAx0 z14ZEVXl#p-&u?tx>XzvVhI|@^oVtjkHiGJ&hu|2k)J_@*cQevP1UBB4xl`_Knd))- zqjU)Rz-C*1#Bth!37?&i%HX|4G4NfY>!Cp<46j^H{rKp+=(yg44N^Jq4iUy`JIQ(L zCCVmDij9C4jbsQZ2?)Vz0OO)u?DbJu{oBtzXp2|h*V{<2Xgll`vJA8yw)EwjE$z#6 zZt17)Sa?GMIEIRw3V&yc!#iq7!}gypCyBtUM7VTFWZdN@4M=7i27JD4xLTw-yRKE# zakMmH$l}0qwI)aK&^~ijJ7AQ&K|AvofK#&z?WA~9H~rqk3b~s_p6?QR*w>y(zE#1C zEjuSY9lR}A3S-AkLwzCM)3}SimMgvor-A(&`82 z7B6)rT46M(snPY?$6?a+h)>*M#9VcH6>c_rQfY%t2j$YmKCub&>m)viJ>rs15&l($;uU*pFy`R{!V4QFI6BsdDc_v4N#|Xf_p&tZx zN-7ig8h;eMa~AEHEzicx+aGpy^K@&aXgN@Z3)LT}hCB6e{F zHBS^pg{OCZcDB(<3A!@!7p{pVUzp;&cX8FU@&yX?3?@bR__qagCFj)rk)A9jYuYU% z%C+aeS-&gqDgJMrSx5ieNV2AkhJg7EC-(7d5)51ILggS zn-oU>Zk8&7UE`>SjZ#eO#1wa=^J-H|Et#54|CfR}<6eQvYfSFTFqRN>Z$$nVpk6$i!#qI#bRC10c#_ea>fnJM|icMMom zjfzkGKd$Jn$AzP#+SC*`cgwIh74odyb|={<$!d&0-FkNl;>#YN5Zb{%adXMgJaTZt zhK6n++$7x{ubY78OrP{UPjhsgwlxpCb}%(gROIsru5rLGj@m;sS3rNbtV9pfD5c-9 zJ$vII@rA^_94< z!_Mj_fhZHYaRdyWcqf~~%*O4kNk{tDT23|dkEXg4A9w3trygDz4z1Ps5WOSMSny=oEd!XenSoH0#}WG@%w+m4(WW4`AThR ziDUIIxaifukJ_XgU$>>pW>>qu&+D+}E?H)LWx{E?xqN-=dsDR^-2G{{;W(X3aei7_ zM8W`u)@Ri3p~M7*oxH;0_qJu__BOrJJHQyJ(PnsqARV(miNY)kdZdWs(F)Pf1=0hw zKc-n{=1lAVrxk=BY9naW)uh_Be%n2x*lm)Yzygwu2kFe9*c>Ezsb#kZu`h6~?Q*^B z30?G9^&+(pB^~qsxnEx+{aEAf($4}Caa$A9A0_l*B*y-+*^w;`7fLaO*@#}|x!%rY zD~lD9J@nLT-|U$Z%;4@P2y@`Xt*q-yl>KEncqN-7-(LbRB*fIjs^SY~okgGLAG%eR zh$(i*D%&N?IgzSb;-Muxy4|-~14ZBbY7bQ8zmYkR(Vk@xSySA&|9~2=oriIPgKnZE4g0spg_F=_h&HSAEe_ z`H!qhf}HGH97`5<08`o+#kr98!>t1U`AfH)HhHT9$=z!J^E*asMIDn(cD#uw58gCr zywoO+zeDfr3@ljzjyLiS8l4Q9Um?evF@@Kc-xj#(w4^cEjmm!;`#SgzyJg91oi-Hi zK&L%!%)RWq3{}R@JOs44ni=qxd`ZCfm7tApca?J4R1TX>0?&|&h0$FGypfX|e{fhvt4IA9FsR{T4Y!F@Xy! zxve#((M^E?l%aG3sWLFDULpt6LV0Y=GzM9yV8j9?&1nHh zBrhV`iq1FX-*mZ~gD;k@R<+J}{Pddf;=}tMh8Y?AO1Vv%7d(?T9&W8k_l$#c$wo4fF z1r;-mU=Y9x0R#i`V!PMMYhfEh1y(S~&_2V94M7h#y4A2Qu6X7$VxX?#cJtvw6W`+V$%9^kP;sEU*1GJ`h zqkMuE(qAbhAY@ps?=OJ&XrTF0`xmjr`H>JnU26JsrxSqR_fi3kIRP=%LM3Qei9qAw zGqrrR;&=c(0hTvE_3jR|@mL>d9QA;xo@G$e$g4k>7ARR0!JnL$-$6X~|CeOo(J_aeHy*+U0OV_D}`~9c7yf6powopu!m9aj>!J0pv3FahF+@QQ(H$2{LH3wII=Uv*u=j{1he#Ro;Or z{hklVTkS0I4ly}uW$K7duzWq?!qPF7>~wq z^ud3yLnB9xNSU|p+_5)QeRU#YH@wG|1&9IeV-7$?rN7DP%sHhWpvzz zd_T++C58J*nM`3@Y$J3x!gDlxCE?#1FZ3D-5REl*%{JBda|mx(pK<6tlp|CyxXH!uu;5po;{v}e*r~&eRi=hs?IGgx$ zNis+AXhr!TG#b*0XUHkQ@MSq$;o33qZT$Vz0B~T zci@LjONnDtCWf1uVmUWQq~gj8O?v~mjgSx!X4t%SD8m7{(f@k|9*O`1)IF79QTktY zFp!)5_k|G)4DbNUYk+R%gJrhh7r0kN9m{eEt~Y-5;kaUuj1?;t+}>qe%bv5ePwyDwIiSEo6a8RpFx9s}~;y*p# z90v}#9pS4MRA)ZjE}|<_(S@*k@pM--n3d+g z$hCkg@r+i}UCIGHsi(NdZ3*Gv1vU>rZ2F>Rp{g9UtfqdnCC&td9`6zlt|yiahkI3Q z(|G$UkCGPoQ}n-a2ociNzoqX#eD0a?4)=39?fda>2=fP4Kc+NhU)ySPXyfDP6))D1 z2`+eHXg+wwqkltDzKn^X8fTXvTs0?LHSlJ>VUsyo{$nC$z}lg+3;s3m)QF{CPs**t z{$$0+PBB-R)r9+xg~LDL3JV5eA!S+p8kw3taQA?cGbIrwJ+KfbJd6cVpdt^ZMNDae z10@}$Uy_?%TN7N3R`W9FGF4qIMBU{y3T36N#)}v5CT>~1OLlrc2k3@COffa5%AhJD z#5WNlW92BXvoqcx5LG+$O%?N*mw`O);7p@9#z0f&fD-VX)1TK0hsO#NlEqCLV-79Ewyf;_^^Wf}ny-Mi zX+p_`q{UD#{NY!{cxiF3DGkoX_12f-d7Ek2(}=B&IEU!IH9zJIzq{n?p~bd#sqn;b ztp+P*k}T$*{_X&8Sevd2dV#(yh*I(4!w$c7hL|a}@PRDT?qGIAk+~mf1L|H3Q_*Ty zy}v(RC=FIYu`B6T)QOfh%?8sI_$`7ezQD&J!%=bTw{)fGb^!44uwt-Xxe|h$N(NKp zb*F+E>B_95Hh4uQyfUF?xugf~sE1>s34-^mQPSqr#*A#0Kly7>11;=9#&}L9kX^Od zBxP|z-55Zn5t}B+_-o%uk{;HD!b{T9&0r%Yub3d zBub>L?~BmXl$zCC$iqNya05$=%4IPI0HT-xASwnQ@l_RM(i70Su@#BbX%`pM9$qa z^`l;hQNa`CGRSl(2czz73B?Jv&W!w~R_t$1H8?4lLIAhEw`EE<#bh=LU!m{Z&6Lgg zPGG=W!L>gBe0t+X5`R6Xey@*JonXY3t$3kE1RhOs3|t;gqs8G~Ugrs=fc{B!V2TQZ z1zt3i1BPj(b96BsXWsXT(j0%8loMW$8elQ&){L_vHlGNBW6J9E;?MpB;0o++ss)l{ zjP;2Wx-oi4l43}LK|KP)uq;byg8m#?sTeXVEOV(8x-Ny0lg!ChrcyHT)J3w}U2VjD z6uOSHp7m?fhq{6BGM~&9D9F;;5DY)>U_7@=Av`7hX99svBd)YSt1_h%?BayDXeLx@ z&=m*WP;5}f%3H&)@2nD7nBS^~M@^7;bGL~pvazbin~l9pe63pF@jBt2EGsx&o4!I1 z!DK*U8-;|Me(4*DryojC->h>r$6TvfumAAKhx_a$-#qXr1G0SfH)+c>n(oO}lVPp_ z3bvh-Z*B)?Z2vKNLo~m^l)c;3jvp}`In+#+LN4aL`ftrI_Tb4d&&L_Nm?$xYIB*NBEydmk0bJgrXN)&6viaH$)dpZCqN7cZaG;NPw;&}3gR~~L@vv8+)837G@@8>-i_g~bVGpw81 z$7yV=0ps(LGpc2knin;)?u~+1_`=gtkwSobUbe(a`DglcTZghrmt-rSzf6IWGI{%A z;Gxd$U;Lnm8RnaLj`NJG!;cxEi)-%xkukAC5L1BQf$%R7S+b+ghKkz$m8~vo9-6h= z8__>#isq;VIPXUb>DGCl&4%vooV}E> zyq?EUVhV2+Q%Zqg5skj#QhwXViR-h-JIE|ih*brKAmZJGr=CK2$+1|!2i5x^^|k*@ z8%GGUoQ(55PACs6mXC1!txjM%*_{BenG-bL?2M3XqbtObX73?+lO_yK(x;XDzKexe zyP@=?4i=sH$o8{<2<*@w6wzj2z0!P3w~NdmL^k|8__H2OXy3`fcjfAqVcia2OcIs0 z6MDQ6U1WW8VHrQxEN}C88s|JrM9%h?_mz??6uR-TY2Yyg=Li`kfdM{&2IDcWX}#Uh zeL2Hm$>qT9lO9p7^}E&daJeIobK~iQo{zGO{0=;-pvm;@|7m?=4>6Fi-(1+9mli^5 zZNF7Ma!W|e2v2#Mcpl0=+p{BkwdVZ$-Q$Yt+X0u*?5H|k*%Oxy z=Mg{ux{J+JH}0CVq>L2;!5aw@|HlQ7T_eTUPd~yO#`VMYbgOR%iZ;sTB)Jp^$l;MN zUI_&}gMo!d+BX#Hs`eLM62x`KSx4Y4$?bJ2XY&*>M(1BXplGNtuzl9T z_uZU%J%t+HA{zA%`R|L2OCTNM;AM@ zmvo2iJVX{cn(=&Xo?rJSL4u@gx((Z|{@>0XE(J(O`y_lEz09|_$}`DZubTBEe%{f& z{deOq=92@0ObmU{Xk$f>o*ge%{4e|?u>9?0bfgoo$z+Q-KB4|-`(kOuc@)_ zEGTjo|Ga;`x5UHdw9j(2bMmH*xKi`isRjAKr?O?5A*qa%M-~G@{YXUvUqQ~V2u=P} zKgLkSvaR?FFQ&i)wQf&Y(O0@L_L+A4%@^%1cFWXkk2`9-~LPmMNo z&;3J67>TW_>g40v=`Fn=p5M9Qu4o1OpXZG6>bMvd z1uXg5g(NT-cJ$)2R%;!J4L>5~qd|0*C>$%_{OaTQT~=y(^|wZhj@9s;J*$2llBnGE zE{|3hRWN^yot{C81*%49Y#`cKZbMc18dTgUL;(M-F-V;g$wE9 zoXf-BrO18t^sTF*V)<8zmn38nk91_NNxc5!)-`m`LqhVOI`a5zUuV<(D-AGSDd`Dy zDf>L@{r7L69}&XikPvDpKL=9fzjNmQ{`mi%24wd5zubUMq5m2e{|lbRBh1~!#&U8v T3O4XXNYA9j6~w-Z7zO Date: Wed, 11 May 2016 20:20:27 +0300 Subject: [PATCH 153/449] Revert "disabled_oauth_sign_in_sources column" and keep on schema only the disabled_oauth_sign_in_sources column This reverts commit 95358e0095403ad44149d11922d52e4590285a20. --- db/schema.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 74facd1208..b21cc16289 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -70,16 +70,16 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "recaptcha_site_key" t.string "recaptcha_private_key" t.integer "metrics_port", default: 8089 - t.boolean "akismet_enabled", default: false - t.string "akismet_api_key" t.integer "metrics_sample_interval", default: 15 t.boolean "sentry_enabled", default: false t.string "sentry_dsn" + t.boolean "akismet_enabled", default: false + t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" t.boolean "repository_checks_enabled", default: false - t.text "shared_runners_text" t.integer "metrics_packet_size", default: 1 + t.text "shared_runners_text" t.text "disabled_oauth_sign_in_sources" end @@ -427,10 +427,10 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "state" t.integer "iid" t.integer "updated_by_id" + t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" t.date "due_date" - t.integer "moved_to_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -717,8 +717,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.integer "project_id" t.text "data" t.text "encrypted_credentials" - t.string "encrypted_credentials_iv" - t.string "encrypted_credentials_salt" + t.text "encrypted_credentials_iv" + t.text "encrypted_credentials_salt" end create_table "projects", force: :cascade do |t| @@ -816,9 +816,9 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "active", null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true From 8313476f5e1f1b51b3857ba065a65a707f5ca8af Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Wed, 11 May 2016 23:39:07 +0300 Subject: [PATCH 154/449] no schema info for ApplicationSetting --- app/models/application_setting.rb | 49 ------------------------------- 1 file changed, 49 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 667b45335b..c143cf215e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,52 +1,3 @@ -# == Schema Information -# -# Table name: application_settings -# -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# default_group_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string(255) -# session_expire_delay :integer default(10080), not null -# import_sources :text -# disabled_oauth_sign_in_sources :text -# help_page_text :text -# admin_notification_email :string(255) -# shared_runners_enabled :boolean default(TRUE), not null -# max_artifacts_size :integer default(100), not null -# runners_registration_token :string -# require_two_factor_authentication :boolean default(FALSE) -# two_factor_grace_period :integer default(48) -# metrics_enabled :boolean default(FALSE) -# metrics_host :string default("localhost") -# metrics_username :string -# metrics_password :string -# metrics_pool_size :integer default(16) -# metrics_timeout :integer default(10) -# metrics_method_call_threshold :integer default(10) -# recaptcha_enabled :boolean default(FALSE) -# recaptcha_site_key :string -# recaptcha_private_key :string -# metrics_port :integer default(8089) -# sentry_enabled :boolean default(FALSE) -# sentry_dsn :string -# email_author_in_body :boolean default(FALSE) -# - class ApplicationSetting < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :runners_registration_token From b17cca1d9460a00510a2e9f04300f07cfb5eb95a Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 09:37:15 +0300 Subject: [PATCH 155/449] delete the enabled_oauth_sign_in_sources after getting it's value into enabled_oauth_sign_in_sources just delete enabled_oauth_sign_in_sources and get it's value --- app/controllers/admin/application_settings_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index c300560236..b9eb7ae792 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -53,14 +53,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end - enabled_oauth_sign_in_sources = params[:application_setting][:enabled_oauth_sign_in_sources] + enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources) params[:application_setting][:disabled_oauth_sign_in_sources] = AuthHelper.button_based_providers.map(&:to_s) - Array(enabled_oauth_sign_in_sources) - params[:application_setting].delete(:enabled_oauth_sign_in_sources) - params.require(:application_setting).permit( :default_projects_limit, :default_branch_protection, From 5553ad24e4fe42670ba0f4e2010819abac499e7d Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 13:37:44 +0300 Subject: [PATCH 156/449] Revert "new screenshot for the docs on how to enabled/disable OmniAuth Sign In" [ci skip] This reverts commit 7b295370efc124360d52e37e8e709914028f6443. --- .../img/enabled-oauth-sign-in-sources.png | Bin 92674 -> 49081 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png index 08155a4fade75af85f36fdbe8cacff1af936280c..95f8bbdcd2489c4eb14c100c92dffadf3a1a65fc 100644 GIT binary patch literal 49081 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 n{ErgGcytqE7Zr6^h`m+mD8d?#n7A>v#VTY~gf5 From 6cdce04937837de8014b1c044c91ed5b32bdc90a Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 13:40:24 +0300 Subject: [PATCH 157/449] Revert "Revert "no need to show the help about Github or the other authentication sources"" This reverts commit 075ace5686c63d40f7e9fa02bf37e1bd3168f7e7. --- app/helpers/application_settings_helper.rb | 9 +-------- app/views/admin/application_settings/_form.html.haml | 3 --- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 82056a4c97..878b1b254c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -71,15 +71,8 @@ module ApplicationSettingsHelper label_tag(checkbox_name, class: css_class) do check_box_tag(checkbox_name, source, !disabled, autocomplete: 'off', - aria: { describedby: help_block_id }) + Gitlab::OAuth::Provider.label_for(source) + 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) end end end - - def oauth_providers_with_help_links - button_based_providers.map do |provider| - Gitlab::OAuth::Provider.label_for(provider) + ' ' + - link_to("(?)", help_page_path("integration", provider)) - end - end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index f3681bc96e..576509e3f2 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -116,9 +116,6 @@ .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes('oauth-providers-help').each do |source| = source - %span.help-block#oauth-providers-help - Enabled OmniAuth must be configured for - = oauth_providers_with_help_links.to_sentence.html_safe .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 From 260c88a75865a1b849a4bac8a14c897fbe522047 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 13:43:12 +0300 Subject: [PATCH 158/449] no need to define the help block since there's no help block --- app/helpers/application_settings_helper.rb | 5 ++--- app/views/admin/application_settings/_form.html.haml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 878b1b254c..03080d2593 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -61,7 +61,7 @@ module ApplicationSettingsHelper end end - def oauth_providers_checkboxes(help_block_id) + def oauth_providers_checkboxes button_based_providers.map do |source| disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) css_class = 'btn' @@ -70,8 +70,7 @@ module ApplicationSettingsHelper label_tag(checkbox_name, class: css_class) do check_box_tag(checkbox_name, source, !disabled, - autocomplete: 'off', - 'aria-describedby' => help_block_id) + Gitlab::OAuth::Provider.label_for(source) + autocomplete: 'off') + Gitlab::OAuth::Provider.label_for(source) end end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 576509e3f2..f7c799c968 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -114,7 +114,7 @@ = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' .col-sm-10 .btn-group{ data: { toggle: 'buttons' } } - - oauth_providers_checkboxes('oauth-providers-help').each do |source| + - oauth_providers_checkboxes.each do |source| = source .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' From d66833bca5ae0347751ceab99062b638965eab78 Mon Sep 17 00:00:00 2001 From: Andrei Gliga Date: Thu, 12 May 2016 13:56:04 +0300 Subject: [PATCH 159/449] =?UTF-8?q?schema=20fix=20=F0=9F=98=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/schema.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index b21cc16289..74facd1208 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -70,16 +70,16 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "recaptcha_site_key" t.string "recaptcha_private_key" t.integer "metrics_port", default: 8089 + t.boolean "akismet_enabled", default: false + t.string "akismet_api_key" t.integer "metrics_sample_interval", default: 15 t.boolean "sentry_enabled", default: false t.string "sentry_dsn" - t.boolean "akismet_enabled", default: false - t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" t.boolean "repository_checks_enabled", default: false - t.integer "metrics_packet_size", default: 1 t.text "shared_runners_text" + t.integer "metrics_packet_size", default: 1 t.text "disabled_oauth_sign_in_sources" end @@ -427,10 +427,10 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "state" t.integer "iid" t.integer "updated_by_id" - t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" t.date "due_date" + t.integer "moved_to_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -717,8 +717,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.integer "project_id" t.text "data" t.text "encrypted_credentials" - t.text "encrypted_credentials_iv" - t.text "encrypted_credentials_salt" + t.string "encrypted_credentials_iv" + t.string "encrypted_credentials_salt" end create_table "projects", force: :cascade do |t| @@ -816,9 +816,9 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "active", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "active", default: false, null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true From e867099091e19e8cae47d928761b869ec20db2c3 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 May 2016 14:14:32 +0200 Subject: [PATCH 160/449] Added helper methods for database migrations These helpers can be used to perform migrations without taking down the entire application. For example, the method "add_column_with_default" can be used to add a new column with a default value without locking the entire table. --- lib/gitlab/database/migration_helpers.rb | 132 ++++++++++++++++++ .../gitlab/database/migration_helpers_spec.rb | 124 ++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 lib/gitlab/database/migration_helpers.rb create mode 100644 spec/lib/gitlab/database/migration_helpers_spec.rb diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb new file mode 100644 index 0000000000..7aa8c90e6d --- /dev/null +++ b/lib/gitlab/database/migration_helpers.rb @@ -0,0 +1,132 @@ +module Gitlab + module Database + module MigrationHelpers + # Creates a new index, concurrently when supported + # + # On PostgreSQL this method creates an index concurrently, on MySQL this + # creates a regular index. + # + # Example: + # + # add_concurrent_index :users, :some_column + # + # See Rails' `add_index` for more info on the available arguments. + def add_concurrent_index(*args) + if transaction_open? + raise 'add_concurrent_index can not be run inside a transaction, ' \ + 'you can disable transactions by calling disable_ddl_transaction! ' \ + 'in the body of your migration class' + end + + if Database.postgresql? + args << { algorithm: :concurrently } + end + + add_index(*args) + end + + # Updates the value of a column in batches. + # + # This method updates the table in batches of 5% of the total row count. + # Any data inserted while running this method (or after it has finished + # running) is _not_ updated automatically. + # + # This method _only_ updates rows where the column's value is set to NULL. + # + # table - The name of the table. + # column - The name of the column to update. + # value - The value for the column. + def update_column_in_batches(table, column, value) + quoted_table = quote_table_name(table) + quoted_column = quote_column_name(column) + quoted_value = quote(value) + processed = 0 + + total = exec_query("SELECT COUNT(*) AS count FROM #{quoted_table}"). + to_hash. + first['count']. + to_i + + # Update in batches of 5% with an upper limit of 5000 rows. + batch_size = ((total / 100.0) * 5.0).ceil + + while processed < total + start_row = exec_query(%Q{ + SELECT id + FROM #{quoted_table} + ORDER BY id ASC + LIMIT 1 OFFSET #{processed} + }).to_hash.first + + stop_row = exec_query(%Q{ + SELECT id + FROM #{quoted_table} + ORDER BY id ASC + LIMIT 1 OFFSET #{processed + batch_size} + }.strip_heredoc).to_hash.first + + query = %Q{ + UPDATE #{quoted_table} + SET #{quoted_column} = #{quoted_value} + WHERE id >= #{start_row['id']} + } + + if stop_row + query += " AND id < #{stop_row['id']}" + end + + execute(query.strip_heredoc) + + processed += batch_size + end + end + + # Adds a column with a default value without locking an entire table. + # + # This method runs the following steps: + # + # 1. Add the column with a default value of NULL. + # 2. Update all existing rows in batches. + # 3. Change the default value of the column to the specified value. + # 4. Update any remaining rows. + # + # These steps ensure a column can be added to a large and commonly used + # table without locking the entire table for the duration of the table + # modification. + # + # table - The name of the table to update. + # column - The name of the column to add. + # type - The column type (e.g. `:integer`). + # default - The default value for the column. + # allow_null - When set to `true` the column will allow NULL values, the + # default is to not allow NULL values. + def add_column_with_default(table, column, type, default:, allow_null: false) + if transaction_open? + raise 'add_column_with_default can not be run inside a transaction, ' \ + 'you can disable transactions by calling disable_ddl_transaction! ' \ + 'in the body of your migration class' + end + + transaction do + add_column(table, column, type, default: nil) + + # Changing the default before the update ensures any newly inserted + # rows already use the proper default value. + change_column_default(table, column, default) + end + + begin + transaction do + update_column_in_batches(table, column, default) + end + rescue Exception => error + remove_column(table, column) + + raise error + end + + change_column_null(table, column, false) unless allow_null + end + end + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb new file mode 100644 index 0000000000..ad2f3cb7e4 --- /dev/null +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -0,0 +1,124 @@ +require 'spec_helper' + +describe Gitlab::Database::MigrationHelpers, lib: true do + let(:model) do + Class.new do + include Gitlab::Database::MigrationHelpers + + def method_missing(name, *args, &block) + ActiveRecord::Base.connection.send(name, *args, &block) + end + end.new + end + + describe '#add_concurrent_index' do + context 'outside a transaction' do + before do + expect(model).to receive(:transaction_open?).and_return(false) + end + + context 'using PostgreSQL' do + it 'creates the index concurrently' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + + expect(model).to receive(:add_index). + with(:users, :foo, algorithm: :concurrently) + + model.add_concurrent_index(:users, :foo) + end + end + + context 'using MySQL' do + it 'creates a regular index' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).to receive(:add_index). + with(:users, :foo) + + model.add_concurrent_index(:users, :foo) + end + end + end + + context 'inside a transaction' do + it 'raises RuntimeError' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect { model.add_concurrent_index(:users, :foo) }. + to raise_error(RuntimeError) + end + end + end + + describe '#update_column_in_batches' do + before do + create_list(:empty_project, 5) + end + + it 'updates all the rows in a table' do + model.update_column_in_batches(:projects, :import_error, 'foo') + + expect(Project.where(import_error: 'foo').count).to eq(5) + end + end + + describe '#add_column_with_default' do + context 'outside of a transaction' do + before do + expect(model).to receive(:transaction_open?).and_return(false) + + expect(model).to receive(:transaction).twice.and_yield + + expect(model).to receive(:add_column). + with(:projects, :foo, :integer, default: nil) + + expect(model).to receive(:change_column_default). + with(:projects, :foo, 10) + end + + it 'adds the column while allowing NULL values' do + expect(model).to receive(:update_column_in_batches). + with(:projects, :foo, 10) + + expect(model).not_to receive(:change_column_null) + + model.add_column_with_default(:projects, :foo, :integer, + default: 10, + allow_null: true) + end + + it 'adds the column while not allowing NULL values' do + expect(model).to receive(:update_column_in_batches). + with(:projects, :foo, 10) + + expect(model).to receive(:change_column_null). + with(:projects, :foo, false) + + model.add_column_with_default(:projects, :foo, :integer, default: 10) + end + + it 'removes the added column whenever updating the rows fails' do + expect(model).to receive(:update_column_in_batches). + with(:projects, :foo, 10). + and_raise(RuntimeError) + + expect(model).to receive(:remove_column). + with(:projects, :foo) + + expect { + model.add_column_with_default(:projects, :foo, :integer, default: 10) + }.to raise_error(RuntimeError) + end + end + + context 'inside a transaction' do + it 'raises RuntimeError' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect { + model.add_column_with_default(:projects, :foo, :integer, default: 10) + }.to raise_error(RuntimeError) + end + end + end +end From d7cfe1a5bf671774a2fb7ffdb483fdbadf9f3228 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 May 2016 14:31:39 +0200 Subject: [PATCH 161/449] Added custom migration generator templates This allows us to automatically include Gitlab::Database::MigrationHelpers into newly generated migrations. --- .../migration/create_table_migration.rb | 32 ++++++++++++ .../active_record/migration/migration.rb | 52 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 lib/templates/active_record/migration/create_table_migration.rb create mode 100644 lib/templates/active_record/migration/migration.rb diff --git a/lib/templates/active_record/migration/create_table_migration.rb b/lib/templates/active_record/migration/create_table_migration.rb new file mode 100644 index 0000000000..1235ecd8e6 --- /dev/null +++ b/lib/templates/active_record/migration/create_table_migration.rb @@ -0,0 +1,32 @@ +class <%= migration_class_name %> < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation failes and can be re-tried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :<%= table_name %> do |t| +<% attributes.each do |attribute| -%> +<% if attribute.password_digest? -%> + t.string :password_digest<%= attribute.inject_options %> +<% else -%> + t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> +<% end -%> +<% end -%> +<% if options[:timestamps] %> + t.timestamps null: false +<% end -%> + end +<% attributes_with_index.each do |attribute| -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> +<% end -%> + end +end diff --git a/lib/templates/active_record/migration/migration.rb b/lib/templates/active_record/migration/migration.rb new file mode 100644 index 0000000000..3cde953d15 --- /dev/null +++ b/lib/templates/active_record/migration/migration.rb @@ -0,0 +1,52 @@ +class <%= migration_class_name %> < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation failes and can be re-tried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + +<%- if migration_action == 'add' -%> + def change +<% attributes.each do |attribute| -%> + <%- if attribute.reference? -%> + add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- if attribute.has_index? -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + <%- end -%> +<%- end -%> + end +<%- elsif migration_action == 'join' -%> + def change + create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t| + <%- attributes.each do |attribute| -%> + <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + end + end +<%- else -%> + def change +<% attributes.each do |attribute| -%> +<%- if migration_action -%> + <%- if attribute.reference? -%> + remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> + <%- if attribute.has_index? -%> + remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- end -%> +<%- end -%> +<%- end -%> + end +<%- end -%> +end From e8efaca23ce0b5aa347dfa053091d0b48e746fb8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 May 2016 15:05:19 +0200 Subject: [PATCH 162/449] Updated migration styleguide for new helpers --- doc/development/migration_style_guide.md | 46 ++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 28dedf3978..1c13b09458 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -8,7 +8,10 @@ In addition, having to take a server offline for a an upgrade small or big is a big burden for most organizations. For this reason it is important that your migrations are written carefully, can be applied online and adhere to the style guide below. -It's advised to have offline migrations only in major GitLab releases. +Migrations should not require GitLab installations to be taken offline unless +_absolutely_ necessary. If a migration requires downtime this should be +clearly mentioned during the review process as well as being documented in the +monthly release post. When writing your migrations, also consider that databases might have stale data or inconsistencies and guard for that. Try to make as little assumptions as possible @@ -58,6 +61,45 @@ remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation. +When adding an index make sure to use the method `add_concurrent_index` instead +of the regular `add_index` method. The `add_concurrent_index` method +automatically creates concurrent indexes when using PostgreSQL, removing the +need for downtime. To use this method you must disable transactions by calling +the method `disable_ddl_transaction!` in the body of your migration class like +so: + +``` +class MyMigration < ActiveRecord::Migration + disable_ddl_transaction! + + def change + + end +end +``` + +## Adding Columns With Default Values + +When adding columns with default values you should use the method +`add_column_with_default`. This method ensures the table is updated without +requiring downtime. This method is not reversible so you must manually define +the `up` and `down` methods in your migration class. + +For example, to add the column `foo` to the `projects` table with a default +value of `10` you'd write the following: + +``` +class MyMigration < ActiveRecord::Migration + def up + add_column_with_default(:projects, :foo, :integer, 10) + end + + def down + remove_column(:projects, :foo) + end +end +``` + ## Testing Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct. @@ -89,4 +131,4 @@ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(i execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})") execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})") end -``` \ No newline at end of file +``` From d890807939a7b5ae9306cc347f87a1f53d46728e Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 9 May 2016 15:06:22 +0200 Subject: [PATCH 163/449] Link to migration guide in migration templates --- .../active_record/migration/create_table_migration.rb | 3 +++ lib/templates/active_record/migration/migration.rb | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/templates/active_record/migration/create_table_migration.rb b/lib/templates/active_record/migration/create_table_migration.rb index 1235ecd8e6..4b7e39e057 100644 --- a/lib/templates/active_record/migration/create_table_migration.rb +++ b/lib/templates/active_record/migration/create_table_migration.rb @@ -1,3 +1,6 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + class <%= migration_class_name %> < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/lib/templates/active_record/migration/migration.rb b/lib/templates/active_record/migration/migration.rb index 3cde953d15..ae88593cd1 100644 --- a/lib/templates/active_record/migration/migration.rb +++ b/lib/templates/active_record/migration/migration.rb @@ -1,3 +1,6 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + class <%= migration_class_name %> < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers From 443c3aa5972fda2f9899fe3618ebfb9a31f59e1f Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 11 May 2016 12:43:54 +0200 Subject: [PATCH 164/449] Tell Rubocop to ignore lib/templates --- .rubocop.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 9f179efa3c..5d56be65f1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -21,6 +21,7 @@ AllCops: - 'lib/email_validator.rb' - 'lib/gitlab/upgrader.rb' - 'lib/gitlab/seeder.rb' + - 'lib/templates/**/*' ##################### Style ################################## @@ -276,7 +277,7 @@ Style/IdenticalConditionalBranches: Enabled: false # Checks the indentation of the first line of the right-hand-side of a -# multi-line assignment. +# multi-line assignment. Style/IndentAssignment: Enabled: false From 36083b4d0ee6787da6be13b45cc7144de704c308 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 11 May 2016 14:11:04 +0200 Subject: [PATCH 165/449] Fixed styling per Rubocop pedantics --- lib/gitlab/database/migration_helpers.rb | 4 +++- spec/lib/gitlab/database/migration_helpers_spec.rb | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 7aa8c90e6d..4773a88d0c 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -119,7 +119,9 @@ module Gitlab transaction do update_column_in_batches(table, column, default) end - rescue Exception => error + # We want to rescue _all_ exceptions here, even those that don't inherit + # from StandardError. + rescue Exception => error # rubocop: disable all remove_column(table, column) raise error diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index ad2f3cb7e4..ec43165bb5 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -105,9 +105,9 @@ describe Gitlab::Database::MigrationHelpers, lib: true do expect(model).to receive(:remove_column). with(:projects, :foo) - expect { + expect do model.add_column_with_default(:projects, :foo, :integer, default: 10) - }.to raise_error(RuntimeError) + end.to raise_error(RuntimeError) end end @@ -115,9 +115,9 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'raises RuntimeError' do expect(model).to receive(:transaction_open?).and_return(true) - expect { + expect do model.add_column_with_default(:projects, :foo, :integer, default: 10) - }.to raise_error(RuntimeError) + end.to raise_error(RuntimeError) end end end From 945c5b3fe6e0552f77da8b1a1efe75cd04434f53 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 12 May 2016 15:14:14 +0200 Subject: [PATCH 166/449] Removed tracking of total method execution times Because method call timings are inclusive (that is, they include the time of any sub method calls) this would lead to the total method execution time often being far greater than the total transaction time. Because this is incredibly confusing it's best to simply _not_ track the total method execution time, after all it's not that useful to begin with. Fixes gitlab-org/gitlab-ce#17239 --- CHANGELOG | 1 + lib/gitlab/metrics/instrumentation.rb | 2 -- spec/lib/gitlab/metrics/instrumentation_spec.rb | 6 ------ 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 187011c601..985e25aab3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,7 @@ v 8.8.0 (unreleased) - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 + - Total method execution timings are no longer tracked v 8.7.5 - Fix relative links in wiki pages. !4050 diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index 708ef79f30..0f115893a1 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -154,8 +154,6 @@ module Gitlab duration = (Time.now - start) * 1000.0 if duration >= Gitlab::Metrics.method_call_threshold - trans.increment(:method_duration, duration) - trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, { duration: duration }, method: #{label.inspect}) diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 5c885a7a98..7b86450a22 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -56,9 +56,6 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:increment). - with(:method_duration, a_kind_of(Numeric)) - expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy.foo') @@ -139,9 +136,6 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:increment). - with(:method_duration, a_kind_of(Numeric)) - expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy#bar') From 5bd356eb31fb4684199f6f681f4901ebe025ed92 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 12 May 2016 09:27:58 -0500 Subject: [PATCH 167/449] Fix specs --- app/models/ci/build.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 01236b8e3c..4a3f92ad34 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -95,7 +95,7 @@ module Ci end def trace_html - trace_with_state[:html] + trace_with_state[:html] || '' end def trace_with_state(state = nil) From f4ab8ea4968b97fc2c918b85c534978aaa954c94 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 17:16:35 +0200 Subject: [PATCH 168/449] rename method --- app/services/projects/create_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index c61a515a6b..e0081e5162 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -50,7 +50,7 @@ module Projects @project.build_forked_project_link(forked_from_project_id: forked_from_project_id) end - process_import_data(import_data) + save_project_and_import_data(import_data) start_import if @project.import? @@ -100,7 +100,7 @@ module Projects end end - def process_import_data(import_data) + def save_project_and_import_data(import_data) Project.transaction do @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data @project.save From 827fbc10f6147c3c86278ed7f1675b085cc2a3cb Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 3 May 2016 13:58:50 +0100 Subject: [PATCH 169/449] Only show forks for users who can download code The ForksController enforces this, so don't show the link if it won't go anywhere. --- CHANGELOG | 1 + app/helpers/projects_helper.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 187011c601..878453e861 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,7 @@ v 8.8.0 (unreleased) - Backport GitHub Enterprise import support from EE - Create tags using Rugged for performance reasons. !3745 - API: Expose Issue#user_notes_count. !3126 (Anton Popov) + - Don't show forks button when user can't view forks - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) - Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724 diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 85f8854d2e..e1ab78df69 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -138,10 +138,10 @@ module ProjectsHelper private def get_project_nav_tabs(project, current_user) - nav_tabs = [:home, :forks] + nav_tabs = [:home] if !project.empty_repo? && can?(current_user, :download_code, project) - nav_tabs << [:files, :commits, :network, :graphs] + nav_tabs << [:files, :commits, :network, :graphs, :forks] end if project.repo_exists? && can?(current_user, :read_merge_request, project) From 7c32488dc0170547720b36b11f9d8b7f6466c309 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 17:22:43 +0200 Subject: [PATCH 170/449] added changelog --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 4841361482..f8fd6a8ee2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,9 @@ v 8.8.0 (unreleased) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 +v 8.7.5 + - Fix Bitbucket imports not finding the import data + v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) - Fix setting trusted proxies !3970 From f27d7b2c642e66a45b2b3644e955250786ae07fa Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 18:03:16 +0200 Subject: [PATCH 171/449] updated changelog --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f8fd6a8ee2..8dc52cde60 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,7 +44,7 @@ v 8.8.0 (unreleased) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 v 8.7.5 - - Fix Bitbucket imports not finding the import data + - Fix external imports not finding the import data v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) From ad77ab0376fabf3dfadea86c716358964b526956 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 12 May 2016 09:04:04 -0700 Subject: [PATCH 172/449] Add Changelog entry and drop exception handling around HealthCheck::Utils.process_checks, it wasn't needed --- CHANGELOG | 1 + app/controllers/admin/health_check_controller.rb | 6 +----- app/views/admin/health_check/show.html.haml | 12 ++++++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7d5f424eae..7ded902776 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.8.0 (unreleased) - Reduce delay in destroying a project from 1-minute to immediately - Make build status canceled if any of the jobs was canceled and none failed - Upgrade Sidekiq to 4.1.2 + - Added /health_check endpoint for checking service status - Sanitize repo paths in new project error message - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb index 3153a765e3..241c7be0ea 100644 --- a/app/controllers/admin/health_check_controller.rb +++ b/app/controllers/admin/health_check_controller.rb @@ -1,9 +1,5 @@ class Admin::HealthCheckController < Admin::ApplicationController def show - begin - @errors = HealthCheck::Utils.process_checks('standard') - rescue => e - @errors = e.message.blank? ? e.class.to_s : e.message.to_s - end + @errors = HealthCheck::Utils.process_checks('standard') end end diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index ed7025f7a0..ad79fd26d0 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -16,21 +16,21 @@ Health information can be reteived as plain text, json, or xml using: %ul %li - %code= health_check_url(token:current_application_settings.health_check_access_token) + %code= health_check_url(token: current_application_settings.health_check_access_token) %li - %code= health_check_url(token:current_application_settings.health_check_access_token, format: :json) + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json) %li - %code= health_check_url(token:current_application_settings.health_check_access_token, format: :xml) + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml) %p.light You can also ask for the status of specific services: %ul %li - %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :cache) + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache) %li - %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :database) + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database) %li - %code= health_check_url(token:current_application_settings.health_check_access_token, checks: :migrations) + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations) %hr .panel.panel-default From 0641fe6f72a3d42f94062fd00b1cf8a0d643e6a4 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 12 May 2016 18:11:03 +0200 Subject: [PATCH 173/449] Updated CHANGELOG for 8.7.5 [ci skip] --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 1d8712ae92..5dc5e05705 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -48,6 +48,8 @@ v 8.8.0 (unreleased) v 8.7.5 - Fix relative links in wiki pages. !4050 + - Fix always showing build notification message when switching between merge requests !4086 + - Fix an issue when filtering merge requests with more than one label. !3886 v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) From cacbecd05c4edc7d27c458462463f2ee65383ea5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 18:27:20 +0200 Subject: [PATCH 174/449] more changes based on MR feedback --- app/controllers/projects/imports_controller.rb | 2 +- app/services/projects/create_service.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index c70b9bf2f5..d353a39034 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -22,7 +22,7 @@ class Projects::ImportsController < Projects::ApplicationController Project.transaction do @project.import_start end - @project.add_import_job if @project.persisted? + @project.add_import_job end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index e0081e5162..2687544ae6 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -103,9 +103,8 @@ module Projects def save_project_and_import_data(import_data) Project.transaction do @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data - @project.save - if @project.persisted? && !@project.import? + if @project.save && !@project.import? raise 'Failed to create repository' unless @project.create_repository end end From 18fdbf0a035b6feec3b576c01ee1a2f3a95e4305 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Wed, 11 May 2016 23:46:19 +0900 Subject: [PATCH 175/449] Fix a description for default scope on builds --- CHANGELOG | 1 + app/views/admin/builds/index.html.haml | 2 +- app/views/projects/builds/index.html.haml | 2 +- spec/features/admin/admin_builds_spec.rb | 1 + spec/features/builds_spec.rb | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 777b211daa..8d993e3013 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,6 +51,7 @@ v 8.7.5 - Fix relative links in wiki pages. !4050 - Fix always showing build notification message when switching between merge requests !4086 - Fix an issue when filtering merge requests with more than one label. !3886 + - Fix short note for the default scope on build page (Takuya Noguchi) v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 804d7851bd..ed24757087 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -20,7 +20,7 @@ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post .row-content-block.second-block - #{(@scope || 'running').capitalize} builds + #{(@scope || 'all').capitalize} builds %ul.content-list - if @builds.blank? diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 2e8015d119..98f4a9416e 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -36,7 +36,7 @@ %span CI Lint .row-content-block - #{(@scope || 'running').capitalize} builds from this project + #{(@scope || 'all').capitalize} builds from this project %ul.content-list - if @builds.blank? diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index 2e9851fb44..7bbe20fec4 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -19,6 +19,7 @@ describe 'Admin Builds' do visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') + expect(page).to have_selector('.row-content-block', text: 'All builds') expect(page.all('.build-link').size).to eq(4) expect(page).to have_link 'Cancel all' end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 090a941958..f83a78308e 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -43,6 +43,7 @@ describe "Builds" do end it { expect(page).to have_selector('.nav-links li.active', text: 'All') } + it { expect(page).to have_selector('.row-content-block', text: 'All builds from this project') } it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } From bade0ed63e55a8168b9c2c1ddd16edbcfcd9dcd7 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 18:39:10 +0200 Subject: [PATCH 176/449] looks like 8.7.6 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8dc52cde60..1b246919a1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,7 +43,7 @@ v 8.8.0 (unreleased) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 -v 8.7.5 +v 8.7.6 - Fix external imports not finding the import data v 8.7.4 From 491a312724076f5113766b5fb55f38fee316dd38 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 18:40:50 +0200 Subject: [PATCH 177/449] fix merge conflict --- CHANGELOG | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 986f465327..5412e4c19b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,14 +47,14 @@ v 8.8.0 (unreleased) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 - Total method execution timings are no longer tracked +v 8.7.6 + - Fix external imports not finding the import data + v 8.7.5 - Fix relative links in wiki pages. !4050 - Fix always showing build notification message when switching between merge requests !4086 - Fix an issue when filtering merge requests with more than one label. !3886 -v 8.7.6 - - Fix external imports not finding the import data - v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) - Fix setting trusted proxies !3970 From fc2d985bfaa156ad052858cd2025b0300327ff95 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 12 May 2016 12:47:55 -0500 Subject: [PATCH 178/449] Fix CI tests --- app/controllers/jwt_controller.rb | 2 +- ...ntainer_registry_authentication_service.rb | 6 +- config/initializers/1_settings.rb | 2 +- lib/jwt/rsa_token.rb | 2 +- lib/jwt/token.rb | 4 +- spec/lib/jwt/rsa_token_spec.rb | 2 +- spec/lib/jwt/token_spec.rb | 2 +- ...er_registry_authentication_service_spec.rb | 56 +++++++++---------- 8 files changed, 38 insertions(+), 38 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 0048a1a31e..599f62bd12 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -3,7 +3,7 @@ class JwtController < ApplicationController skip_before_action :verify_authenticity_token SERVICES = { - 'container_registry' => Jwt::ContainerRegistryAuthenticationService, + 'container_registry' => JWT::ContainerRegistryAuthenticationService, } def auth diff --git a/app/services/jwt/container_registry_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb index b9fcd38047..0ab3e6d02b 100644 --- a/app/services/jwt/container_registry_authentication_service.rb +++ b/app/services/jwt/container_registry_authentication_service.rb @@ -1,11 +1,11 @@ -module Jwt +module JWT class ContainerRegistryAuthenticationService < BaseService def execute if params[:offline_token] return error('forbidden', 403) unless current_user end - return error('forbidden', 401) if scopes.empty? + return error('forbidden', 401) if scopes.blank? { token: authorized_token(scopes).encoded } end @@ -13,7 +13,7 @@ module Jwt private def authorized_token(access) - token = ::Jwt::RSAToken.new(registry.key) + token = ::JWT::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = params[:service] token.subject = current_user.try(:username) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 1040d840e3..3853845fee 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -271,7 +271,7 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes # Registry # Settings['registry'] ||= Settingslogic.new({}) -Settings.registry['registry'] = false if Settings.registry['enabled'].nil? +Settings.registry['enabled'] = false if Settings.registry['enabled'].nil? Settings.registry['host'] ||= "example.com" Settings.registry['internal_host']||= "localhost" Settings.registry['key'] ||= nil diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb index cc265e3b31..0438135ad5 100644 --- a/lib/jwt/rsa_token.rb +++ b/lib/jwt/rsa_token.rb @@ -1,4 +1,4 @@ -module Jwt +module JWT class RSAToken < Token attr_reader :key_file diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb index 765ab0d60c..f13abf2b71 100644 --- a/lib/jwt/token.rb +++ b/lib/jwt/token.rb @@ -1,4 +1,4 @@ -module Jwt +module JWT class Token attr_accessor :issuer, :subject, :audience, :id attr_accessor :issued_at, :not_before, :expire_time @@ -43,4 +43,4 @@ module Jwt }.compact end end -end \ No newline at end of file +end diff --git a/spec/lib/jwt/rsa_token_spec.rb b/spec/lib/jwt/rsa_token_spec.rb index 710801923e..a5b1d3a67d 100644 --- a/spec/lib/jwt/rsa_token_spec.rb +++ b/spec/lib/jwt/rsa_token_spec.rb @@ -1,4 +1,4 @@ -describe Jwt::RSAToken do +describe JWT::RSAToken do let(:rsa_key) { generate_key } let(:rsa_token) { described_class.new(nil) } let(:rsa_encoded) { rsa_token.encoded } diff --git a/spec/lib/jwt/token_spec.rb b/spec/lib/jwt/token_spec.rb index a56b4cf39b..92fdc3f1b7 100644 --- a/spec/lib/jwt/token_spec.rb +++ b/spec/lib/jwt/token_spec.rb @@ -1,4 +1,4 @@ -describe Jwt::Token do +describe JWT::Token do let(:token) { described_class.new } context 'custom parameters' do diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/jwt/container_registry_authentication_service_spec.rb index ea91f499d0..1873ea2639 100644 --- a/spec/services/jwt/container_registry_authentication_service_spec.rb +++ b/spec/services/jwt/container_registry_authentication_service_spec.rb @@ -1,23 +1,23 @@ require 'spec_helper' -describe Jwt::ContainerRegistryAuthenticationService, services: true do +describe JWT::ContainerRegistryAuthenticationService, services: true do let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } - let(:registry_settings) { + let(:registry_settings) do { issuer: 'rspec', key: nil } - } + end let(:payload) { JWT.decode(subject[:token], rsa_key).first } subject { described_class.new(current_project, current_user, current_params).execute } before do allow(Gitlab.config.registry).to receive_messages(registry_settings) - allow_any_instance_of(Jwt::RSAToken).to receive(:key).and_return(rsa_key) + allow_any_instance_of(JWT::RSAToken).to receive(:key).and_return(rsa_key) end shared_examples 'an authenticated' do @@ -26,13 +26,13 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do end shared_examples 'a accessible' do - let(:access) { + let(:access) do [{ 'type' => 'repository', 'name' => project.path_with_namespace, 'actions' => actions, }] - } + end it_behaves_like 'an authenticated' it { expect(payload).to include('access' => access) } @@ -68,9 +68,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'allow developer to push images' do before { project.team << [current_user, :developer] } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push" } - } + end it_behaves_like 'a pushable' end @@ -78,9 +78,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'allow reporter to pull images' do before { project.team << [current_user, :reporter] } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull" } - } + end it_behaves_like 'a pullable' end @@ -88,9 +88,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'return a least of privileges' do before { project.team << [current_user, :reporter] } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push,pull" } - } + end it_behaves_like 'a pullable' end @@ -98,9 +98,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'disallow guest to pull or push images' do before { project.team << [current_user, :guest] } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull,push" } - } + end it_behaves_like 'a forbidden' end @@ -110,9 +110,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do let(:current_project) { create(:empty_project) } context 'allow to pull and push images' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{current_project.path_with_namespace}:pull,push" } - } + end it_behaves_like 'a pullable and pushable' do let(:project) { current_project } @@ -121,9 +121,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'for other projects' do context 'when pulling' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull" } - } + end context 'allow for public' do let(:project) { create(:empty_project, :public) } @@ -137,9 +137,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do end context 'when pushing' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push" } - } + end context 'disallow for all' do let(:project) { create(:empty_project, :public) } @@ -152,9 +152,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'unauthorized' do context 'for invalid scope' do - let(:current_params) { + let(:current_params) do { scope: 'invalid:aa:bb' } - } + end it_behaves_like 'a forbidden' end @@ -162,9 +162,9 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do context 'for private project' do let(:project) { create(:empty_project, :private) } - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull" } - } + end it_behaves_like 'a forbidden' end @@ -173,17 +173,17 @@ describe Jwt::ContainerRegistryAuthenticationService, services: true do let(:project) { create(:empty_project, :public) } context 'when pulling and pushing' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull,push" } - } + end it_behaves_like 'a pullable' end context 'when pushing' do - let(:current_params) { + let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push" } - } + end it_behaves_like 'a forbidden' end From 5c2f2fd2890b7efd7a63f9a371b2f795f2e9fa43 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 12 May 2016 13:03:04 -0500 Subject: [PATCH 179/449] Fix CI tests --- .../projects/container_registry_controller.rb | 1 - app/helpers/gitlab_routing_helper.rb | 2 +- app/models/ability.rb | 2 +- app/models/namespace.rb | 2 +- app/models/project.rb | 4 ++-- .../jwt/container_registry_authentication_service.rb | 2 +- app/services/projects/destroy_service.rb | 4 +++- app/services/projects/transfer_service.rb | 2 +- app/views/projects/container_registry/_tag.html.haml | 9 +++++---- .../projects/container_registry/index.html.haml | 5 +++-- lib/container_registry/client.rb | 2 +- lib/container_registry/repository.rb | 4 +++- spec/features/container_registry_spec.rb | 12 +++++++++--- 13 files changed, 31 insertions(+), 20 deletions(-) diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index 82c621b29e..c470789a5b 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -1,7 +1,6 @@ class Projects::ContainerRegistryController < Projects::ApplicationController before_action :authorize_read_container_registry! before_action :authorize_update_container_registry!, only: [:destroy] - before_action :tag, except: [:index] layout 'project' def index diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index dd9536b146..2ce2d4e694 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -34,7 +34,7 @@ module GitlabRoutingHelper end def project_container_registry_path(project, *args) - namespace_project_container_registry_index_url(project.namespace, project, *args) + namespace_project_container_registry_index_path(project.namespace, project, *args) end def activity_project_path(project, *args) diff --git a/app/models/ability.rb b/app/models/ability.rb index 59d5195f5b..2465c1f424 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -291,7 +291,7 @@ class Ability rules += named_abilities('build') end - unless project.container_registry_enabled + unless project.container_registry_enabled && Gitlab.config.registry.enabled rules += named_abilities('container_registry') end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 0f61cee788..62ef6de5db 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -128,7 +128,7 @@ class Namespace < ActiveRecord::Base gitlab_shell.add_namespace(path_was) if any_project_has_container_registry_tags? - raise Exception.new('namespace cannot be moved, because at least one project has tags in container registry') + raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') end if gitlab_shell.mv_namespace(path_was, path) diff --git a/app/models/project.rb b/app/models/project.rb index d3ae7803be..e5ace7d755 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -377,7 +377,7 @@ class Project < ActiveRecord::Base def container_registry_repository @container_registry_repository ||= begin - token = Jwt::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace) + token = JWT::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace) url = Gitlab.config.registry.api_url host_port = Gitlab.config.registry.host_port registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) @@ -814,7 +814,7 @@ class Project < ActiveRecord::Base if has_container_registry_tags? # we currently doesn't support renaming repository if it contains tags in container registry - raise Exception.new('repository cannot be renamed, due to tags in container registry') + raise Exception.new('Project cannot be renamed, because tags are present in its container registry') end if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) diff --git a/app/services/jwt/container_registry_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb index bc7e663caa..91bad34727 100644 --- a/app/services/jwt/container_registry_authentication_service.rb +++ b/app/services/jwt/container_registry_authentication_service.rb @@ -14,7 +14,7 @@ module JWT def self.full_access_token(*names) registry = Gitlab.config.registry - token = ::Jwt::RSAToken.new(registry.key) + token = ::JWT::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = AUDIENCE token[:access] = names.map do |name| diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 8e2e46346c..0ff2bc3cb8 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -64,7 +64,9 @@ module Projects end def remove_registry_tags - project.image_registry.delete_tags + return unless Gitlab.config.registry.enabled + + project.container_registry_repository.delete_tags end def raise_error(message) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 0d72286dec..03b57dea51 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -36,7 +36,7 @@ module Projects if project.has_container_registry_tags? # we currently doesn't support renaming repository if it contains tags in container registry - raise TransferError.new('Repository cannot be renamed, due to tags in container registry') + raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') end project.expire_caches_before_rename(old_path) diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index f97988d20c..bf816d109b 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -14,7 +14,8 @@ = pluralize(tag.layers.size, "layer") %td = time_ago_in_words(tag.created_at) - %td.content - .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_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") + - if can?(current_user, :update_container_registry, @project) + %td.content + .controls.hidden-xs.pull-right + = link_to namespace_project_container_registry_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/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index 701f1a86b5..ea7c4faaae 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -18,7 +18,7 @@ %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: + Then you are free to create and upload a container image with build and push commands: %pre docker build -t #{escape_once(@project.container_registry_repository_url)} . %br @@ -33,7 +33,8 @@ %th Image ID %th Size %th Created - %th + - if can?(current_user, :update_container_registry, @project) + %th - @tags.each do |tag| = render 'tag', tag: tag \ No newline at end of file diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index 0bfb6baffd..c250a4b694 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -55,7 +55,7 @@ module ContainerRegistry conn.request :json conn.headers['Accept'] = MANIFEST_VERSION - conn.response :json, :content_type => /\bjson$/ + conn.response :json, content_type: /\bjson$/ if options[:user] && options[:password] conn.request(:basic_auth, options[:user].to_s, options[:password].to_s) diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb index f01330f364..b30cb527b6 100644 --- a/lib/container_registry/repository.rb +++ b/lib/container_registry/repository.rb @@ -30,19 +30,21 @@ module ContainerRegistry def tags return @tags if defined?(@tags) return [] unless manifest && manifest['tags'] + @tags = manifest['tags'].map do |tag| ContainerRegistry::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 diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index 7bef7a2ee8..6c4d675fd6 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -5,14 +5,18 @@ describe "Container Registry" do let(:repository) { project.container_registry_repository } let(:tag_name) { 'latest' } let(:tags) { [tag_name] } - - before do + let(:registry_settings) do + { + enabled: true + } end before do login_as(:user) project.team << [@user, :developer] stub_container_registry(*tags) + allow(Gitlab.config.registry).to receive_messages(registry_settings) + allow(JWT::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') end describe 'GET /:project/container_registry' do @@ -22,6 +26,7 @@ describe "Container Registry" do context 'when no tags' do let(:tags) { [] } + it { expect(page).to have_content('No images in Container Registry for this project') } end @@ -37,7 +42,8 @@ describe "Container Registry" do it do expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true) + click_on 'Remove' end end -end \ No newline at end of file +end From 0d43b9270677106f3aa37112da1dd0482ed40b55 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 12 May 2016 13:08:18 -0500 Subject: [PATCH 180/449] Fix CI tests --- app/controllers/projects/pipelines_controller.rb | 8 +++++--- app/models/project.rb | 2 +- app/services/ci/create_pipeline_service.rb | 2 +- app/views/projects/commit/_ci_stage.html.haml | 5 +++-- app/views/projects/pipelines/index.html.haml | 4 ++-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index a261d03f4d..bdb5722c55 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -7,8 +7,10 @@ class Projects::PipelinesController < Projects::ApplicationController def index @scope = params[:scope] - @all_pipelines = project.ci_commits - @pipelines = PipelinesFinder.new(project).execute(@all_pipelines, @scope) + all_pipelines = project.ci_commits + @pipelines_count = all_pipelines.count + @running_or_pending_count = all_pipelines.running_or_pending.count + @pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope) @pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30) end @@ -22,7 +24,7 @@ class Projects::PipelinesController < Projects::ApplicationController rescue ArgumentError => e @error = e.message render 'new' - rescue => e + rescue @error = 'Undefined error' render 'new' end diff --git a/app/models/project.rb b/app/models/project.rb index dfd1e54ecf..82489235a3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -354,7 +354,7 @@ class Project < ActiveRecord::Base join_body = "INNER JOIN ( SELECT project_id, COUNT(*) AS amount FROM notes - WHERE created_at >= #{sanitize(since)} + WHERE created_at >= #{sanitize(since)}project.ci_commits GROUP BY project_id ) join_note_counts ON projects.id = join_note_counts.project_id" diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 40414b4986..223514968f 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -39,4 +39,4 @@ module Ci @pipeline ||= project.ci_commits.new(sha: commit.id, ref: params[:ref], before_sha: Gitlab::Git::BLANK_SHA) end end -end \ No newline at end of file +end diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml index f9f2757dc3..bdadf2944c 100644 --- a/app/views/projects/commit/_ci_stage.html.haml +++ b/app/views/projects/commit/_ci_stage.html.haml @@ -6,8 +6,9 @@ - status = latest.status %span{class: "ci-status-link ci-status-icon-#{status}"} = ci_icon_for_status(status) -   - = stage.titleize.pluralize + - if stage +   + = stage.titleize.pluralize = render latest.ordered, coverage: @project.build_coverage_enabled?, tage: false, ref: false, allow_retry: true = render retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true %tr diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index d247ea445e..574941ed5f 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -7,13 +7,13 @@ = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count - = number_with_delimiter(@all_pipelines.count) + = number_with_delimiter(@pipelines_count) %li{class: ('active' if @scope == 'running')} = link_to project_pipelines_path(@project, scope: :running) do Running %span.badge.js-running-count - = number_with_delimiter(@all_pipelines.running_or_pending.count) + = number_with_delimiter(@running_or_pending_count) %li{class: ('active' if @scope == 'branches')} = link_to project_pipelines_path(@project, scope: :branches) do From 0c22698bd4dbe7d0d3e4a6c8bc946ac6f5de1c12 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Thu, 12 May 2016 22:48:09 +0200 Subject: [PATCH 181/449] Add API endpoints for un/subscribing from/to a label Closes #15638 --- CHANGELOG | 1 + app/models/concerns/subscribable.rb | 6 ++ doc/api/labels.md | 70 +++++++++++++++++++ lib/api/api.rb | 1 + lib/api/entities.rb | 4 ++ lib/api/helpers.rb | 11 +++ lib/api/issues.rb | 39 +---------- lib/api/labels.rb | 6 +- lib/api/merge_requests.rb | 36 ---------- lib/api/subscriptions.rb | 60 +++++++++++++++++ spec/models/concerns/subscribable_spec.rb | 10 +++ spec/requests/api/issues_spec.rb | 12 ++++ spec/requests/api/labels_spec.rb | 82 +++++++++++++++++++++++ 13 files changed, 261 insertions(+), 77 deletions(-) create mode 100644 lib/api/subscriptions.rb diff --git a/CHANGELOG b/CHANGELOG index efb6dc6f61..aedd9f8ebd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,6 +47,7 @@ v 8.8.0 (unreleased) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 - Total method execution timings are no longer tracked - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034. (Andrei Gliga) + - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) v 8.7.5 - Fix relative links in wiki pages. !4050 diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index d5a881b244..083257f100 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -36,6 +36,12 @@ module Subscribable update(subscribed: !subscribed?(user)) end + def subscribe(user) + subscriptions. + find_or_initialize_by(user_id: user.id). + update(subscribed: true) + end + def unsubscribe(user) subscriptions. find_or_initialize_by(user_id: user.id). diff --git a/doc/api/labels.md b/doc/api/labels.md index 3730c07c5a..b857d81768 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -165,3 +165,73 @@ Example response: "description": "Documentation" } ``` + +## Subscribe to a label + +Subscribes the authenticated user to a label to receive notifications. If the +operation is successful, status code `201` together with the updated label is +returned. If the user is already subscribed to the label, the status code `304` +is returned. If the project or label is not found, status code `404` is +returned. + +``` +POST /projects/:id/labels/:label_id/subscription +``` + +| Attribute | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------ | +| `id` | integer | yes | The ID of a project | +| `label_id` | integer or string | yes | The ID or title of a project's label | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +``` + +Example response: + +```json +{ + "name": "Docs", + "color": "#cc0033", + "description": "", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": true +} +``` + +## Unsubscribe from a label + +Unsubscribes the authenticated user from a label to not receive notifications +from it. If the operation is successful, status code `200` together with the +updated label is returned. If the user is not subscribed to the label, the +status code `304` is returned. If the project or label is not found, status code +`404` is returned. + +``` +DELETE /projects/:id/labels/:label_id/subscription +``` + +| Attribute | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------ | +| `id` | integer | yes | The ID of a project | +| `label_id` | integer or string | yes | The ID or title of a project's label | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +``` + +Example response: + +```json +{ + "name": "Docs", + "color": "#cc0033", + "description": "", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false +} +``` diff --git a/lib/api/api.rb b/lib/api/api.rb index 5fd9c30cb4..360fb41a72 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -57,5 +57,6 @@ module API mount ::API::Variables mount ::API::Runners mount ::API::Licenses + mount ::API::Subscriptions end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 2870a6a40e..406f5ea913 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -307,6 +307,10 @@ module API class Label < Grape::Entity expose :name, :color, :description expose :open_issues_count, :closed_issues_count, :open_merge_requests_count + + expose :subscribed do |label, options| + label.subscribed?(options[:current_user]) + end end class Compare < Grape::Entity diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 40c967453f..5e638dbe16 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -95,6 +95,17 @@ module API end end + def find_project_label(id) + label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id) + label || not_found!('Label') + end + + def find_project_issue(id) + issue = user_project.issues.find(id) + not_found! unless can?(current_user, :read_issue, issue) + issue + end + def paginate(relation) relation.page(params[:page]).per(params[:per_page].to_i).tap do |data| add_pagination_headers(data) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 4092874948..f59a4d6c01 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -103,8 +103,7 @@ module API # Example Request: # GET /projects/:id/issues/:issue_id get ":id/issues/:issue_id" do - @issue = user_project.issues.find(params[:issue_id]) - not_found! unless can?(current_user, :read_issue, @issue) + @issue = find_project_issue(params[:issue_id]) present @issue, with: Entities::Issue, current_user: current_user end @@ -234,42 +233,6 @@ module API authorize!(:destroy_issue, issue) issue.destroy end - - # Subscribes to a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # POST /projects/:id/issues/:issue_id/subscription - post ':id/issues/:issue_id/subscription' do - issue = user_project.issues.find(params[:issue_id]) - - if issue.subscribed?(current_user) - not_modified! - else - issue.toggle_subscription(current_user) - present issue, with: Entities::Issue, current_user: current_user - end - end - - # Unsubscribes from a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # DELETE /projects/:id/issues/:issue_id/subscription - delete ':id/issues/:issue_id/subscription' do - issue = user_project.issues.find(params[:issue_id]) - - if issue.subscribed?(current_user) - issue.unsubscribe(current_user) - present issue, with: Entities::Issue, current_user: current_user - else - not_modified! - end - end end end end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 4af6bef0fa..c806829d69 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -11,7 +11,7 @@ module API # Example Request: # GET /projects/:id/labels get ':id/labels' do - present user_project.labels, with: Entities::Label + present user_project.labels, with: Entities::Label, current_user: current_user end # Creates a new label @@ -36,7 +36,7 @@ module API label = user_project.labels.create(attrs) if label.valid? - present label, with: Entities::Label + present label, with: Entities::Label, current_user: current_user else render_validation_error!(label) end @@ -90,7 +90,7 @@ module API attrs[:name] = attrs.delete(:new_name) if attrs.key?(:new_name) if label.update(attrs) - present label, with: Entities::Label + present label, with: Entities::Label, current_user: current_user else render_validation_error!(label) end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 7e78609ecb..4e7de8867b 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -327,42 +327,6 @@ module API issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: Entities::Issue, current_user: current_user end - - # Subscribes to a merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of a merge request - # Example Request: - # POST /projects/:id/issues/:merge_request_id/subscription - post "#{path}/subscription" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - if merge_request.subscribed?(current_user) - not_modified! - else - merge_request.toggle_subscription(current_user) - present merge_request, with: Entities::MergeRequest, current_user: current_user - end - end - - # Unsubscribes from a merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of a merge request - # Example Request: - # DELETE /projects/:id/merge_requests/:merge_request_id/subscription - delete "#{path}/subscription" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - if merge_request.subscribed?(current_user) - merge_request.unsubscribe(current_user) - present merge_request, with: Entities::MergeRequest, current_user: current_user - else - not_modified! - end - end end end end diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb new file mode 100644 index 0000000000..c49e2a21b8 --- /dev/null +++ b/lib/api/subscriptions.rb @@ -0,0 +1,60 @@ +module API + class Subscriptions < Grape::API + before { authenticate! } + + subscribable_types = { + 'merge_request' => proc { |id| user_project.merge_requests.find(id) }, + 'merge_requests' => proc { |id| user_project.merge_requests.find(id) }, + 'issues' => proc { |id| find_project_issue(id) }, + 'labels' => proc { |id| find_project_label(id) }, + } + + resource :projects do + subscribable_types.each do |type, finder| + type_singularized = type.singularize + type_id_str = :"#{type_singularized}_id" + entity_class = Entities.const_get(type_singularized.camelcase) + + # Subscribe to a resource + # + # Parameters: + # id (required) - The ID of a project + # subscribable_id (required) - The ID of a resource + # Example Request: + # POST /projects/:id/labels/:subscribable_id/subscription + # POST /projects/:id/issues/:subscribable_id/subscription + # POST /projects/:id/merge_requests/:subscribable_id/subscription + post ":id/#{type}/:#{type_id_str}/subscription" do + resource = instance_exec(params[type_id_str], &finder) + + if resource.subscribed?(current_user) + not_modified! + else + resource.subscribe(current_user) + present resource, with: entity_class, current_user: current_user + end + end + + # Unsubscribe from a resource + # + # Parameters: + # id (required) - The ID of a project + # subscribable_id (required) - The ID of a resource + # Example Request: + # DELETE /projects/:id/labels/:subscribable_id/subscription + # DELETE /projects/:id/issues/:subscribable_id/subscription + # DELETE /projects/:id/merge_requests/:subscribable_id/subscription + delete ":id/#{type}/:#{type_id_str}/subscription" do + resource = instance_exec(params[type_id_str], &finder) + + if !resource.subscribed?(current_user) + not_modified! + else + resource.unsubscribe(current_user) + present resource, with: entity_class, current_user: current_user + end + end + end + end + end +end diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb index e31fdb0bff..b7fc5a9249 100644 --- a/spec/models/concerns/subscribable_spec.rb +++ b/spec/models/concerns/subscribable_spec.rb @@ -44,6 +44,16 @@ describe Subscribable, 'Subscribable' do end end + describe '#subscribe' do + it 'subscribes the given user' do + expect(resource.subscribed?(user)).to be_falsey + + resource.subscribe(user) + + expect(resource.subscribed?(user)).to be_truthy + end + end + describe '#unsubscribe' do it 'unsubscribes the given current user' do resource.subscriptions.create(user: user, subscribed: true) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9dd43f4fab..37ab9cc8cf 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -623,6 +623,12 @@ describe API::API, api: true do expect(response.status).to eq(404) end + + it 'returns 404 if the issue is confidential' do + post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response.status).to eq(404) + end end describe 'DELETE :id/issues/:issue_id/subscription' do @@ -644,5 +650,11 @@ describe API::API, api: true do expect(response.status).to eq(404) end + + it 'returns 404 if the issue is confidential' do + delete api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response.status).to eq(404) + end end end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 6943ff9d26..b2c7f8d9ac 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -190,4 +190,86 @@ describe API::API, api: true do expect(json_response['message']['color']).to eq(['must be a valid color code']) end end + + describe "POST /projects/:id/labels/:label_id/subscription" do + context "when label_id is a label title" do + it "should subscribe to the label" do + post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + + expect(response.status).to eq(201) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_truthy + end + end + + context "when label_id is a label ID" do + it "should subscribe to the label" do + post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(201) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_truthy + end + end + + context "when user is already subscribed to label" do + before { label1.subscribe(user) } + + it "should return 304" do + post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(304) + end + end + + context "when label ID is not found" do + it "should a return 404 error" do + post api("/projects/#{project.id}/labels/1234/subscription", user) + + expect(response.status).to eq(404) + end + end + end + + describe "DELETE /projects/:id/labels/:label_id/subscription" do + before { label1.subscribe(user) } + + context "when label_id is a label title" do + it "should unsubscribe from the label" do + delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_falsey + end + end + + context "when label_id is a label ID" do + it "should unsubscribe from the label" do + delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_falsey + end + end + + context "when user is already unsubscribed from label" do + before { label1.unsubscribe(user) } + + it "should return 304" do + delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(304) + end + end + + context "when label ID is not found" do + it "should a return 404 error" do + delete api("/projects/#{project.id}/labels/1234/subscription", user) + + expect(response.status).to eq(404) + end + end + end end From e4d3e949555c51cf54ead163bf001b4ec86a2733 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 12 May 2016 16:22:21 -0500 Subject: [PATCH 182/449] Move broadcast msg into content-wrapper --- app/views/layouts/_page.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 3c3bc41bf0..3c0660d34f 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,5 +1,4 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } - = render "layouts/broadcast" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo %a#logo @@ -27,6 +26,7 @@ .container-fluid = render "layouts/nav/#{nav}" .content-wrapper{ class: ('page-with-layout-nav' if defined?(nav) && nav) } + = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message %div{ class: (container_class unless @no_container) } From 05bbad5817c567b547177441769643042d699d8a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 11 May 2016 15:17:16 -0500 Subject: [PATCH 183/449] Clean up EventsHelper spec --- spec/helpers/events_helper_spec.rb | 95 +++++++++++++++--------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index e68a5ec29a..c0d2be98e8 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -1,64 +1,65 @@ require 'spec_helper' describe EventsHelper do - include ApplicationHelper - include GitlabMarkdownHelper + describe '#event_note' do + before do + allow(helper).to receive(:current_user).and_return(double) + end - let(:current_user) { create(:user, email: "current@email.com") } + it 'should display one line of plain text without alteration' do + input = 'A short, plain note' + expect(helper.event_note(input)).to match(input) + expect(helper.event_note(input)).not_to match(/\.\.\.\z/) + end - it 'should display one line of plain text without alteration' do - input = 'A short, plain note' - expect(event_note(input)).to match(input) - expect(event_note(input)).not_to match(/\.\.\.\z/) - end + it 'should display inline code' do + input = 'A note with `inline code`' + expected = 'A note with inline code' - it 'should display inline code' do - input = 'A note with `inline code`' - expected = 'A note with inline code' + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should truncate a note with multiple paragraphs' do + input = "Paragraph 1\n\nParagraph 2" + expected = 'Paragraph 1...' - it 'should truncate a note with multiple paragraphs' do - input = "Paragraph 1\n\nParagraph 2" - expected = 'Paragraph 1...' + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should display the first line of a code block' do + input = "```\nCode block\nwith two lines\n```" + expected = %r{Code block\.\.\.} - it 'should display the first line of a code block' do - input = "```\nCode block\nwith two lines\n```" - expected = %r{Code block\.\.\.} + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should truncate a single long line of text' do + text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars + input = text * 4 + expected = (text * 2).sub(/.{3}/, '...') - it 'should truncate a single long line of text' do - text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars - input = "#{text}#{text}#{text}#{text}" # 200 chars - expected = "#{text}#{text}".sub(/.{3}/, '...') + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should preserve a link href when link text is truncated' do + text = 'The quick brown fox jumped over the lazy dog' # 44 chars + input = "#{text}#{text}#{text} " # 133 chars + link_url = 'http://example.com/foo/bar/baz' # 30 chars + input << link_url + expected_link_text = 'http://example...' - it 'should preserve a link href when link text is truncated' do - text = 'The quick brown fox jumped over the lazy dog' # 44 chars - input = "#{text}#{text}#{text} " # 133 chars - link_url = 'http://example.com/foo/bar/baz' # 30 chars - input << link_url - expected_link_text = 'http://example...' + expect(helper.event_note(input)).to match(link_url) + expect(helper.event_note(input)).to match(expected_link_text) + end - expect(event_note(input)).to match(link_url) - expect(event_note(input)).to match(expected_link_text) - end - - it 'should preserve code color scheme' do - input = "```ruby\ndef test\n 'hello world'\nend\n```" - expected = '

    ' \
    -      "def test\n" \
    -      "  \'hello world\'\n" \
    -      "end" \
    -      '
    ' - expect(event_note(input)).to eq(expected) + it 'should preserve code color scheme' do + input = "```ruby\ndef test\n 'hello world'\nend\n```" + expected = '
    ' \
    +        "def test\n" \
    +        "  \'hello world\'\n" \
    +        "end" \
    +        '
    ' + expect(helper.event_note(input)).to eq(expected) + end end end From 7bc594735b7923d1e57eb504f09716bbb72c8c06 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 12 May 2016 17:06:47 -0500 Subject: [PATCH 184/449] Undo changes; reduce sidebar internal width --- app/assets/stylesheets/framework/sidebar.scss | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index c243dd8a42..f90d7a806d 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -63,7 +63,7 @@ float: left; height: $header-height; width: 100%; - padding-left: 16px; + padding-left: 22px; overflow: hidden; outline: none; transition-duration: .3s; @@ -78,7 +78,7 @@ } .gitlab-text-container { - width: 220px; + width: 230px; h3 { width: 158px; @@ -146,12 +146,12 @@ a { padding: 7px 15px; - transition-duration: .3s; font-size: $gl-font-size; line-height: 24px; color: $gray; display: block; text-decoration: none; + padding-left: 23px; font-weight: normal; outline: none; @@ -253,11 +253,6 @@ width: auto; a { - @media (min-width: $screen-xs-min) { - padding-left: 23px; - transition-duration: .3s; - } - span { display: none; } @@ -317,7 +312,7 @@ } .nav-sidebar li a { - width: 220px; + width: $sidebar_width; &.back-link { i { From 6a29117793934850986a9e423c86746b4a0763e9 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 13 May 2016 00:11:01 +0200 Subject: [PATCH 185/449] Removed stray strip_heredoc from migration helpers --- lib/gitlab/database/migration_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 4773a88d0c..74c87fa290 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -63,7 +63,7 @@ module Gitlab FROM #{quoted_table} ORDER BY id ASC LIMIT 1 OFFSET #{processed + batch_size} - }.strip_heredoc).to_hash.first + }).to_hash.first query = %Q{ UPDATE #{quoted_table} From 4fea5cda0bf7ff56dafae90306441b0d65f3ca05 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 12 May 2016 17:11:37 -0500 Subject: [PATCH 186/449] Fix minor typos in admin health check page --- app/views/admin/health_check/show.html.haml | 2 +- spec/features/admin/admin_health_check_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index ad79fd26d0..c2313986a7 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -13,7 +13,7 @@ = icon('refresh') Reset health check access token %p.light - Health information can be reteived as plain text, json, or xml using: + Health information can be retrieved as plain text, JSON, or XML using: %ul %li %code= health_check_url(token: current_application_settings.health_check_access_token) diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 4fde04b609..dec2dedf2b 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -13,7 +13,7 @@ feature "Admin Health Check", feature: true do end it { page.has_text? 'Health Check' } - it { page.has_text? 'Health information can be reteived' } + it { page.has_text? 'Health information can be retrieved' } it 'has a health check access token' do token = current_application_settings.health_check_access_token From 311fb58c44fe6dd14709c8eb8f9346ec73e03aff Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 10 May 2016 23:24:23 -0300 Subject: [PATCH 187/449] Require space around operators --- .rubocop.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 3fb2f2df59..562197300b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -276,7 +276,7 @@ Style/IdenticalConditionalBranches: Enabled: false # Checks the indentation of the first line of the right-hand-side of a -# multi-line assignment. +# multi-line assignment. Style/IndentAssignment: Enabled: false @@ -531,7 +531,7 @@ Style/SpaceAroundKeyword: # Use a single space around operators. Style/SpaceAroundOperators: - Enabled: false + Enabled: true # Checks that the left block brace has or doesn't have space before it. Style/SpaceBeforeBlockBraces: From f5a0ac0fc197bae2eb5fe1045ed237cdbbaf6ea4 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 10 May 2016 23:58:06 -0300 Subject: [PATCH 188/449] Codestyle: make sure we have space around operators --- app/controllers/application_controller.rb | 2 +- app/controllers/concerns/creates_commit.rb | 2 +- app/helpers/search_helper.rb | 2 +- app/helpers/selects_helper.rb | 2 +- .../project_services/external_wiki_service.rb | 2 +- app/models/project_wiki.rb | 2 +- app/models/repository.rb | 4 ++-- app/services/merge_requests/build_service.rb | 2 +- app/services/system_hooks_service.rb | 2 +- config/environments/test.rb | 2 +- config/initializers/1_settings.rb | 18 ++++++++--------- config/initializers/carrierwave.rb | 2 +- config/initializers/devise.rb | 2 +- config/routes.rb | 2 +- features/steps/dashboard/issues.rb | 2 +- features/steps/dashboard/merge_requests.rb | 2 +- features/steps/profile/profile.rb | 2 +- features/steps/project/team_management.rb | 2 +- lib/api/helpers.rb | 2 +- lib/gitlab/email/reply_parser.rb | 2 +- lib/gitlab/redis.rb | 6 +++--- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 4 ++-- .../ci/build/artifacts/metadata/entry_spec.rb | 4 ++-- spec/lib/gitlab/lfs/lfs_router_spec.rb | 4 ++-- spec/models/commit_spec.rb | 2 +- spec/models/hooks/service_hook_spec.rb | 4 ++-- spec/models/hooks/system_hook_spec.rb | 20 +++++++++---------- .../slack_service/note_message_spec.rb | 4 ++-- spec/requests/api/builds_spec.rb | 4 ++-- spec/requests/api/group_members_spec.rb | 10 +++++----- spec/requests/ci/api/builds_spec.rb | 4 ++-- spec/support/jira_service_helper.rb | 10 +++++----- spec/support/stub_gitlab_calls.rb | 20 +++++++++---------- 33 files changed, 77 insertions(+), 77 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 17b3f49aed..1429ee40bb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -176,7 +176,7 @@ class ApplicationController < ActionController::Base end def check_password_expiration - if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? + if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? redirect_to new_profile_password_path and return end end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 787416c17a..dacb5679dd 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -122,7 +122,7 @@ module CreatesCommit # Merge request from fork to this project @mr_source_project = @tree_edit_project @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch ||= @ref end end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 24c4c098c6..d2f94d4ae6 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -59,7 +59,7 @@ module SearchHelper # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - ref = @ref || @project.repository.root_ref + ref = @ref || @project.repository.root_ref [ { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index e951a87a21..bb395e3788 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -18,7 +18,7 @@ module SelectsHelper first_user: first_user, current_user: opts[:current_user] || false, "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], - author_id: opts[:author_id] || '' + author_id: opts[:author_id] || '' } } diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index f042bfc521..d7b6e50519 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -25,7 +25,7 @@ class ExternalWikiService < Service def execute(_data) @response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil - if @response !=200 + if @response != 200 nil end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 7c1a61bb0b..060ed9b44e 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -113,7 +113,7 @@ class ProjectWiki end def page_title_and_dir(title) - title_array = title.split("/") + title_array = title.split("/") title = title_array.pop [title, title_array.join("/")] end diff --git a/app/models/repository.rb b/app/models/repository.rb index de7e163078..0eff74320f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -795,7 +795,7 @@ class Repository def check_revert_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] - args << { mainline: 1 } if commit.merge_commit? + args << { mainline: 1 } if commit.merge_commit? revert_index = rugged.revert_commit(*args) return false if revert_index.conflicts? @@ -809,7 +809,7 @@ class Repository def check_cherry_pick_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] - args << 1 if commit.merge_commit? + args << 1 if commit.merge_commit? cherry_pick_index = rugged.cherrypick_commit(*args) return false if cherry_pick_index.conflicts? diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index cd4230aa5e..1b48899bb0 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -66,7 +66,7 @@ module MergeRequests commits = merge_request.compare_commits if commits && commits.count == 1 commit = commits.first - merge_request.title = commit.title + merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) case issue diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index e43b5b51e5..1fb72cf89e 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -85,7 +85,7 @@ class SystemHooksService path_with_namespace: model.path_with_namespace, project_id: model.id, owner_name: owner.name, - owner_email: owner.respond_to?(:email) ? owner.email : "", + owner_email: owner.respond_to?(:email) ? owner.email : "", project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase } end diff --git a/config/environments/test.rb b/config/environments/test.rb index a703c0934f..fb25d3a8b1 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -20,7 +20,7 @@ Rails.application.configure do config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false + config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 23c8cea038..a69b933d81 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -126,7 +126,7 @@ end Settings['omniauth'] ||= Settingslogic.new({}) -Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? +Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_providers'].nil? @@ -134,7 +134,7 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? -Settings.omniauth['providers'] ||= [] +Settings.omniauth['providers'] ||= [] Settings.omniauth['cas3'] ||= Settingslogic.new({}) Settings.omniauth.cas3['session_duration'] ||= 8.hours Settings.omniauth['session_tickets'] ||= Settingslogic.new({}) @@ -168,7 +168,7 @@ end Settings['shared'] ||= Settingslogic.new({}) Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) -Settings['issues_tracker'] ||= {} +Settings['issues_tracker'] ||= {} # # GitLab @@ -183,7 +183,7 @@ Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80 Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' -Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" +Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}" Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab' @@ -196,7 +196,7 @@ Settings.gitlab['user_home'] ||= begin rescue ArgumentError # no user configured '/home/' + Settings.gitlab['user'] end -Settings.gitlab['time_zone'] ||= nil +Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) @@ -211,7 +211,7 @@ Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.g Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? -Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) +Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] @@ -225,8 +225,8 @@ Settings['gitlab_ci'] ||= Settingslogic.new({}) Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['shared_runners_enabled'].nil? Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil? Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? -Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) +Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) # # Reply by email @@ -240,7 +240,7 @@ Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'] Settings['artifacts'] ||= Settingslogic.new({}) Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) -Settings.artifacts['max_size'] ||= 100 # in megabytes +Settings.artifacts['max_size'] ||= 100 # in megabytes # # Git LFS @@ -298,7 +298,7 @@ Settings['backup'] ||= Settingslogic.new({}) Settings.backup['keep_time'] ||= 0 Settings.backup['pg_schema'] = nil Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root) -Settings.backup['archive_permissions'] ||= 0600 +Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) # Convert upload connection settings to use symbol keys, to make Fog happy if Settings.backup['upload']['connection'] diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 0d278717d6..1933afcbfb 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -20,7 +20,7 @@ if File.exist?(aws_file) config.fog_public = false # optional, defaults to {} - config.fog_attributes = { 'Cache-Control'=>'max-age=315576000' } + config.fog_attributes = { 'Cache-Control' => 'max-age=315576000' } # optional time (in seconds) that authenticated urls will be valid. # when fog_public is false and provider is AWS or Google, defaults to 600 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 31dceaebca..021bdb1125 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -243,7 +243,7 @@ Devise.setup do |config| when Hash # Add procs for handling SLO if provider['name'] == 'cas3' - provider['args'][:on_single_sign_out] = lambda do |request| + provider['args'][:on_single_sign_out] = lambda do |request| ticket = request.params[:session_index] raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) Gitlab::OAuth::Session.destroy(:cas3, ticket) diff --git a/config/routes.rb b/config/routes.rb index ec876eba34..0c01c52921 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -83,7 +83,7 @@ Rails.application.routes.draw do get 'help' => 'help#index' get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ } get 'help/shortcuts' - get 'help/ui' => 'help#ui' + get 'help/ui' => 'help#ui' # # Global snippets diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index e21af72a77..8706f0e8e7 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -74,7 +74,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps def project @project ||= begin - project =create :project + project = create :project project.team << [current_user, :master] project end diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index a2adc87f8e..06db36c701 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -100,7 +100,7 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps def project @project ||= begin - project =create :project + project = create :project project.team << [current_user, :master] project end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 909de31a47..b1a87b96ef 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -166,7 +166,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I have group with projects' do - @group = create(:group) + @group = create(:group) @group.add_owner(current_user) @project = create(:project, namespace: @group) @event = create(:closed_issue_event, project: @project) diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 3fbcf770b6..c6ced74737 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -126,7 +126,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I share project with group "OpenSource"' do project = Project.find_by(name: 'Shop') - os_group = create(:group, name: 'OpenSource') + os_group = create(:group, name: 'OpenSource') create(:project, group: os_group) @os_user1 = create(:user) @os_user2 = create(:user) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 5e638dbe16..cadf9f98fe 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -2,7 +2,7 @@ module API module Helpers PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" PRIVATE_TOKEN_PARAM = :private_token - SUDO_HEADER ="HTTP_SUDO" + SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo def parse_boolean(value) diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb index 6ed36b51f1..3411eb1d9c 100644 --- a/lib/gitlab/email/reply_parser.rb +++ b/lib/gitlab/email/reply_parser.rb @@ -65,7 +65,7 @@ module Gitlab (l =~ /On \w+ \d+,? \d+,?.*wrote:/) # Headers on subsequent lines - break if (0..2).all? { |off| lines[idx+off] =~ REPLYING_HEADER_REGEX } + break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX } # Headers on the same line break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3 diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index f7db7a6391..40766f35f7 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -25,7 +25,7 @@ module Gitlab end @pool.with { |redis| yield redis } end - + def self.redis_store_options url = new.url redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url) @@ -40,10 +40,10 @@ module Gitlab def initialize(rails_env=nil) rails_env ||= Rails.env config_file = File.expand_path('../../../config/resque.yml', __FILE__) - + @url = "redis://localhost:6379" if File.exist?(config_file) - @url =YAML.load_file(config_file)[rails_env] + @url = YAML.load_file(config_file)[rails_env] end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index c7ab318537..9eef8ea097 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -443,12 +443,12 @@ module Ci context 'when job variables are defined' do context 'when syntax is correct' do it 'returns job variables' do - variables = { + variables = { KEY1: 'value1', SOME_KEY_2: 'value2' } - config = YAML.dump( + config = YAML.dump( { before_script: ['pwd'], rspec: { variables: variables, diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb index acca0b08ba..46a5b7fce6 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -10,8 +10,8 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do 'path/dir_1/subdir/subfile' => { size: 10 }, 'path/second_dir' => {}, 'path/second_dir/dir_3/file_2' => { size: 10 }, - 'path/second_dir/dir_3/file_3'=> { size: 10 }, - 'another_directory/'=> {}, + 'path/second_dir/dir_3/file_3' => { size: 10 }, + 'another_directory/' => {}, 'another_file' => {}, '/file/with/absolute_path' => {} } end diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb index 5852b31ab3..3325190789 100644 --- a/spec/lib/gitlab/lfs/lfs_router_spec.rb +++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb @@ -26,8 +26,8 @@ describe Gitlab::Lfs::Router, lib: true do let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_size) { 499013 } - let(:respond_with_deprecated) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} - let(:respond_with_disabled) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} + let(:respond_with_deprecated) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} + let(:respond_with_disabled) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} describe 'when lfs is disabled' do before do diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ad47e338a3..ccb100cd96 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -56,7 +56,7 @@ describe Commit, models: true do end it "does not truncates a message with a newline after 80 but less 100 characters" do - message =<'application/json', 'X-Gitlab-Event'=>'Service Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' } ).once end it "POSTs the data as JSON" do @service_hook.execute(@data) expect(WebMock).to have_requested(:post, @service_hook.url).with( - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' } ).once end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 56a9fbe972..4078b9e4ff 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -33,7 +33,7 @@ describe SystemHook, models: true do Projects::CreateService.new(user, name: 'empty').execute expect(WebMock).to have_requested(:post, system_hook.url).with( body: /project_create/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -42,7 +42,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /project_destroy/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -51,7 +51,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_create/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -60,7 +60,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_destroy/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -69,7 +69,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_add_to_team/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -79,7 +79,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_team/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -88,7 +88,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /group_create/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -97,7 +97,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /group_destroy/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -106,7 +106,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_add_to_group/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -116,7 +116,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_group/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index d37590cab7..379c3e1219 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -65,7 +65,7 @@ describe SlackService::NoteMessage, models: true do expect(message.pretext).to eq("Test User commented on " \ " in : " \ "*merge request title*") - expected_attachments = [ + expected_attachments = [ { text: "comment on a merge request", color: color, @@ -117,7 +117,7 @@ describe SlackService::NoteMessage, models: true do expect(message.pretext).to eq("Test User commented on " \ " in : " \ "*snippet title*") - expected_attachments = [ + expected_attachments = [ { text: "comment on a snippet", color: color, diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 5ead735be4..0fbc984c06 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -106,8 +106,8 @@ describe API::API, api: true do context 'authorized user' do let(:download_headers) do - { 'Content-Transfer-Encoding'=>'binary', - 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } end it 'should return specific build artifacts' do diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb index 96d89e6920..02553d0f8e 100644 --- a/spec/requests/api/group_members_spec.rb +++ b/spec/requests/api/group_members_spec.rb @@ -34,11 +34,11 @@ describe API::API, api: true do expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.size).to eq(5) - expect(json_response.find { |e| e['id']==owner.id }['access_level']).to eq(GroupMember::OWNER) - expect(json_response.find { |e| e['id']==reporter.id }['access_level']).to eq(GroupMember::REPORTER) - expect(json_response.find { |e| e['id']==developer.id }['access_level']).to eq(GroupMember::DEVELOPER) - expect(json_response.find { |e| e['id']==master.id }['access_level']).to eq(GroupMember::MASTER) - expect(json_response.find { |e| e['id']==guest.id }['access_level']).to eq(GroupMember::GUEST) + expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER) + expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER) + expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER) + expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER) + expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST) end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index dfd361a2cd..cae4656010 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -402,8 +402,8 @@ describe Ci::API::API do context 'build has artifacts' do let(:build) { create(:ci_build, :artifacts) } let(:download_headers) do - { 'Content-Transfer-Encoding'=>'binary', - 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } end it 'should download artifact' do diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index a3f496359b..5ebe095743 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -2,11 +2,11 @@ module JiraServiceHelper def jira_service_settings properties = { - "title"=>"JIRA tracker", - "project_url"=>"http://jira.example/issues/?jql=project=A", - "issues_url"=>"http://jira.example/browse/JIRA-1", - "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa", - "api_url"=>"http://jira.example/rest/api/2" + "title" => "JIRA tracker", + "project_url" => "http://jira.example/issues/?jql=project=A", + "issues_url" => "http://jira.example/browse/JIRA-1", + "new_issue_url" => "http://jira.example/secure/CreateIssue.jspa", + "api_url" => "http://jira.example/rest/api/2" } jira_tracker.update_attributes(properties: properties, active: true) diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index eec2e68111..b5ca34bc02 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -36,20 +36,20 @@ module StubGitlabCalls stub_request(:post, "#{gitlab_url}api/v3/session.json"). with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}", - headers: { 'Content-Type'=>'application/json' }). - to_return(status: 201, body: f, headers: { 'Content-Type'=>'application/json' }) + headers: { 'Content-Type' => 'application/json' }). + to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_user f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json')) stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + with(headers: { 'Content-Type' => 'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token"). - with(headers: { 'Content-Type'=>'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + with(headers: { 'Content-Type' => 'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_project_8 @@ -66,19 +66,19 @@ module StubGitlabCalls f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json')) stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + with(headers: { 'Content-Type' => 'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_projects_owned stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). + with(headers: { 'Content-Type' => 'application/json' }). to_return(status: 200, body: "", headers: {}) end def stub_ci_enable stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). + with(headers: { 'Content-Type' => 'application/json' }). to_return(status: 200, body: "", headers: {}) end From 8d58e691c69f623878522c4f0dccf9f24229e9ce Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Fri, 13 May 2016 04:51:13 -0300 Subject: [PATCH 189/449] remove extra space to fix codestyle --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 0c01c52921..9e776a1f54 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,7 +74,7 @@ Rails.application.routes.draw do end # Health check - get 'health_check(/:checks)' => 'health_check#index', as: :health_check + get 'health_check(/:checks)' => 'health_check#index', as: :health_check # Enable Grack support mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] From 8eb67187d04f49f88afc13cdc3d0d591bb1e2a20 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 13:12:21 +0200 Subject: [PATCH 190/449] removed transaction stuff --- app/controllers/projects/imports_controller.rb | 4 +--- app/services/projects/create_service.rb | 8 +------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index d353a39034..a1b84afcd9 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -19,9 +19,7 @@ class Projects::ImportsController < Projects::ApplicationController if @project.import_failed? @project.import_retry else - Project.transaction do - @project.import_start - end + @project.import_start @project.add_import_job end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 2687544ae6..6728fabea1 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -52,7 +52,7 @@ module Projects save_project_and_import_data(import_data) - start_import if @project.import? + @project.import_start if @project.import? after_create_actions if @project.persisted? @@ -94,12 +94,6 @@ module Projects end end - def start_import - Project.transaction do - @project.import_start - end - end - def save_project_and_import_data(import_data) Project.transaction do @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data From 886bfb0a4ddeb546b9e14f7671273466dc5708ba Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 08:36:44 -0500 Subject: [PATCH 191/449] Initial specs for pipelines --- spec/features/pipelines_spec.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 spec/features/pipelines_spec.rb diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb new file mode 100644 index 0000000000..1e0c58be97 --- /dev/null +++ b/spec/features/pipelines_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe "Pipelines" do + include GitlabRoutingHelper + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + before { login_as(user) } + + describe "GET /:project/pipelines" do + + end + + describe "GET /:project/pipelines/:id" do + let(:pipeline) { create(:ci_commit, project: project, ref: 'master') } + + before do + create(:ci_build, :success, commit: pipeline) + end + + before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } + + it { expect(page).to()} + end +end From 575a73c896a113e6e9b76adb582cb23025cf0032 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 08:45:57 -0500 Subject: [PATCH 192/449] Fix specs --- app/models/namespace.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 62ef6de5db..cadd4ab044 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -153,7 +153,7 @@ class Namespace < ActiveRecord::Base end def any_project_has_container_registry_tags? - projects.any?(:has_container_registry_tags?) + projects.any?(&:has_container_registry_tags?) end def send_update_instructions From ae89161db5d6294948990b1d87f4fa306f2a1be9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 May 2016 09:25:38 -0500 Subject: [PATCH 193/449] Remove background color differentiation from gitlab logo/text in sidebar Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/gitlab-theme.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index dc4668877f..51a17d1469 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -9,9 +9,6 @@ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { .page-with-sidebar { .header-logo { - background-color: $color; - border-color: $color; - a { color: $color-light; @@ -21,7 +18,7 @@ } &:hover { - background-color: $color-darker; + background-color: $color-dark; a { color: #fff; From 6da24056030ccf45ec39f2f3b6addc6f0ffe1d1f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 May 2016 10:05:52 -0500 Subject: [PATCH 194/449] Add UI changes in 8.8 to changelog Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 588a4b4eef..30d4955c9f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -49,6 +49,8 @@ v 8.8.0 (unreleased) - Total method execution timings are no longer tracked - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034. (Andrei Gliga) - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) + - Hide left sidebar on phone screens to give more space for content + - Redesign navigation for profile and group pages v 8.7.5 - Fix relative links in wiki pages. !4050 From 16b1d3a34f665776bbdfca041326b9c2d569e59b Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 13 May 2016 11:12:49 -0500 Subject: [PATCH 195/449] Fix mobile dropdown bug --- app/assets/stylesheets/framework/nav.scss | 19 +++++++++++++++++++ app/helpers/nav_helper.rb | 8 ++++++++ app/views/layouts/_page.html.haml | 2 +- .../layouts/nav/_group_settings.html.haml | 2 +- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index f2d7dc625e..a81fcb1c6b 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -209,6 +209,15 @@ float: right; padding: 7px 0 0; + @media (max-width: $screen-xs-min) { + float: none; + padding: 0 9px; + + .dropdown-new { + width: 100%; + } + } + i { color: $layout-link-gray; } @@ -225,6 +234,10 @@ .dropdown { margin-left: 7px; + + @media (max-width: $screen-xs-min) { + margin-left: 0; + } } } @@ -260,4 +273,10 @@ .page-with-layout-nav { margin-top: 50px; + + &.controls-dropdown-visible { + @media (max-width: $screen-xs-min) { + margin-top: 96px; + } + } } diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 3aa4103045..fbb799eecd 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -43,4 +43,12 @@ module NavHelper class_name += " with-horizontal-nav" if defined?(nav) && nav class_name end + + def layout_nav_class + "page-with-layout-nav" if defined?(nav) && nav + end + + def layout_dropdown_class + "controls-dropdown-visible" if current_user + end end diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 3c0660d34f..5be0b546a6 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -25,7 +25,7 @@ .layout-nav .container-fluid = render "layouts/nav/#{nav}" - .content-wrapper{ class: ('page-with-layout-nav' if defined?(nav) && nav) } + .content-wrapper{ class: "#{layout_nav_class} #{layout_dropdown_class}" } = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index e391ec7f2b..0b2673f1a8 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,7 +1,7 @@ - if current_user - if access = @group.users.find_by(id: current_user.id) .controls - %span.dropdown.group-settings-dropdown + .dropdown.group-settings-dropdown %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} = icon('cog') = icon('caret-down') From 2148566b3dce96f36bbf86555ef0d68758044429 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 May 2016 11:21:31 -0500 Subject: [PATCH 196/449] Fix issue sidebar top position after header height change Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/header.scss | 4 ++-- app/assets/stylesheets/pages/issuable.scss | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 8190a97ed5..0da96c4017 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -6,12 +6,12 @@ header { transition-duration: .3s; &.navbar-empty { - height: 58px; + height: $header-height; background: #fff; border-bottom: 1px solid $btn-gray-hover; .center-logo { - margin: 11px 0; + margin: 8px 0; text-align: center; #tanuki-logo, img { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 1cf3023ecc..d06086a581 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -125,7 +125,7 @@ .right-sidebar { position: fixed; - top: 58px; + top: $header-height; bottom: 0; right: 0; z-index: 10; From 9a7a0415535da7541095de363dee37b9f09fe702 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 May 2016 11:24:00 -0500 Subject: [PATCH 197/449] Fix login btn vertical align Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/common.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 3386523dbf..f8aecd0558 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -289,7 +289,7 @@ table { text-shadow: none; @media (min-width: $screen-sm-min) { - margin-top: 11px; + margin-top: 8px; } } From 935c684ad3427069431a1130d364a4481af6cb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 13 May 2016 18:39:32 +0200 Subject: [PATCH 198/449] Revert "Merge branch 'doc-fix-db-gem-install-instructions' into 'master' " This reverts commit 66d8ef1ad9d5952b7e7f026658f71aafc861950e, reversing changes made to 6a8359f3d3be01af6f5b124b61af7ee1c77c17d0. The `--with` option was added in Bundler 1.10 but in this case it shouldn't even be needed: I've tested the original command locally and the pg gem was installed, so I believe the user that opened the original MR ran the wrong command (i.e. `sudo -u git -H bundle install --without development test postgres --deployment` instead of `sudo -u git -H bundle install --without development test mysql --deployment`. See discussion at https://gitlab.com/gitlab-org/gitlab-ce/commit/66d8ef1ad9d5952b7e7f026658f71aafc861950e for more context. --- doc/update/patch_versions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index b4283a526f..60729316cd 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -57,10 +57,10 @@ sudo -u git -H make cd /home/git/gitlab # PostgreSQL -sudo -u git -H bundle install --without development test mysql --with postgres --deployment +sudo -u git -H bundle install --without development test mysql --deployment # MySQL -sudo -u git -H bundle install --without development test postgres --with mysql --deployment +sudo -u git -H bundle install --without development test postgres --deployment # Optional: clean up old gems sudo -u git -H bundle clean From 9e318bd99deb90a93130cd4ef79e54f18555d4dc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 12:20:23 -0500 Subject: [PATCH 199/449] Fix container registry permissions --- app/models/ability.rb | 1 + .../container_registry_authentication_service.rb | 6 +++++- app/services/projects/destroy_service.rb | 2 +- ...ntainer_registry_authentication_service_spec.rb | 14 ++++++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 2465c1f424..09dea54689 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -61,6 +61,7 @@ class Ability :read_merge_request, :read_note, :read_commit_status, + :read_container_registry, :download_code ] diff --git a/app/services/jwt/container_registry_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb index 91bad34727..b60cd3c57e 100644 --- a/app/services/jwt/container_registry_authentication_service.rb +++ b/app/services/jwt/container_registry_authentication_service.rb @@ -3,6 +3,8 @@ module JWT AUDIENCE = 'container_registry' def execute + return error('not found', 404) unless registry.enabled + if params[:offline_token] return error('forbidden', 403) unless current_user end @@ -65,9 +67,11 @@ module JWT end def can_access?(requested_project, requested_action) + return false unless requested_project.container_registry_enabled? + case requested_action when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) + requested_project == project || can?(current_user, :read_container_registry, requested_project) when 'push' requested_project == project || can?(current_user, :create_container_registry, requested_project) else diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 0ff2bc3cb8..d3920ac8ba 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -64,7 +64,7 @@ module Projects end def remove_registry_tags - return unless Gitlab.config.registry.enabled + return true unless Gitlab.config.registry.enabled project.container_registry_repository.delete_tags end diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/jwt/container_registry_authentication_service_spec.rb index 1873ea2639..672a7579dd 100644 --- a/spec/services/jwt/container_registry_authentication_service_spec.rb +++ b/spec/services/jwt/container_registry_authentication_service_spec.rb @@ -7,6 +7,7 @@ describe JWT::ContainerRegistryAuthenticationService, services: true do let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } let(:registry_settings) do { + enabled: true, issuer: 'rspec', key: nil } @@ -146,7 +147,20 @@ describe JWT::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'a forbidden' end end + end + context 'for project without container registry' do + let(:project) { create(:empty_project, :public, container_registry_enabled: false) } + + before { project.update(container_registry_enabled: false) } + + context 'disallow when pulling' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'a forbidden' + end end end From c1bc5c58a2861af25f4f03e0a757dceae4b67cda Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 13:17:15 -0500 Subject: [PATCH 200/449] Added pipelines spec --- .../projects/ci/commits/_commit.html.haml | 2 +- spec/features/pipelines_spec.rb | 130 +++++++++++++++++- 2 files changed, 125 insertions(+), 7 deletions(-) diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index c6359c7c98..90ac41666d 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -55,7 +55,7 @@ .controls.hidden-xs.pull-right - artifacts = commit.builds.latest.select { |b| b.artifacts? } - if artifacts.present? - .dropdown.inline + .dropdown.inline.build-artifacts %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} = icon('download') %b.caret diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 1e0c58be97..0e654c3e40 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -5,21 +5,139 @@ describe "Pipelines" do let(:project) { create(:empty_project) } let(:user) { create(:user) } - before { login_as(user) } - - describe "GET /:project/pipelines" do + before do + login_as(user) + project.team << [user, :developer] end - describe "GET /:project/pipelines/:id" do + describe 'GET /:project/pipelines' do + let!(:pipeline) { create(:ci_commit, project: project, ref: 'master', status: 'running') } + + [:all, :running, :branches].each do |scope| + context "displaying #{scope}" do + let(:project) { create(:project) } + + before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) } + + it { expect(page).to have_content(pipeline.short_sha) } + end + end + + context 'cancelable pipeline' do + let!(:running) { create(:ci_build, :running, commit: pipeline, stage: 'test', commands: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_link('Cancel') } + it { expect(page).to have_selector('.ci-running') } + + context 'when canceling' do + before { click_link('Cancel') } + + it { expect(page).to_not have_link('Cancel') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + + context 'retryable pipelines' do + let!(:failed) { create(:ci_build, :failed, commit: pipeline, stage: 'test', commands: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_link('Retry') } + it { expect(page).to have_selector('.ci-failed') } + + context 'when retrying' do + before { click_link('Retry') } + + it { expect(page).to_not have_link('Retry') } + it { expect(page).to have_selector('.ci-running') } + end + end + + context 'downloadable pipelines' do + before { visit namespace_project_pipelines_path(project.namespace, project) } + + context 'with artifacts' do + let!(:with_artifacts) { create(:ci_build, :success, :artifacts, commit: pipeline, name: 'rspec tests', stage: 'test') } + + it { expect(page).to have_selector('.build-artifacts') } + it { expect(page).to have_link(with_artifacts.name) } + end + + context 'without artifacts' do + let!(:without_artifacts) { create(:ci_build, :success, commit: pipeline, name: 'rspec', stage: 'test') } + + it { expect(page).to_not have_selector('.build-artifacts') } + end + end + end + + describe 'GET /:project/pipelines/:id' do let(:pipeline) { create(:ci_commit, project: project, ref: 'master') } before do - create(:ci_build, :success, commit: pipeline) + @success = create(:ci_build, :success, commit: pipeline, stage: 'build') + @failed = create(:ci_build, :failed, commit: pipeline, stage: 'test', commands: 'test') + @running = create(:ci_build, :running, commit: pipeline, stage: 'deploy') + @external = create(:generic_commit_status, :success, commit: pipeline, stage: 'external') end before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } - it { expect(page).to()} + it 'showing a list of builds' do + expect(page).to have_content('Tests') + expect(page).to have_content(@success.id) + expect(page).to have_content('Deploy') + expect(page).to have_content(@failed.id) + expect(page).to have_content(@running.id) + expect(page).to have_content(@external.id) + expect(page).to have_content('Retry failed') + expect(page).to have_content('Cancel running') + end + + context 'retrying builds' do + it { expect(page).to_not have_content('retried') } + + context 'when retrying' do + before { click_on 'Retry failed' } + + it { expect(page).to_not have_content('Retry failed') } + it { expect(page).to have_content('retried') } + end + end + + context 'canceling builds' do + it { expect(page).to_not have_selector('.ci-canceled') } + + context 'when canceling' do + before { click_on 'Cancel running' } + + it { expect(page).to_not have_content('Cancel running') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + end + + describe 'POST /:project/pipelines' do + let(:project) { create(:project) } + + before { visit new_namespace_project_pipeline_path(project.namespace, project) } + + context 'for valid commit' do + before { fill_in('Create for', with: 'master') } + + it { expect{ click_on 'Create pipeline' }.to change{ Ci::Commit.count }.by(1) } + end + + context 'for invalid commit' do + before do + fill_in('Create for', with: 'invalid reference') + click_on 'Create pipeline' + end + + it { expect(page).to have_content('Reference not found') } + end end end From e5c97101478829fe8200557afd02bde92c3eb4e4 Mon Sep 17 00:00:00 2001 From: Artem Sidorenko Date: Tue, 3 May 2016 17:33:43 +0200 Subject: [PATCH 201/449] Use the relative url prefix for links in Wiki --- CHANGELOG | 3 +++ app/models/project_wiki.rb | 2 +- lib/gitlab/url_builder.rb | 2 +- spec/models/project_wiki_spec.rb | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..5ffe91ce4e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -52,6 +52,9 @@ v 8.8.0 (unreleased) - Hide left sidebar on phone screens to give more space for content - Redesign navigation for profile and group pages +v 8.7.6 + - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) + v 8.7.5 - Fix relative links in wiki pages. !4050 - Fix always showing build notification message when switching between merge requests !4086 diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 060ed9b44e..339fb0b9f9 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -40,7 +40,7 @@ class ProjectWiki end def wiki_base_path - ["/", @project.path_with_namespace, "/wikis"].join('') + [Gitlab.config.gitlab.relative_url_root, "/", @project.path_with_namespace, "/wikis"].join('') end # Returns the Gollum::Wiki object. diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 2bbbd3074e..fe65c24610 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -62,7 +62,7 @@ module Gitlab end def wiki_page_url - "#{Gitlab.config.gitlab.url}#{object.wiki.wiki_base_path}/#{object.slug}" + namespace_project_wiki_url(object.wiki.project.namespace, object.wiki.project, object.slug) end end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 532e3f013f..91ebb612ba 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -38,7 +38,8 @@ describe ProjectWiki, models: true do describe "#wiki_base_path" do it "returns the wiki base path" do - wiki_base_path = "/#{project.path_with_namespace}/wikis" + wiki_base_path = "#{Gitlab.config.gitlab.relative_url_root}/#{project.path_with_namespace}/wikis" + expect(subject.wiki_base_path).to eq(wiki_base_path) end end From 9ef9e008feb99aaf0c4edc85bb76039eb46f0794 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 16:22:50 -0500 Subject: [PATCH 202/449] Move JWT to Gitlab::JWT --- app/controllers/jwt_controller.rb | 2 +- ...ntainer_registry_authentication_service.rb | 118 +++++++++--------- 2 files changed, 61 insertions(+), 59 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 599f62bd12..c203c50d1f 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -3,7 +3,7 @@ class JwtController < ApplicationController skip_before_action :verify_authenticity_token SERVICES = { - 'container_registry' => JWT::ContainerRegistryAuthenticationService, + 'container_registry' => ::Gitlab::JWT::ContainerRegistryAuthenticationService, } def auth diff --git a/app/services/jwt/container_registry_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb index 0ab3e6d02b..dd0f295478 100644 --- a/app/services/jwt/container_registry_authentication_service.rb +++ b/app/services/jwt/container_registry_authentication_service.rb @@ -1,69 +1,71 @@ -module JWT - class ContainerRegistryAuthenticationService < BaseService - def execute - if params[:offline_token] - return error('forbidden', 403) unless current_user +module Gitlab + module JWT + class ContainerRegistryAuthenticationService < BaseService + def execute + if params[:offline_token] + return error('forbidden', 403) unless current_user + end + + return error('forbidden', 401) if scopes.blank? + + { token: authorized_token(scopes).encoded } end - return error('forbidden', 401) if scopes.blank? + private - { token: authorized_token(scopes).encoded } - end - - private - - def authorized_token(access) - token = ::JWT::RSAToken.new(registry.key) - token.issuer = registry.issuer - token.audience = params[:service] - token.subject = current_user.try(:username) - token[:access] = access - token - end - - def scopes - return unless params[:scope] - - @scopes ||= begin - scope = process_scope(params[:scope]) - [scope].compact - end - end - - def process_scope(scope) - type, name, actions = scope.split(':', 3) - actions = actions.split(',') - - case type - when 'repository' - process_repository_access(type, name, actions) - end - end - - def process_repository_access(type, name, actions) - requested_project = Project.find_with_namespace(name) - return unless requested_project - - actions = actions.select do |action| - can_access?(requested_project, action) + def authorized_token(access) + token = ::JWT::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token[:access] = access + token end - { type: type, name: name, actions: actions } if actions.present? - end + def scopes + return unless params[:scope] - def can_access?(requested_project, requested_action) - case requested_action - when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) - when 'push' - requested_project == project || can?(current_user, :create_container_registry, requested_project) - else - false + @scopes ||= begin + scope = process_scope(params[:scope]) + [scope].compact + end end - end - def registry - Gitlab.config.registry + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + requested_project = Project.find_with_namespace(name) + return unless requested_project + + actions = actions.select do |action| + can_access?(requested_project, action) + end + + { type: type, name: name, actions: actions } if actions.present? + end + + def can_access?(requested_project, requested_action) + case requested_action + when 'pull' + requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) + when 'push' + requested_project == project || can?(current_user, :create_container_registry, requested_project) + else + false + end + end + + def registry + Gitlab.config.registry + end end end end From e900ff972a4a6133a499adcc1263d3634863f410 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 16:23:02 -0500 Subject: [PATCH 203/449] Improve JwtController code --- app/controllers/jwt_controller.rb | 37 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index c203c50d1f..e067f59808 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -1,22 +1,13 @@ class JwtController < ApplicationController skip_before_action :authenticate_user! skip_before_action :verify_authenticity_token + before_action :authenticate_project_or_user SERVICES = { 'container_registry' => ::Gitlab::JWT::ContainerRegistryAuthenticationService, } def auth - @authenticated = authenticate_with_http_basic do |login, password| - # if it's possible we first try to authenticate project with login and password - @project = authenticate_project(login, password) - @user = authenticate_user(login, password) unless @project - end - - unless @authenticated - head :forbidden if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) - end - service = SERVICES[params[:service]] head :not_found unless service @@ -28,19 +19,28 @@ class JwtController < ApplicationController private + def authenticate_project_or_user + authenticate_with_http_basic do |login, password| + # if it's possible we first try to authenticate project with login and password + @project = authenticate_project(login, password) + return if @project + + @user = authenticate_user(login, password) + return if @user + end + + if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) + head :forbidden + end + end + def auth_params params.permit(:service, :scope, :offline_token, :account, :client_id) end def authenticate_project(login, password) - matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) - - if matched_login.present? - underscored_service = matched_login['s'].underscore - - if underscored_service == 'gitlab_ci' - Project.find_by(builds_enabled: true, runners_token: password) - end + if login == 'gitlab_ci_token' + Project.find_by(builds_enabled: true, runners_token: password) end end @@ -77,6 +77,7 @@ class JwtController < ApplicationController if banned Rails.logger.info "IP #{request.ip} failed to login " \ "as #{login} but has been temporarily banned from Git auth" + return end end end From 9eb4569ed9330d65fb24e0272402fe5ac739b8be Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 13 May 2016 16:17:01 -0500 Subject: [PATCH 204/449] Change all occurrences of doc.gitlab.com to docs.gitlab.com [ci skip] --- doc/administration/environment_variables.md | 2 +- doc/administration/high_availability/load_balancer.md | 2 +- doc/administration/high_availability/nfs.md | 2 +- doc/api/services.md | 2 +- doc/development/doc_styleguide.md | 4 ++-- doc/gitlab-basics/create-issue.md | 2 +- doc/gitlab-basics/create-project.md | 2 +- doc/hooks/custom_hooks.md | 2 +- doc/install/installation.md | 2 +- doc/install/relative_url.md | 2 +- doc/integration/README.md | 2 +- doc/intro/README.md | 2 +- doc/logs/logs.md | 2 +- doc/raketasks/README.md | 2 +- doc/update/README.md | 6 +++--- doc/workflow/gitlab_flow.md | 4 ++-- doc/workflow/groups.md | 2 +- doc/workflow/importing/import_projects_from_github.md | 2 +- doc/workflow/importing/import_projects_from_gitlab_com.md | 2 +- doc/workflow/lfs/manage_large_binaries_with_git_lfs.md | 2 +- 20 files changed, 24 insertions(+), 24 deletions(-) diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index 43ab153d76..7f53915a4d 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -58,4 +58,4 @@ to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`. It's possible to preconfigure the GitLab docker image by adding the environment variable `GITLAB_OMNIBUS_CONFIG` to the `docker run` command. -For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://doc.gitlab.com/omnibus/docker/#preconfigure-docker-container). +For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://docs.gitlab.com/omnibus/docker/#preconfigure-docker-container). diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index b1fe34ed9a..136f570ac2 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -60,4 +60,4 @@ Read more on high-availability configuration: configure custom domains with custom SSL, which would not be possible if SSL was terminated at the load balancer. -[gitlab-pages]: http://doc.gitlab.com/ee/pages/administration.html +[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index e4e124e200..49ff5d536a 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -113,4 +113,4 @@ Read more on high-availability configuration: 1. [Configure the GitLab application servers](gitlab.md) 1. [Configure the load balancers](load_balancer.md) -[udp-log-shipping]: http://doc.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" +[udp-log-shipping]: http://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" diff --git a/doc/api/services.md b/doc/api/services.md index 7d45b2cf46..83ac784515 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -491,7 +491,7 @@ Jira issue tracker Set JIRA service for a project. -> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html) +> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html) ``` PUT /projects/:id/services/jira diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 187ec9e7b7..8292b39375 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -127,7 +127,7 @@ Inside the document: ``` If the document you are editing resides in a place other than the GitLab CE/EE `doc/` directory, instead of the relative link, use the full path: - `http://doc.gitlab.com/ce/administration/restart_gitlab.html`. + `http://docs.gitlab.com/ce/administration/restart_gitlab.html`. Replace `reconfigure` with `restart` where appropriate. ## Installation guide @@ -266,5 +266,5 @@ curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "restricted_signup_domai [cURL]: http://curl.haxx.se/ "cURL website" [single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html -[gfm]: http://doc.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" +[gfm]: http://docs.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" [doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation" diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index 87f078def0..5221d85b66 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all ![Submit new issue](basicsimages/submit_new_issue.png) -Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://doc.gitlab.com/ce/customization/issue_closing.html). +Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html). diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index b545d62549..f737dffc02 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -14,7 +14,7 @@ Fill out the required information: 1. Select a [visibility level](https://gitlab.com/help/public_access/public_access) -1. You can also [import your existing projects](http://doc.gitlab.com/ce/workflow/importing/README.html) +1. You can also [import your existing projects](http://docs.gitlab.com/ce/workflow/importing/README.html) 1. Click on "create project" diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index dcdf49d337..820934f97f 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** +Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://docs.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. diff --git a/doc/install/installation.md b/doc/install/installation.md index 44ae0be406..fa11eb9ba6 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -6,7 +6,7 @@ Since an installation from source is a lot of work and error prone we strongly r One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes. On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time. -Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://doc.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. +Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://docs.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. After this termination Runit will detect Sidekiq is not running and will start it. Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time. diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md index 0245febfcd..44d2a14f36 100644 --- a/doc/install/relative_url.md +++ b/doc/install/relative_url.md @@ -132,5 +132,5 @@ To disable the relative URL: 1. Follow the same as above starting from 2. and set up the GitLab URL to one that doesn't contain a relative path. -[omnibus-rel]: http://doc.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" +[omnibus-rel]: http://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" [restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab" diff --git a/doc/integration/README.md b/doc/integration/README.md index 6fe04aa2a0..fd330dd7a7 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -19,7 +19,7 @@ See the documentation below for details on how to configure these services. GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. -[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html +[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html ## Project services diff --git a/doc/intro/README.md b/doc/intro/README.md index ab298d3808..382d10aaf4 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -39,4 +39,4 @@ Install and update your GitLab installation. - [Install GitLab](https://about.gitlab.com/installation/) - [Update GitLab](https://about.gitlab.com/update/) -- [Explore Omnibus GitLab configuration options](http://doc.gitlab.com/omnibus/settings/configuration.html) +- [Explore Omnibus GitLab configuration options](http://docs.gitlab.com/omnibus/settings/configuration.html) diff --git a/doc/logs/logs.md b/doc/logs/logs.md index 27937e5176..ef5affa2eb 100644 --- a/doc/logs/logs.md +++ b/doc/logs/logs.md @@ -1,6 +1,6 @@ ## Log system GitLab has advanced log system so everything is logging and you can analize your instance using various system log files. -In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://doc.gitlab.com/ee/administration/audit_events.html) +In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://docs.gitlab.com/ee/administration/audit_events.html) System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 6be954ad68..a49c43b8ef 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -8,4 +8,4 @@ - [User management](user_management.md) - [Webhooks](web_hooks.md) - [Import](import.md) of git repositories in bulk -- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators +- [Rebuild authorized_keys file](http://docs.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators diff --git a/doc/update/README.md b/doc/update/README.md index a770633c9b..975d72164b 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -29,7 +29,7 @@ Based on your installation, choose a section below that fits your needs. ## Omnibus Packages -- The [Omnibus update guide](http://doc.gitlab.com/omnibus/update/README.html) +- The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html) contains the steps needed to update an Omnibus GitLab package. ## Installation from source @@ -86,10 +86,10 @@ possible. information about configuring GitLab to work with a MySQL database. - [Restoring from backup after a failed upgrade](restore_after_failure.md) -[omnidocker]: http://doc.gitlab.com/omnibus/docker/README.html +[omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html [source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update [source-ce]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update [ee-ce]: ../downgrade_ee_to_ce/README.md [ce]: https://about.gitlab.com/features/#community [ee]: https://about.gitlab.com/features/#enterprise -[omni-ce-ee]: http://doc.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition +[omni-ce-ee]: http://docs.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 1b354bcc0f..2b2f140f8b 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged. If the assigned person does not feel comfortable they can close the merge request without merging. -In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://doc.gitlab.com/ce/permissions/permissions.html). +In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html). So if you want to merge it into a protected branch you assign it to someone with master authorizations. ## Issues with GitLab flow @@ -187,7 +187,7 @@ If you have an issue that spans across multiple repositories, the best thing is ![Vim screen showing the rebase view](rebase.png) With git you can use an interactive rebase (`rebase -i`) to squash multiple commits into one and reorder them. -In GitLab EE and .com you can also [rebase before merge](http://doc.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. +In GitLab EE and .com you can also [rebase before merge](http://docs.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical. However you should never rebase commits you have pushed to a remote server. Somebody can have referred to the commits or cherry-picked them. diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md index 52bf611dc5..34ada1774d 100644 --- a/doc/workflow/groups.md +++ b/doc/workflow/groups.md @@ -54,7 +54,7 @@ If necessary, you can increase the access level of an individual user for a spec ## Managing group memberships via LDAP In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. -See [the GitLab Enterprise Edition documentation](http://doc.gitlab.com/ee/integration/ldap.html) for more information. +See [the GitLab Enterprise Edition documentation](http://docs.gitlab.com/ee/integration/ldap.html) for more information. ## Allowing only admins to create groups diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index e670e415c7..a7dfac2c12 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -44,5 +44,5 @@ case the namespace is taken, the project will be imported on the user's namespace. [gh-import]: ../../integration/github.md "GitHub integration" -[ee-gh]: http://doc.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE" +[ee-gh]: http://docs.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE" [new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab" diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index 1117db98e7..dcc00074b7 100644 --- a/doc/workflow/importing/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -2,7 +2,7 @@ You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if GitLab support is enabled on your GitLab instance. -You can read more about GitLab support [here](http://doc.gitlab.com/ce/integration/gitlab.html) +You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html) To get to the importer page you need to go to "New project" page. ![New project page](gitlab_importer/new_project_page.png) diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 83db44c10b..1295dfbd77 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -4,7 +4,7 @@ Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. The general recommendation is to not have Git repositories larger than 1GB to preserve performance. -GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) +GitLab already supports [managing large files with git annex](http://docs.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain environments it is not always convenient to use different commands to differentiate between the large files and regular ones. From 509654b3784da2a084a0c8303e9c6cc1498b8d2b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 16:41:30 -0500 Subject: [PATCH 205/449] Improve JWT::RSAToken::kid --- lib/jwt/rsa_token.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb index 0438135ad5..4de89bf0d3 100644 --- a/lib/jwt/rsa_token.rb +++ b/lib/jwt/rsa_token.rb @@ -24,11 +24,13 @@ module JWT @key ||= OpenSSL::PKey::RSA.new(key_data) end + def public_key + key.public_key + end + def kid - sha256 = Digest::SHA256.new - sha256.update(key.public_key.to_der) - payload = StringIO.new(sha256.digest).read(30) - Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem| + fingerprint = Digest::SHA256.digest(public_key.to_der) + Base32.encode(fingerprint).split('').each_slice(4).each_with_object([]) do |slice, mem| mem << slice.join end.join(':') end From 8532dc0d774d43e2e23ab169d5af0c2ab12c139c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 21 Apr 2016 17:16:22 -0300 Subject: [PATCH 206/449] Import pull requests from GitHub where the source branch was removed --- lib/gitlab/github_import/importer.rb | 44 ++++++++++++------- .../github_import/pull_request_formatter.rb | 26 +++++++---- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 0f9e3ee14e..a3f2789178 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,12 +3,15 @@ module Gitlab class Importer include Gitlab::ShellAdapter - attr_reader :project, :client + attr_reader :client, :project, :repo, :repo_url def initialize(project) @project = project - if import_data_credentials - @client = Client.new(import_data_credentials[:user]) + @repo = project.import_source + @repo_url = project.import_url + + if credentials + @client = Client.new(credentials[:user]) @formatter = Gitlab::ImportFormatter.new else raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" @@ -22,8 +25,8 @@ module Gitlab private - def import_data_credentials - @import_data_credentials ||= project.import_data.credentials if project.import_data + def credentials + @credentials ||= project.import_data.credentials if project.import_data end def import_labels @@ -68,22 +71,31 @@ module Gitlab end def import_pull_requests - client.pull_requests(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| - pull_request = PullRequestFormatter.new(project, raw_data) + pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc) + .map { |raw| PullRequestFormatter.new(project, raw) } + .reject(&:cross_project?) - if pull_request.valid? - merge_request = MergeRequest.new(pull_request.attributes) + source_branches_removed = pull_requests.reject(&:source_branch_exists?) + source_branches_removed.each do |pull_request| + client.create_ref(repo, "refs/heads/#{pull_request.source_branch}", pull_request.source_sha) + end - if merge_request.save - apply_labels(pull_request.number, merge_request) - import_comments(pull_request.number, merge_request) - import_comments_on_diff(pull_request.number, merge_request) - end + project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') + + pull_requests.each do |pull_request| + merge_request = MergeRequest.new(pull_request.attributes) + + if merge_request.save + apply_labels(pull_request.number, merge_request) + import_comments(pull_request.number, merge_request) + import_comments_on_diff(pull_request.number, merge_request) end end + source_branches_removed.each do |pull_request| + client.delete_ref(repo, "heads/#{pull_request.source_branch}") + end + true rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index d21b942ad4..be970f724c 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -7,7 +7,7 @@ module Gitlab title: raw_data.title, description: description, source_project: source_project, - source_branch: source_branch.name, + source_branch: source_branch, target_project: target_project, target_branch: target_branch.name, state: state, @@ -27,6 +27,22 @@ module Gitlab !cross_project? && source_branch.present? && target_branch.present? end + def cross_project? + source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + end + + def source_branch_exists? + source_project.repository.branch_names.include?(source_branch) + end + + def source_branch + raw_data.head.ref + end + + def source_sha + raw_data.head.sha + end + private def assigned? @@ -51,10 +67,6 @@ module Gitlab raw_data.body || "" end - def cross_project? - source_repo.present? && target_repo.present? && source_repo.id != target_repo.id - end - def description formatter.author_line(author) + body end @@ -73,10 +85,6 @@ module Gitlab raw_data.head.repo end - def source_branch - source_project.repository.find_branch(raw_data.head.ref) - end - def target_project project end From 9b4dc552cb51faee38baffa6f29954d795282658 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 22 Apr 2016 15:25:04 -0300 Subject: [PATCH 207/449] Import pull requests from GitHub where the target branch was removed --- lib/gitlab/github_import/importer.rb | 25 +++++++++++++------ .../github_import/pull_request_formatter.rb | 18 +++++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index a3f2789178..bc25fc8c0b 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -75,10 +75,11 @@ module Gitlab .map { |raw| PullRequestFormatter.new(project, raw) } .reject(&:cross_project?) - source_branches_removed = pull_requests.reject(&:source_branch_exists?) - source_branches_removed.each do |pull_request| - client.create_ref(repo, "refs/heads/#{pull_request.source_branch}", pull_request.source_sha) - end + source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch, pr.source_sha] } + target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch, pr.target_sha] } + branches_removed = source_branches_removed | target_branches_removed + + create_refs(branches_removed) project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') @@ -92,15 +93,25 @@ module Gitlab end end - source_branches_removed.each do |pull_request| - client.delete_ref(repo, "heads/#{pull_request.source_branch}") - end + delete_refs(branches_removed) true rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message end + def create_refs(branches) + branches.each do |branch| + client.create_ref(repo, "refs/heads/#{branch.first}", branch.last) + end + end + + def delete_refs(branches) + branches.each do |branch| + client.delete_ref(repo, "heads/#{branch.first}") + end + end + def apply_labels(number, issuable) issue = client.issue(project.import_source, number) diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index be970f724c..ef32a23b04 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -9,7 +9,7 @@ module Gitlab source_project: source_project, source_branch: source_branch, target_project: target_project, - target_branch: target_branch.name, + target_branch: target_branch, state: state, milestone: milestone, author_id: author_id, @@ -43,6 +43,18 @@ module Gitlab raw_data.head.sha end + def target_branch_exists? + target_project.repository.branch_names.include?(target_branch) + end + + def target_branch + raw_data.base.ref + end + + def target_sha + raw_data.base.sha + end + private def assigned? @@ -93,10 +105,6 @@ module Gitlab raw_data.base.repo end - def target_branch - target_project.repository.find_branch(raw_data.base.ref) - end - def state @state ||= case true when raw_data.state == 'closed' && raw_data.merged_at.present? From 3b50867550818413eed01fcbe85e5442707150be Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 26 Apr 2016 12:03:48 -0300 Subject: [PATCH 208/449] Fix spec for Gitlab::GithubImport::PullRequestFormatter --- lib/gitlab/github_import/importer.rb | 2 +- .../github_import/pull_request_formatter.rb | 10 +- .../pull_request_formatter_spec.rb | 104 +++++++++++++----- 3 files changed, 82 insertions(+), 34 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index bc25fc8c0b..9aea947a81 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -73,7 +73,7 @@ module Gitlab def import_pull_requests pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc) .map { |raw| PullRequestFormatter.new(project, raw) } - .reject(&:cross_project?) + .select(&:valid?) source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch, pr.source_sha] } target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch, pr.target_sha] } diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index ef32a23b04..f242596bdf 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -24,11 +24,7 @@ module Gitlab end def valid? - !cross_project? && source_branch.present? && target_branch.present? - end - - def cross_project? - source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + !cross_project? end def source_branch_exists? @@ -79,6 +75,10 @@ module Gitlab raw_data.body || "" end + def cross_project? + source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + end + def description formatter.author_line(author) + body end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index e59c0ca110..b1f3d17373 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -4,9 +4,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:project) { create(:project) } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } - let(:source_branch) { double(ref: 'feature', repo: source_repo) } + let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } let(:target_repo) { repository } - let(:target_branch) { double(ref: 'master', repo: target_repo) } + let(:target_branch) { double(ref: 'master', repo: target_repo, sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7') } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -137,11 +137,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:milestone) { double(number: 45) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } - it 'returns nil when milestone does not exists' do + it 'returns nil when milestone does not exist' do expect(pull_request.attributes.fetch(:milestone)).to be_nil end - it 'returns milestone when is exists' do + it 'returns milestone when it exists' do milestone = create(:milestone, project: project, iid: 45) expect(pull_request.attributes.fetch(:milestone)).to eq milestone @@ -157,32 +157,80 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + describe '#source_branch' do + let(:raw_data) { double(base_data) } + + it 'returns head ref' do + expect(pull_request.source_branch).to eq 'feature' + end + end + + describe '#source_sha' do + let(:raw_data) { double(base_data) } + + it 'returns head sha' do + expect(pull_request.source_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end + end + + describe '#source_branch_exists?' do + context 'when source branch exists' do + let(:raw_data) { double(base_data) } + + it 'returns true' do + expect(pull_request.source_branch_exists?).to eq true + end + end + + context 'when source branch does not exist' do + let(:raw_data) { double(base_data.merge(head: double(ref: 'removed-branch').as_null_object)) } + + it 'returns false' do + expect(pull_request.source_branch_exists?).to eq false + end + end + end + + describe '#target_branch' do + let(:raw_data) { double(base_data) } + + it 'returns base ref' do + expect(pull_request.target_branch).to eq 'master' + end + end + + describe '#target_sha' do + let(:raw_data) { double(base_data) } + + it 'returns base sha' do + expect(pull_request.target_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7' + end + end + + describe '#target_branch_exists?' do + context 'when target branch exists' do + let(:raw_data) { double(base_data) } + + it 'returns true' do + expect(pull_request.target_branch_exists?).to eq true + end + end + + context 'when target branch does not exist' do + let(:raw_data) { double(base_data.merge(base: double(ref: 'removed-branch').as_null_object)) } + + it 'returns false' do + expect(pull_request.target_branch_exists?).to eq false + end + end + end + describe '#valid?' do - let(:invalid_branch) { double(ref: 'invalid-branch').as_null_object } + context 'when source, and target repos are not a fork' do + let(:raw_data) { double(base_data) } - context 'when source, and target repositories are the same' do - context 'and source and target branches exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: target_branch)) } - - it 'returns true' do - expect(pull_request.valid?).to eq true - end - end - - context 'and source branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: invalid_branch, base: target_branch)) } - - it 'returns false' do - expect(pull_request.valid?).to eq false - end - end - - context 'and target branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: invalid_branch)) } - - it 'returns false' do - expect(pull_request.valid?).to eq false - end + it 'returns true' do + expect(pull_request.valid?).to eq true end end From 6d19e13df62376916e024ff44939bf2a8f5b671b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 May 2016 17:11:57 -0500 Subject: [PATCH 209/449] Fix specs --- app/models/commit_status.rb | 11 +++------- app/models/project.rb | 2 +- .../projects/ci/commits/_commit.html.haml | 3 +-- spec/features/pipelines_spec.rb | 21 +++++++++++-------- spec/models/commit_status_spec.rb | 12 ----------- 5 files changed, 17 insertions(+), 32 deletions(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 2d09edf3ca..c7451ea0a8 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -92,16 +92,11 @@ class CommitStatus < ActiveRecord::Base def self.stages # We group by stage name, but order stages by theirs' index - unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') + unscoped.where(id: all).group('stage').order('max(stage_idx)', 'stage').pluck('stage') end - def self.stages_status - # We execute subquery for each stage to calculate a stage status - statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) - statuses.inject({}) do |h, k| - h[k.first] = k.last - h - end + def self.status_for_stage(stage) + where(stage: stage).status end def ignored? diff --git a/app/models/project.rb b/app/models/project.rb index 82489235a3..dfd1e54ecf 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -354,7 +354,7 @@ class Project < ActiveRecord::Base join_body = "INNER JOIN ( SELECT project_id, COUNT(*) AS amount FROM notes - WHERE created_at >= #{sanitize(since)}project.ci_commits + WHERE created_at >= #{sanitize(since)} GROUP BY project_id ) join_note_counts ON projects.id = join_note_counts.project_id" diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index 90ac41666d..7f9a341783 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -31,10 +31,9 @@ Cant find HEAD commit for this branch - - stages_status = commit.statuses.stages_status - stages.each do |stage| %td - - if status = stages_status[stage] + - if status = commit.statuses.status_for_stage(stage) - tooltip = "#{stage.titleize}: #{status}" %span.has-tooltip(title="#{tooltip}"){class: "ci-status-icon-#{status}"} = ci_icon_for_status(status) diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 0e654c3e40..1df516eafd 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -52,15 +52,15 @@ describe "Pipelines" do before { click_link('Retry') } it { expect(page).to_not have_link('Retry') } - it { expect(page).to have_selector('.ci-running') } + it { expect(page).to have_selector('.ci-pending') } end end context 'downloadable pipelines' do - before { visit namespace_project_pipelines_path(project.namespace, project) } - context 'with artifacts' do - let!(:with_artifacts) { create(:ci_build, :success, :artifacts, commit: pipeline, name: 'rspec tests', stage: 'test') } + let!(:with_artifacts) { create(:ci_build, :artifacts, :success, commit: pipeline, name: 'rspec tests', stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } it { expect(page).to have_selector('.build-artifacts') } it { expect(page).to have_link(with_artifacts.name) } @@ -78,10 +78,10 @@ describe "Pipelines" do let(:pipeline) { create(:ci_commit, project: project, ref: 'master') } before do - @success = create(:ci_build, :success, commit: pipeline, stage: 'build') - @failed = create(:ci_build, :failed, commit: pipeline, stage: 'test', commands: 'test') - @running = create(:ci_build, :running, commit: pipeline, stage: 'deploy') - @external = create(:generic_commit_status, :success, commit: pipeline, stage: 'external') + @success = create(:ci_build, :success, commit: pipeline, stage: 'build', name: 'build') + @failed = create(:ci_build, :failed, commit: pipeline, stage: 'test', name: 'test', commands: 'test') + @running = create(:ci_build, :running, commit: pipeline, stage: 'deploy', name: 'deploy') + @external = create(:generic_commit_status, status: 'success', commit: pipeline, name: 'jenkins', stage: 'external') end before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } @@ -126,7 +126,10 @@ describe "Pipelines" do before { visit new_namespace_project_pipeline_path(project.namespace, project) } context 'for valid commit' do - before { fill_in('Create for', with: 'master') } + before do + fill_in('Create for', with: 'master') + stub_ci_commit_to_return_yaml_file + end it { expect{ click_on 'Create pipeline' }.to change{ Ci::Commit.count }.by(1) } end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 971e675037..eb3715f00d 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -219,17 +219,5 @@ describe CommitStatus, models: true do is_expected.to eq(%w(build test deploy)) end end - - context 'stages with statuses' do - subject { CommitStatus.where(commit: commit).stages_status } - - it 'return list of stages with statuses' do - is_expected.to eq({ - 'build' => 'failed', - 'test' => 'success', - 'deploy' => 'running' - }) - end - end end end From 99d3e21f19ffb5cccb58fdfeac4fb6174e7e65e2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 10 May 2016 17:41:46 -0500 Subject: [PATCH 210/449] Extract LegacyDiffNote out of Note --- app/assets/javascripts/notes.js.coffee | 2 + app/assets/stylesheets/pages/notes.scss | 3 +- app/controllers/projects/commit_controller.rb | 11 +- .../projects/compare_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 7 +- app/controllers/projects/notes_controller.rb | 6 +- app/finders/notes_finder.rb | 2 +- app/helpers/diff_helper.rb | 22 +- app/helpers/notes_helper.rb | 29 +-- app/models/legacy_diff_note.rb | 182 ++++++++++++++ app/models/note.rb | 222 +++--------------- .../notify/note_merge_request_email.html.haml | 2 +- .../projects/diffs/_parallel_view.html.haml | 10 +- app/views/projects/diffs/_text_file.html.haml | 9 +- .../notes/_commit_discussion.html.haml | 0 .../notes/_diff_notes_with_reply.html.haml | 18 +- .../_diff_notes_with_reply_parallel.html.haml | 20 +- .../projects/notes/_discussion.html.haml | 47 +++- app/views/projects/notes/_form.html.haml | 1 + app/views/projects/notes/_note.html.haml | 9 +- app/views/projects/notes/_notes.html.haml | 9 +- .../notes/discussions/_active.html.haml | 16 -- .../notes/discussions/_commit.html.haml | 25 -- .../notes/discussions/_diff.html.haml | 28 --- .../_legacy_diff_with_notes.html.haml | 30 +++ .../notes/discussions/_notes.html.haml | 7 + .../notes/discussions/_outdated.html.haml | 14 -- .../20160508215820_add_type_to_notes.rb | 5 + ...508221410_set_type_on_legacy_diff_notes.rb | 5 + db/schema.rb | 5 +- lib/api/commits.rb | 2 + lib/api/entities.rb | 6 +- spec/models/note_spec.rb | 8 +- 33 files changed, 390 insertions(+), 374 deletions(-) create mode 100644 app/models/legacy_diff_note.rb delete mode 100644 app/views/projects/notes/_commit_discussion.html.haml delete mode 100644 app/views/projects/notes/discussions/_active.html.haml delete mode 100644 app/views/projects/notes/discussions/_commit.html.haml delete mode 100644 app/views/projects/notes/discussions/_diff.html.haml create mode 100644 app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml create mode 100644 app/views/projects/notes/discussions/_notes.html.haml delete mode 100644 app/views/projects/notes/discussions/_outdated.html.haml create mode 100644 db/migrate/20160508215820_add_type_to_notes.rb create mode 100644 db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index efb3e8e219..6d9d6528f4 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -285,6 +285,7 @@ class @Notes form.addClass "js-main-target-form" form.find("#note_line_code").remove() + form.find("#note_type").remove() ### General note form setup. @@ -472,6 +473,7 @@ class @Notes setupDiscussionNoteForm: (dataHolder, form) => # setup note target form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}" + form.find("#note_type").val dataHolder.data("noteType") form.find("#line_type").val dataHolder.data("lineType") form.find("#note_commit_id").val dataHolder.data("commitId") form.find("#note_line_code").val dataHolder.data("lineCode") diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 624c8249f7..a3e1ac13a4 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -226,8 +226,7 @@ ul.notes { } } -.note-action-button, -.discussion-action-button { +.note-action-button { display: inline-block; margin-left: 10px; line-height: 24px; diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index a202cb3869..9bcb82ef3f 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -17,11 +17,12 @@ class Projects::CommitController < Projects::ApplicationController def show apply_diff_view_cookie! - @line_notes = commit.notes.inline + @grouped_diff_notes = commit.notes.grouped_diff_notes + @note = @project.build_commit_note(commit) - @notes = commit.notes.not_inline.fresh + @notes = commit.notes.non_diff_notes.fresh @noteable = @commit - @comments_allowed = @reply_allowed = true + @comments_allowed = true @comments_target = { noteable_type: 'Commit', commit_id: @commit.id @@ -67,10 +68,10 @@ class Projects::CommitController < Projects::ApplicationController create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", success_path: successful_change_path, failure_path: failed_change_path) end - + def cherry_pick assign_change_commit_vars(@commit.cherry_pick_branch_name) - + return render_404 if @target_branch.blank? create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 671d5c2302..587b84fd92 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -22,7 +22,7 @@ class Projects::CompareController < Projects::ApplicationController @base_commit = @project.merge_base_commit(@base_ref, @head_ref) @diffs = compare.diffs(diff_options) @diff_refs = [@base_commit, @commit] - @line_notes = [] + @grouped_diff_notes = {} end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 9c147b3689..e571da64e4 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -73,12 +73,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController # but we need it for the "View file @ ..." link by deleted files @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit - @comments_allowed = @reply_allowed = true + @comments_allowed = true @comments_target = { noteable_type: 'MergeRequest', noteable_id: @merge_request.id } - @line_notes = @merge_request.notes.where("line_code is not null") + + @grouped_diff_notes = @merge_request.notes.grouped_diff_notes respond_to do |format| format.html @@ -300,7 +301,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh - @discussions = Note.discussions_from_notes(@notes) + @discussions = @notes.discussions @noteable = @merge_request # Get commits from repository diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 707a0d0e5c..4a57cd29a2 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -96,7 +96,7 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_html(note) - return unless note.for_diff_line? + return unless note.diff_note? if params[:view] == 'parallel' template = "projects/notes/_diff_notes_with_reply_parallel" @@ -120,7 +120,7 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_with_diff_html(note) - return unless note.for_diff_line? + return unless note.diff_note? render_to_string( "projects/notes/_discussion", @@ -158,7 +158,7 @@ class Projects::NotesController < Projects::ApplicationController def note_params params.require(:note).permit( :note, :noteable, :noteable_id, :noteable_type, :project_id, - :attachment, :line_code, :commit_id + :attachment, :line_code, :commit_id, :type ) end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index fa4c635f55..c41be33353 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -10,7 +10,7 @@ class NotesFinder notes = case target_type when "commit" - project.notes.for_commit_id(target_id).not_inline + project.notes.for_commit_id(target_id).non_diff_notes when "issue" project.issues.find(target_id).notes.nonawards.inc_author when "merge_request" diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 9f73edb455..5f311f3780 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -55,22 +55,18 @@ module DiffHelper end end - def line_comments - @line_comments ||= @line_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) - end + def organize_comments(left, right) + notes_left = notes_right = nil - def organize_comments(type_left, type_right, line_code_left, line_code_right) - comments_left = comments_right = nil - - unless type_left.nil? && type_right == 'new' - comments_left = line_comments[line_code_left] + unless left[:type].nil? && right[:type] == 'new' + notes_left = @grouped_diff_notes[left[:line_code]] end - unless type_left.nil? && type_right.nil? - comments_right = line_comments[line_code_right] + unless left[:type].nil? && right[:type].nil? + notes_right = @grouped_diff_notes[right[:line_code]] end - [comments_left, comments_right] + [notes_left, notes_right] end def inline_diff_btn @@ -96,8 +92,8 @@ module DiffHelper ].join(' ').html_safe end - def commit_for_diff(diff) - if diff.deleted_file + def commit_for_diff(diff_file) + if diff_file.deleted_file @base_commit || @commit.parent || @commit else @commit diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 95072b5373..b401c8385b 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -1,7 +1,7 @@ module NotesHelper # Helps to distinguish e.g. commit notes in mr notes list def note_for_main_target?(note) - (@noteable.class.name == note.noteable_type && !note.for_diff_line?) + @noteable.class.name == note.noteable_type && !note.diff_note? end def note_target_fields(note) @@ -15,16 +15,6 @@ module NotesHelper note.editable? && can?(current_user, :admin_note, note) end - def link_to_commit_diff_line_note(note) - if note.for_commit_diff_line? - link_to( - "#{note.diff_file_name}:L#{note.diff_new_line}", - namespace_project_commit_path(@project.namespace, @project, - note.noteable, anchor: note.line_code) - ) - end - end - def noteable_json(noteable) { id: noteable.id, @@ -35,7 +25,7 @@ module NotesHelper end def link_to_new_diff_note(line_code, line_type = nil) - discussion_id = Note.build_discussion_id( + discussion_id = LegacyDiffNote.build_discussion_id( @comments_target[:noteable_type], @comments_target[:noteable_id] || @comments_target[:commit_id], line_code @@ -45,9 +35,10 @@ module NotesHelper noteable_type: @comments_target[:noteable_type], noteable_id: @comments_target[:noteable_id], commit_id: @comments_target[:commit_id], + line_type: line_type, line_code: line_code, - discussion_id: discussion_id, - line_type: line_type + note_type: LegacyDiffNote.name, + discussion_id: discussion_id } button_tag(class: 'btn add-diff-note js-add-diff-note-button', @@ -57,18 +48,24 @@ module NotesHelper end end - def link_to_reply_diff(note, line_type = nil) + def link_to_reply_discussion(note, line_type = nil) return unless current_user data = { noteable_type: note.noteable_type, noteable_id: note.noteable_id, commit_id: note.commit_id, - line_code: note.line_code, discussion_id: note.discussion_id, line_type: line_type } + if note.diff_note? + data.merge!( + line_code: note.line_code, + note_type: LegacyDiffNote.name + ) + end + button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', data: data, title: 'Add a reply' end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb new file mode 100644 index 0000000000..b5de85df99 --- /dev/null +++ b/app/models/legacy_diff_note.rb @@ -0,0 +1,182 @@ +class LegacyDiffNote < Note + serialize :st_diff + + validates :line_code, presence: true, line_code: true + + before_create :set_diff + + class << self + def build_discussion_id(noteable_type, noteable_id, line_code, active = true) + [super(noteable_type, noteable_id), line_code, active].join("-") + end + end + + def diff_note? + true + end + + def legacy_diff_note? + true + end + + def discussion_id + @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code, active?) + end + + def find_diff + return nil unless noteable + return @diff if defined?(@diff) + + # Don't use ||= because nil is a valid value for @diff + @diff = noteable.diffs(Commit.max_diff_options).find do |d| + Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path + end + end + + def set_diff + # First lets find notes with same diff + # before iterating over all mr diffs + diff = diff_for_line_code unless for_merge_request? + diff ||= find_diff + + self.st_diff = diff.to_hash if diff + end + + def diff + @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) + end + + def diff_for_line_code + attributes = { + noteable_type: noteable_type, + line_code: line_code + } + + if for_commit? + attributes[:commit_id] = commit_id + else + attributes[:noteable_id] = noteable_id + end + + self.class.where(attributes).last.try(:diff) + end + + # Check if this note is part of an "active" discussion + # + # This will always return true for anything except MergeRequest noteables, + # which have special logic. + # + # If the note's current diff cannot be matched in the MergeRequest's current + # diff, it's considered inactive. + def active? + return true if for_commit? + return true unless self.diff + return false unless noteable + return @active if defined?(@active) + + noteable_diff = find_noteable_diff + + if noteable_diff + parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) + + @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } + else + @active = false + end + + @active + end + + def diff_file_index + line_code.split('_')[0] if line_code + end + + def diff_file_name + diff.new_path if diff + end + + def file_path + if diff.new_path.present? + diff.new_path + elsif diff.old_path.present? + diff.old_path + end + end + + def diff_old_line + line_code.split('_')[1].to_i if line_code + end + + def diff_new_line + line_code.split('_')[2].to_i if line_code + end + + def generate_line_code(line) + Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + end + + def diff_line + return @diff_line if @diff_line + + if diff + diff_lines.each do |line| + if generate_line_code(line) == self.line_code + @diff_line = line.text + end + end + end + + @diff_line + end + + def diff_line_type + return @diff_line_type if @diff_line_type + + if diff + diff_lines.each do |line| + if generate_line_code(line) == self.line_code + @diff_line_type = line.type + end + end + end + + @diff_line_type + end + + def truncated_diff_lines + max_number_of_lines = 16 + prev_match_line = nil + prev_lines = [] + + highlighted_diff_lines.each do |line| + if line.type == "match" + prev_lines.clear + prev_match_line = line + else + prev_lines << line + + break if generate_line_code(line) == self.line_code + + prev_lines.shift if prev_lines.length >= max_number_of_lines + end + end + + prev_lines + end + + def diff_lines + @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) + end + + def highlighted_diff_lines + Gitlab::Diff::Highlight.new(diff_lines).highlight + end + + private + + # Find the diff on noteable that matches our own + def find_noteable_diff + diffs = noteable.diffs(Commit.max_diff_options) + diffs.find { |d| d.new_path == self.diff.new_path } + end +end diff --git a/app/models/note.rb b/app/models/note.rb index f26aa1bf63..3bc5587070 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -1,6 +1,5 @@ -require 'carrierwave/orm/activerecord' - class Note < ActiveRecord::Base + extend ActiveModel::Naming include Gitlab::CurrentSettings include Participable include Mentionable @@ -22,12 +21,10 @@ class Note < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true before_validation :set_award! - before_validation :clear_blank_line_code! validates :note, :project, presence: true validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award } - validates :line_code, line_code: true, allow_blank: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } @@ -41,8 +38,6 @@ class Note < ActiveRecord::Base scope :awards, ->{ where(is_award: true) } scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } - scope :inline, ->{ where("line_code IS NOT NULL") } - scope :not_inline, ->{ where(line_code: nil) } scope :system, ->{ where(system: true) } scope :user, ->{ where(system: false) } scope :common, ->{ where(noteable_type: ["", nil]) } @@ -50,38 +45,31 @@ class Note < ActiveRecord::Base scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } + scope :legacy_diff_notes, ->{ where(type: 'LegacyDiffNote') } + scope :non_diff_notes, ->{ where(type: ['Note', nil]) } + scope :with_associations, -> do includes(:author, :noteable, :updated_by, project: [:project_members, { group: [:group_members] }]) end - serialize :st_diff - before_create :set_diff, if: ->(n) { n.line_code.present? } + before_validation :clear_blank_line_code! class << self - def discussions_from_notes(notes) - discussion_ids = [] - discussions = [] - - notes.each do |note| - next if discussion_ids.include?(note.discussion_id) - - # don't group notes for the main target - if !note.for_diff_line? && note.for_merge_request? - discussions << [note] - else - discussions << notes.select do |other_note| - note.discussion_id == other_note.discussion_id - end - discussion_ids << note.discussion_id - end - end - - discussions + def model_name + ActiveModel::Name.new(self, nil, 'note') end - def build_discussion_id(type, id, line_code) - [:discussion, type.try(:underscore), id, line_code].join("-").to_sym + def build_discussion_id(noteable_type, noteable_id) + [:discussion, noteable_type.try(:underscore), noteable_id].join("-") + end + + def discussions + all.group_by(&:discussion_id).values + end + + def grouped_diff_notes + legacy_diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) end # Searches for notes matching the given query. @@ -116,167 +104,35 @@ class Note < ActiveRecord::Base system && SystemNoteService.cross_reference?(note) end - def max_attachment_size - current_application_settings.max_attachment_size.megabytes.to_i + def diff_note? + false end - def find_diff - return nil unless noteable - return @diff if defined?(@diff) + def legacy_diff_note? + false + end - # Don't use ||= because nil is a valid value for @diff - @diff = noteable.diffs(Commit.max_diff_options).find do |d| - Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path - end + def discussion_id + @discussion_id ||= + if for_merge_request? + [:discussion, :note, id].join("-") + else + self.class.build_discussion_id(noteable_type, noteable_id || commit_id) + end + end + + def max_attachment_size + current_application_settings.max_attachment_size.megabytes.to_i end def hook_attrs attributes end - def set_diff - # First lets find notes with same diff - # before iterating over all mr diffs - diff = diff_for_line_code unless for_merge_request? - diff ||= find_diff - - self.st_diff = diff.to_hash if diff - end - - def diff - @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) - end - - def diff_for_line_code - Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff) - end - - # Check if this note is part of an "active" discussion - # - # This will always return true for anything except MergeRequest noteables, - # which have special logic. - # - # If the note's current diff cannot be matched in the MergeRequest's current - # diff, it's considered inactive. - def active? - return true unless self.diff - return false unless noteable - return @active if defined?(@active) - - noteable_diff = find_noteable_diff - - if noteable_diff - parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) - - @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } - else - @active = false - end - - @active - end - - def diff_file_index - line_code.split('_')[0] if line_code - end - - def diff_file_name - diff.new_path if diff - end - - def file_path - if diff.new_path.present? - diff.new_path - elsif diff.old_path.present? - diff.old_path - end - end - - def diff_old_line - line_code.split('_')[1].to_i if line_code - end - - def diff_new_line - line_code.split('_')[2].to_i if line_code - end - - def generate_line_code(line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) - end - - def diff_line - return @diff_line if @diff_line - - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line = line.text - end - end - end - - @diff_line - end - - def diff_line_type - return @diff_line_type if @diff_line_type - - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line_type = line.type - end - end - end - - @diff_line_type - end - - def truncated_diff_lines - max_number_of_lines = 16 - prev_match_line = nil - prev_lines = [] - - highlighted_diff_lines.each do |line| - if line.type == "match" - prev_lines.clear - prev_match_line = line - else - prev_lines << line - - break if generate_line_code(line) == self.line_code - - prev_lines.shift if prev_lines.length >= max_number_of_lines - end - end - - prev_lines - end - - def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) - end - - def highlighted_diff_lines - Gitlab::Diff::Highlight.new(diff_lines).highlight - end - - def discussion_id - @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) - end - def for_commit? noteable_type == "Commit" end - def for_commit_diff_line? - for_commit? && for_diff_line? - end - - def for_diff_line? - line_code.present? - end - def for_issue? noteable_type == "Issue" end @@ -285,10 +141,6 @@ class Note < ActiveRecord::Base noteable_type == "MergeRequest" end - def for_merge_request_diff_line? - for_merge_request? && for_diff_line? - end - def for_snippet? noteable_type == "Snippet" end @@ -361,14 +213,8 @@ class Note < ActiveRecord::Base self.line_code = nil if self.line_code.blank? end - # Find the diff on noteable that matches our own - def find_noteable_diff - diffs = noteable.diffs(Commit.max_diff_options) - diffs.find { |d| d.new_path == self.diff.new_path } - end - def awards_supported? - (for_issue? || for_merge_request?) && !for_diff_line? + (for_issue? || for_merge_request?) && !diff_note? end def contains_emoji_only? diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index 65f0e4c406..27e8ea5f5a 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,4 +1,4 @@ -- if @note.diff_file_name +- if @note.legacy_diff_note? %p.details New comment on diff for = link_to @note.diff_file_name, @target_url diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 81948513e4..3d91dc4312 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -30,13 +30,13 @@ %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) - = link_to_new_diff_note(right[:line_code], 'new') + = link_to_new_diff_note(new_line_code, 'new') %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - - if @reply_allowed - - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) - - if comments_left.present? || comments_right.present? - = render "projects/notes/diff_notes_with_reply_parallel", notes_left: comments_left, notes_right: comments_right + - if @comments_allowed + - notes_left, notes_right = organize_comments(left, right) + - if notes_left.present? || notes_right.present? + = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right - if diff_file.diff.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index e7169d7b59..d58444a9d5 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -6,16 +6,15 @@ %table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' } - last_line = 0 - - raw_diff_lines = diff_file.diff_lines.to_a - diff_file.highlighted_diff_lines.each_with_index do |line, index| - line_code = generate_line_code(diff_file.file_path, line) - last_line = line.new_pos = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code} - - if @reply_allowed - - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) - - unless comments.empty? - = render "projects/notes/diff_notes_with_reply", notes: comments, line: raw_diff_lines[index].text + - if @comments_allowed + - diff_notes = @grouped_diff_notes[line_code] + - if diff_notes + = render "projects/notes/diff_notes_with_reply", notes: diff_notes - if last_line > 0 = render "projects/diffs/match_line", { line: "", diff --git a/app/views/projects/notes/_commit_discussion.html.haml b/app/views/projects/notes/_commit_discussion.html.haml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index 39be072855..8144c1ba49 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -1,10 +1,8 @@ -- note = notes.first # example note --# Check if line want not changed since comment was left -- if !defined?(line) || line == note.diff_line - %tr.notes_holder - %td.notes_line{ colspan: 2 } - %td.notes_content - %ul.notes{ data: { discussion_id: note.discussion_id } } - = render notes - .discussion-reply-holder - = link_to_reply_diff(note) +- note = notes.first +%tr.notes_holder + %td.notes_line{ colspan: 2 } + %td.notes_content + %ul.notes{ data: { discussion_id: note.discussion_id } } + = render partial: "projects/notes/note", collection: notes, as: :note + .discussion-reply-holder + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index f8aa5e2fa7..45986b0d1e 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -1,27 +1,27 @@ -- note1 = notes_left.present? ? notes_left.first : nil -- note2 = notes_right.present? ? notes_right.first : nil +- note_left = notes_left.present? ? notes_left.first : nil +- note_right = notes_right.present? ? notes_right.first : nil %tr.notes_holder - - if note1 + - if note_left %td.notes_line.old %td.notes_content.parallel.old - %ul.notes{ data: { discussion_id: note1.discussion_id } } - = render notes_left + %ul.notes{ data: { discussion_id: note_left.discussion_id } } + = render partial: "projects/notes/note", collection: notes_left, as: :note .discussion-reply-holder - = link_to_reply_diff(note1, 'old') + = link_to_reply_discussion(note_left, 'old') - else %td.notes_line.old= "" %td.notes_content.parallel.old= "" - - if note2 + - if note_right %td.notes_line.new %td.notes_content.parallel.new - %ul.notes{ data: { discussion_id: note2.discussion_id } } - = render notes_right + %ul.notes{ data: { discussion_id: note_right.discussion_id } } + = render partial: "projects/notes/note", collection: notes_right, as: :note .discussion-reply-holder - = link_to_reply_diff(note2, 'new') + = link_to_reply_discussion(note_right, 'new') - else %td.notes_line.new= "" %td.notes_content.parallel.new= "" diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index 572b00a38c..40e03050a7 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -1,13 +1,46 @@ - note = discussion_notes.first +- expanded = !note.diff_note? || note.active? %li.note.note-discussion.timeline-entry .timeline-entry-inner .timeline-icon = link_to user_path(note.author) do - = image_tag avatar_icon(note.author_email), class: "avatar s40" + = image_tag avatar_icon(note.author), class: "avatar s40" .timeline-content - - if note.for_merge_request? - - (active_notes, outdated_notes) = discussion_notes.partition(&:active?) - = render "projects/notes/discussions/active", discussion_notes: active_notes if active_notes.length > 0 - = render "projects/notes/discussions/outdated", discussion_notes: outdated_notes if outdated_notes.length > 0 - - else - = render "projects/notes/discussions/commit", discussion_notes: discussion_notes + .discussion.js-toggle-container{ class: note.discussion_id } + .discussion-header + = link_to_member(@project, note.author, avatar: false) + + .inline.discussion-headline-light + = note.author.to_reference + started a discussion on + + - if note.for_commit? + - commit = note.noteable + - if commit + commit + = link_to commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code), class: 'monospace' + - else + a deleted commit + - else + - if note.active? + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do + the diff + - else + the outdated diff + + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago") + + .discussion-actions + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do + - if expanded + = icon("chevron-up") + - else + = icon("chevron-down") + + Toggle discussion + + .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } + - if note.diff_note? + = render "projects/notes/discussions/legacy_diff_with_notes", discussion_notes: discussion_notes + - else + = render "projects/notes/discussions/notes", discussion_notes: discussion_notes diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index d0ac380f21..67ed38a7b2 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -6,6 +6,7 @@ = f.hidden_field :line_code = f.hidden_field :noteable_id = f.hidden_field :noteable_type + = f.hidden_field :type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index aeb7c1d5ee..9fbc9a4554 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,5 +1,8 @@ +- return unless note.author +- return if note.cross_reference_not_visible_for?(current_user) + - note_editable = note_editable?(note) -%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } +%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} @@ -8,8 +11,8 @@ .note-header = link_to_member(note.project, note.author, avatar: false) .inline.note-headline-light - = "#{note.author.to_reference}" - - if !note.system + = note.author.to_reference + - unless note.system commented %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml index 62db86fb18..ebf7e8a9cb 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/projects/notes/_notes.html.haml @@ -2,14 +2,9 @@ - @discussions.each do |discussion_notes| - note = discussion_notes.first - if note_for_main_target?(note) - - next if note.cross_reference_not_visible_for?(current_user) - - = render discussion_notes + = render partial: "projects/notes/note", object: note, as: :note - else = render 'projects/notes/discussion', discussion_notes: discussion_notes - else - @notes.each do |note| - - next unless note.author - - next if note.cross_reference_not_visible_for?(current_user) - - = render note + = render partial: "projects/notes/note", object: note, as: :note diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml deleted file mode 100644 index 0ea8862a68..0000000000 --- a/app/views/projects/notes/discussions/_active.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- note = discussion_notes.first -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion" - = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do - on the diff - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-up - Show/hide discussion - - .discussion-body.js-toggle-content - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml deleted file mode 100644 index 2a2ead58ee..0000000000 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -- note = discussion_notes.first -- commit = note.noteable -- commit_description = commit ? 'commit' : 'a deleted commit' -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion on #{commit_description}" - - if commit - = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-up - Show/hide discussion - .discussion-body.js-toggle-content - - if note.for_diff_line? - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note - - else - .panel.panel-default - .notes{ data: { discussion_id: discussion_notes.first.discussion_id } } - %ul.notes.timeline - = render discussion_notes - .discussion-reply-holder - = link_to_reply_diff(discussion_notes.first) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml deleted file mode 100644 index d46aab000c..0000000000 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- diff = note.diff -- if diff - .diff-file - .diff-header - %span - - if diff.deleted_file - = diff.old_path - - else - = diff.new_path - - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode - %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" - .diff-content.code.js-syntax-highlight - %table - - note.truncated_diff_lines.each do |line| - - type = line.type - - line_code = generate_line_code(note.file_path, line) - %tr.line_holder{ id: line_code, class: "#{type}" } - - if type == "match" - %td.old_line.diff-line-num= "..." - %td.new_line.diff-line-num= "..." - %td.line_content.match= line.text - - else - %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } - %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } - %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) - - - if line_code == note.line_code - = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml new file mode 100644 index 0000000000..3ab11f6461 --- /dev/null +++ b/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml @@ -0,0 +1,30 @@ +- note = discussion_notes.first +- diff = note.diff +- return unless diff + +.diff-file + .diff-header + %span + - if diff.deleted_file + = diff.old_path + - else + = diff.new_path + - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode + %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" + .diff-content.code.js-syntax-highlight + %table + - note.truncated_diff_lines.each do |line| + - type = line.type + - line_code = generate_line_code(note.file_path, line) + %tr.line_holder{ id: line_code, class: "#{type}" } + - if type == "match" + %td.old_line.diff-line-num= "..." + %td.new_line.diff-line-num= "..." + %td.line_content.match= line.text + - else + %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } + %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } + %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) + + - if line_code == note.line_code + = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_notes.html.haml b/app/views/projects/notes/discussions/_notes.html.haml new file mode 100644 index 0000000000..e598e3c7c6 --- /dev/null +++ b/app/views/projects/notes/discussions/_notes.html.haml @@ -0,0 +1,7 @@ +- note = discussion_notes.first +.panel.panel-default + .notes{ data: { discussion_id: note.discussion_id } } + %ul.notes.timeline + = render partial: "projects/notes/note", collection: discussion_notes, as: :note + .discussion-reply-holder + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml deleted file mode 100644 index 45141bcd1d..0000000000 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- note = discussion_notes.first -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion" - on the outdated diff - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-down - Show/hide discussion - .discussion-body.js-toggle-content.hide - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/db/migrate/20160508215820_add_type_to_notes.rb b/db/migrate/20160508215820_add_type_to_notes.rb new file mode 100644 index 0000000000..58944d4e65 --- /dev/null +++ b/db/migrate/20160508215820_add_type_to_notes.rb @@ -0,0 +1,5 @@ +class AddTypeToNotes < ActiveRecord::Migration + def change + add_column :notes, :type, :string + end +end diff --git a/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb new file mode 100644 index 0000000000..c3f23d89d5 --- /dev/null +++ b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb @@ -0,0 +1,5 @@ +class SetTypeOnLegacyDiffNotes < ActiveRecord::Migration + def change + execute "UPDATE notes SET type = 'LegacyDiffNote' WHERE line_code IS NOT NULL" + end +end diff --git a/db/schema.rb b/db/schema.rb index 9b5aa640cb..e1117a0d85 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -632,10 +632,11 @@ ActiveRecord::Schema.define(version: 20160509201028) do t.string "line_code" t.string "commit_id" t.integer "noteable_id" - t.boolean "system", default: false, null: false + t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" - t.boolean "is_award", default: false, null: false + t.boolean "is_award", default: false, null: false + t.string "type" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 93a3a5ce08..4a11c8e362 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -107,6 +107,8 @@ module API break if opts[:line_code] end + + opts[:type] = LegacyDiffNote.name if opts[:line_code] end note = ::Notes::CreateService.new(user_project, current_user, opts).execute diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 406f5ea913..1619199b0a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -227,9 +227,9 @@ module API class CommitNote < Grape::Entity expose :note - expose(:path) { |note| note.diff_file_name } - expose(:line) { |note| note.diff_new_line } - expose(:line_type) { |note| note.diff_line_type } + expose(:path) { |note| note.diff_file_name if note.legacy_diff_note? } + expose(:line) { |note| note.diff_new_line if note.legacy_diff_note? } + expose(:line_type) { |note| note.diff_line_type if note.legacy_diff_note? } expose :author, using: Entities::UserBasic expose :created_at end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 4b788b5788..264888cb37 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -43,12 +43,8 @@ describe Note, models: true do expect(note.noteable.id).to eq(commit.id) end - it "should be recognized by #for_diff_line?" do - expect(note).to be_for_diff_line - end - - it "should be recognized by #for_commit_diff_line?" do - expect(note).to be_for_commit_diff_line + it "should be recognized by #legacy_diff_note?" do + expect(note).to be_legacy_diff_note end end From 9a3ed265e9c82ee55bc6a17737ef6c5c2a1ad7e3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 14:53:04 -0500 Subject: [PATCH 211/449] Use `@diff_notes_disabled` instead of `@comments_allowed` --- app/controllers/projects/commit_controller.rb | 1 - app/controllers/projects/compare_controller.rb | 1 + app/controllers/projects/merge_requests_controller.rb | 2 +- app/views/projects/diffs/_line.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 6 +++--- app/views/projects/diffs/_text_file.html.haml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 9bcb82ef3f..6e02f1240f 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -22,7 +22,6 @@ class Projects::CommitController < Projects::ApplicationController @note = @project.build_commit_note(commit) @notes = commit.notes.non_diff_notes.fresh @noteable = @commit - @comments_allowed = true @comments_target = { noteable_type: 'Commit', commit_id: @commit.id diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 587b84fd92..af0b69a244 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -22,6 +22,7 @@ class Projects::CompareController < Projects::ApplicationController @base_commit = @project.merge_base_commit(@base_ref, @head_ref) @diffs = compare.diffs(diff_options) @diff_refs = [@base_commit, @commit] + @diff_notes_disabled = true @grouped_diff_notes = {} end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e571da64e4..c5757a2462 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -73,7 +73,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController # but we need it for the "View file @ ..." link by deleted files @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit - @comments_allowed = true @comments_target = { noteable_type: 'MergeRequest', noteable_id: @merge_request.id @@ -118,6 +117,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.last_commit @base_commit = @merge_request.diff_base_commit @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare + @diff_notes_disabled = true @ci_commit = @merge_request.ci_commit @statuses = @ci_commit.statuses if @ci_commit diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 107097ad96..f1577e8a47 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -15,7 +15,7 @@ = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " ".html_safe : line.new_pos diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 3d91dc4312..4ecc9528bd 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -16,7 +16,7 @@ - else %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) @@ -29,11 +29,11 @@ %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(new_line_code, 'new') %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - - if @comments_allowed + - unless @diff_notes_disabled - notes_left, notes_right = organize_comments(left, right) - if notes_left.present? || notes_right.present? = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index d58444a9d5..068593a7dd 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -11,7 +11,7 @@ - last_line = line.new_pos = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code} - - if @comments_allowed + - unless @diff_notes_disabled - diff_notes = @grouped_diff_notes[line_code] - if diff_notes = render "projects/notes/diff_notes_with_reply", notes: diff_notes From 7848d54f5b0024f58d8ce2b0259347816be5bdbc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 14:53:31 -0500 Subject: [PATCH 212/449] Clean up LegacyDiffNote somewhat --- app/models/legacy_diff_note.rb | 183 ++++++++---------- app/models/note.rb | 4 + .../notify/note_merge_request_email.html.haml | 2 +- .../_legacy_diff_with_notes.html.haml | 2 +- lib/api/entities.rb | 2 +- spec/models/legacy_diff_note_spec.rb | 74 +++++++ 6 files changed, 160 insertions(+), 107 deletions(-) create mode 100644 spec/models/legacy_diff_note_spec.rb diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index b5de85df99..bbefc911b2 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -23,86 +23,10 @@ class LegacyDiffNote < Note @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code, active?) end - def find_diff - return nil unless noteable - return @diff if defined?(@diff) - - # Don't use ||= because nil is a valid value for @diff - @diff = noteable.diffs(Commit.max_diff_options).find do |d| - Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path - end - end - - def set_diff - # First lets find notes with same diff - # before iterating over all mr diffs - diff = diff_for_line_code unless for_merge_request? - diff ||= find_diff - - self.st_diff = diff.to_hash if diff - end - - def diff - @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) - end - - def diff_for_line_code - attributes = { - noteable_type: noteable_type, - line_code: line_code - } - - if for_commit? - attributes[:commit_id] = commit_id - else - attributes[:noteable_id] = noteable_id - end - - self.class.where(attributes).last.try(:diff) - end - - # Check if this note is part of an "active" discussion - # - # This will always return true for anything except MergeRequest noteables, - # which have special logic. - # - # If the note's current diff cannot be matched in the MergeRequest's current - # diff, it's considered inactive. - def active? - return true if for_commit? - return true unless self.diff - return false unless noteable - return @active if defined?(@active) - - noteable_diff = find_noteable_diff - - if noteable_diff - parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) - - @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } - else - @active = false - end - - @active - end - - def diff_file_index + def diff_file_hash line_code.split('_')[0] if line_code end - def diff_file_name - diff.new_path if diff - end - - def file_path - if diff.new_path.present? - diff.new_path - elsif diff.old_path.present? - diff.old_path - end - end - def diff_old_line line_code.split('_')[1].to_i if line_code end @@ -111,36 +35,32 @@ class LegacyDiffNote < Note line_code.split('_')[2].to_i if line_code end - def generate_line_code(line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + def diff + @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) + end + + def diff_file_path + diff.new_path.presence || diff.old_path + end + + def diff_lines + @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) end def diff_line - return @diff_line if @diff_line + @diff_line ||= diff_lines.find { |line| generate_line_code(line) == self.line_code } + end - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line = line.text - end - end - end - - @diff_line + def diff_line_text + diff_line.try(:text) end def diff_line_type - return @diff_line_type if @diff_line_type + diff_line.try(:type) + end - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line_type = line.type - end - end - end - - @diff_line_type + def highlighted_diff_lines + Gitlab::Diff::Highlight.new(diff_lines).highlight end def truncated_diff_lines @@ -164,16 +84,71 @@ class LegacyDiffNote < Note prev_lines end - def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) - end + # Check if this note is part of an "active" discussion + # + # This will always return true for anything except MergeRequest noteables, + # which have special logic. + # + # If the note's current diff cannot be matched in the MergeRequest's current + # diff, it's considered inactive. + def active? + return @active if defined?(@active) + return true if for_commit? + return true unless self.diff + return false unless noteable - def highlighted_diff_lines - Gitlab::Diff::Highlight.new(diff_lines).highlight + noteable_diff = find_noteable_diff + + if noteable_diff + parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) + + @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line_text } + else + @active = false + end + + @active end private + def find_diff + return nil unless noteable + return @diff if defined?(@diff) + + @diff = noteable.diffs(Commit.max_diff_options).find do |d| + d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash + end + end + + def set_diff + # First lets find notes with same diff + # before iterating over all mr diffs + diff = diff_for_line_code unless for_merge_request? + diff ||= find_diff + + self.st_diff = diff.to_hash if diff + end + + def diff_for_line_code + attributes = { + noteable_type: noteable_type, + line_code: line_code + } + + if for_commit? + attributes[:commit_id] = commit_id + else + attributes[:noteable_id] = noteable_id + end + + self.class.where(attributes).last.try(:diff) + end + + def generate_line_code(line) + Gitlab::Diff::LineCode.generate(diff_file_path, line.new_pos, line.old_pos) + end + # Find the diff on noteable that matches our own def find_noteable_diff diffs = noteable.diffs(Commit.max_diff_options) diff --git a/app/models/note.rb b/app/models/note.rb index 3bc5587070..7e5bdc09a8 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -112,6 +112,10 @@ class Note < ActiveRecord::Base false end + def active? + true + end + def discussion_id @discussion_id ||= if for_merge_request? diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index 27e8ea5f5a..a3643a00cf 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,7 @@ - if @note.legacy_diff_note? %p.details New comment on diff for - = link_to @note.diff_file_name, @target_url + = link_to @note.diff_file_path, @target_url \: = render 'note_message' diff --git a/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml index 3ab11f6461..6401245bf7 100644 --- a/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml +++ b/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml @@ -15,7 +15,7 @@ %table - note.truncated_diff_lines.each do |line| - type = line.type - - line_code = generate_line_code(note.file_path, line) + - line_code = generate_line_code(note.diff_file_path, line) %tr.line_holder{ id: line_code, class: "#{type}" } - if type == "match" %td.old_line.diff-line-num= "..." diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 1619199b0a..93a5798e21 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -227,7 +227,7 @@ module API class CommitNote < Grape::Entity expose :note - expose(:path) { |note| note.diff_file_name if note.legacy_diff_note? } + expose(:path) { |note| note.diff_file_path if note.legacy_diff_note? } expose(:line) { |note| note.diff_new_line if note.legacy_diff_note? } expose(:line_type) { |note| note.diff_line_type if note.legacy_diff_note? } expose :author, using: Entities::UserBasic diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb new file mode 100644 index 0000000000..7c29bef54e --- /dev/null +++ b/spec/models/legacy_diff_note_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe LegacyDiffNote, models: true do + describe "Commit diff line notes" do + let!(:note) { create(:note_on_commit_diff, note: "+1 from me") } + let!(:commit) { note.noteable } + + it "should save a valid note" do + expect(note.commit_id).to eq(commit.id) + expect(note.noteable.id).to eq(commit.id) + end + + it "should be recognized by #legacy_diff_note?" do + expect(note).to be_legacy_diff_note + end + end + + describe '#active?' do + it 'is always true when the note has no associated diff' do + note = build(:note_on_merge_request_diff) + + expect(note).to receive(:diff).and_return(nil) + + expect(note).to be_active + end + + it 'is never true when the note has no noteable associated' do + note = build(:note_on_merge_request_diff) + + expect(note).to receive(:diff).and_return(double) + expect(note).to receive(:noteable).and_return(nil) + + expect(note).not_to be_active + end + + it 'returns the memoized value if defined' do + note = build(:note_on_merge_request_diff) + + note.instance_variable_set(:@active, 'foo') + expect(note).not_to receive(:find_noteable_diff) + + expect(note.active?).to eq 'foo' + end + + context 'for a merge request noteable' do + it 'is false when noteable has no matching diff' do + merge = build_stubbed(:merge_request, :simple) + note = build(:note_on_merge_request_diff, noteable: merge) + + allow(note).to receive(:diff).and_return(double) + expect(note).to receive(:find_noteable_diff).and_return(nil) + + expect(note).not_to be_active + end + + it 'is true when noteable has a matching diff' do + merge = create(:merge_request, :simple) + + # Generate a real line_code value so we know it will match. We use a + # random line from a random diff just for funsies. + diff = merge.diffs.to_a.sample + line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample + code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) + + # We're persisting in order to trigger the set_diff callback + note = create(:note_on_merge_request_diff, noteable: merge, line_code: code) + + # Make sure we don't get a false positive from a guard clause + expect(note).to receive(:find_noteable_diff).and_call_original + expect(note).to be_active + end + end + end +end From c452fa8124ffe18e2e74e14491dcb3e419e60057 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 14:53:42 -0500 Subject: [PATCH 213/449] Update specs --- features/steps/shared/diff_note.rb | 12 +-- spec/factories/notes.rb | 4 +- spec/features/notes_on_merge_requests_spec.rb | 2 +- spec/models/note_spec.rb | 76 +------------------ 4 files changed, 10 insertions(+), 84 deletions(-) diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index e846c52d47..e8b1e4b487 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -23,7 +23,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Typo, please fix" find(".js-comment-button").trigger("click") sleep 0.05 @@ -33,7 +33,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do click_parallel_diff_line(sample_commit.line_code, 'old') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Old comment" find(".js-comment-button").trigger("click") end @@ -41,7 +41,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the right side like "New comment"' do click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") end @@ -51,7 +51,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Should fix it :smile:" find('.js-md-preview-button').click end @@ -62,7 +62,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.del_line_code) - page.within("form[id$='#{sample_commit.del_line_code}']") do + page.within("form[id$='#{sample_commit.del_line_code}-true']") do fill_in "note[note]", with: "DRY this up" find('.js-md-preview-button').click end @@ -91,7 +91,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in 'note[note]', with: ':smile:' click_button('Comment') end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 840b13196a..26719f2652 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -9,10 +9,10 @@ FactoryGirl.define do author factory :note_on_commit, traits: [:on_commit] - factory :note_on_commit_diff, traits: [:on_commit, :on_diff] + factory :note_on_commit_diff, traits: [:on_commit, :on_diff], class: LegacyDiffNote factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] factory :note_on_merge_request, traits: [:on_merge_request] - factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] + factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] factory :downvote_note, traits: [:award, :downvote] diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 389812ff7e..9e9fec0194 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -192,7 +192,7 @@ describe 'Comments', feature: true do end it 'should be removed when canceled' do - page.within(".diff-file form[id$='#{line_code}']") do + page.within(".diff-file form[id$='#{line_code}-true']") do find('.js-close-discussion-note-form').trigger('click') end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 264888cb37..5d916f0e6a 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -34,20 +34,6 @@ describe Note, models: true do end end - describe "Commit diff line notes" do - let!(:note) { create(:note_on_commit_diff, note: "+1 from me") } - let!(:commit) { note.noteable } - - it "should save a valid note" do - expect(note.commit_id).to eq(commit.id) - expect(note.noteable.id).to eq(commit.id) - end - - it "should be recognized by #legacy_diff_note?" do - expect(note).to be_legacy_diff_note - end - end - describe 'authorization' do before do @p1 = create(:project) @@ -144,66 +130,6 @@ describe Note, models: true do end end - describe '#active?' do - it 'is always true when the note has no associated diff' do - note = build(:note) - - expect(note).to receive(:diff).and_return(nil) - - expect(note).to be_active - end - - it 'is never true when the note has no noteable associated' do - note = build(:note) - - expect(note).to receive(:diff).and_return(double) - expect(note).to receive(:noteable).and_return(nil) - - expect(note).not_to be_active - end - - it 'returns the memoized value if defined' do - note = build(:note) - - expect(note).to receive(:diff).and_return(double) - expect(note).to receive(:noteable).and_return(double) - - note.instance_variable_set(:@active, 'foo') - expect(note).not_to receive(:find_noteable_diff) - - expect(note.active?).to eq 'foo' - end - - context 'for a merge request noteable' do - it 'is false when noteable has no matching diff' do - merge = build_stubbed(:merge_request, :simple) - note = build(:note, noteable: merge) - - allow(note).to receive(:diff).and_return(double) - expect(note).to receive(:find_noteable_diff).and_return(nil) - - expect(note).not_to be_active - end - - it 'is true when noteable has a matching diff' do - merge = create(:merge_request, :simple) - - # Generate a real line_code value so we know it will match. We use a - # random line from a random diff just for funsies. - diff = merge.diffs.to_a.sample - line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample - code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) - - # We're persisting in order to trigger the set_diff callback - note = create(:note, noteable: merge, line_code: code) - - # Make sure we don't get a false positive from a guard clause - expect(note).to receive(:find_noteable_diff).and_call_original - expect(note).to be_active - end - end - end - describe "editable?" do it "returns true" do note = build(:note) @@ -254,7 +180,7 @@ describe Note, models: true do end it "is not an award emoji when comment is on a diff" do - note = create(:note, note: ":blowfish:", noteable: merge_request, line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2") + note = create(:note_on_merge_request_diff, note: ":blowfish:", noteable: merge_request, line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2") note = note.reload expect(note.note).to eq(":blowfish:") From e0bb169ae390a00b3993bd0551859db05ad2bde1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 14:53:55 -0500 Subject: [PATCH 214/449] "the outdated diff" -> "an outdated diff" --- app/views/projects/notes/_discussion.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index 40e03050a7..aa26d8e87f 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -26,7 +26,7 @@ = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do the diff - else - the outdated diff + an outdated diff = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago") From e5ce1668061904a559ac6b9e5c830436fc743795 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 16:02:22 -0500 Subject: [PATCH 215/449] Satisfy Rubocop --- app/controllers/projects/commit_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 6e02f1240f..10b5932aff 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -22,7 +22,7 @@ class Projects::CommitController < Projects::ApplicationController @note = @project.build_commit_note(commit) @notes = commit.notes.non_diff_notes.fresh @noteable = @commit - @comments_target = { + @comments_target = { noteable_type: 'Commit', commit_id: @commit.id } From 876dc7bacb5c2835537d13dfedd2f7ed55463c6b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 May 2016 17:25:00 -0500 Subject: [PATCH 216/449] Rename diff_with_notes partial. --- app/views/projects/notes/_discussion.html.haml | 2 +- ...acy_diff_with_notes.html.haml => _diff_with_notes.html.haml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/views/projects/notes/discussions/{_legacy_diff_with_notes.html.haml => _diff_with_notes.html.haml} (100%) diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index aa26d8e87f..7869d6413d 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -41,6 +41,6 @@ .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } - if note.diff_note? - = render "projects/notes/discussions/legacy_diff_with_notes", discussion_notes: discussion_notes + = render "projects/notes/discussions/diff_with_notes", discussion_notes: discussion_notes - else = render "projects/notes/discussions/notes", discussion_notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml similarity index 100% rename from app/views/projects/notes/discussions/_legacy_diff_with_notes.html.haml rename to app/views/projects/notes/discussions/_diff_with_notes.html.haml From 4b4f2cf53b2ed9d5d1747469b7fb7779ea803058 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 26 Apr 2016 18:14:37 -0600 Subject: [PATCH 217/449] Replace animate.css with a smaller subset of animations. As discussed in #15667. --- CHANGELOG | 1 + app/assets/stylesheets/application.scss | 1 - app/assets/stylesheets/framework.scss | 1 + .../stylesheets/framework/animations.scss | 72 +++++++++++++++++++ vendor/assets/stylesheets/animate.css | 11 --- 5 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 app/assets/stylesheets/framework/animations.scss delete mode 100644 vendor/assets/stylesheets/animate.css diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..b166598a11 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -72,6 +72,7 @@ v 8.7.3 - Merge request widget displays TeamCity build state and code coverage correctly again. - Fix the line code when importing PR review comments from GitHub. !4010 - Wikis are now initialized on legacy projects when checking repositories + - Remove animate.css in favor of a smaller subset of animations. !3937 (Connor Shea) v 8.7.2 - The "New Branch" button is now loaded asynchronously diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 69b3b6586d..e2d590f4df 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,7 +10,6 @@ *= require dropzone/basic *= require cal-heatmap *= require cropper.css - *= require animate */ /* diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 560de9fc0b..3cbddc59f1 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -5,6 +5,7 @@ @import 'framework/tw_bootstrap'; @import "framework/layout"; +@import "framework/animations.scss"; @import "framework/avatar.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss new file mode 100644 index 0000000000..1fec61bdba --- /dev/null +++ b/app/assets/stylesheets/framework/animations.scss @@ -0,0 +1,72 @@ +// This file is based off animate.css 3.5.1, available here: +// https://github.com/daneden/animate.css/blob/3.5.1/animate.css +// +// animate.css - http://daneden.me/animate +// Version - 3.5.1 +// Licensed under the MIT license - http://opensource.org/licenses/MIT +// +// Copyright (c) 2016 Daniel Eden + +.animated { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.animated.infinite { + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; +} + +.animated.hinge { + -webkit-animation-duration: 2s; + animation-duration: 2s; +} + +.animated.flipOutX, +.animated.flipOutY, +.animated.bounceIn, +.animated.bounceOut { + -webkit-animation-duration: .75s; + animation-duration: .75s; +} + +@-webkit-keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.pulse { + -webkit-animation-name: pulse; + animation-name: pulse; +} diff --git a/vendor/assets/stylesheets/animate.css b/vendor/assets/stylesheets/animate.css deleted file mode 100644 index b6f6129539..0000000000 --- a/vendor/assets/stylesheets/animate.css +++ /dev/null @@ -1,11 +0,0 @@ -@charset "UTF-8"; - -/*! - * animate.css -http://daneden.me/animate - * Version - 3.5.1 - * Licensed under the MIT license - http://opensource.org/licenses/MIT - * - * Copyright (c) 2016 Daniel Eden - */ - -.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file From 7e1f14e21517d3907a0e096d44b30797612f69cd Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 26 Apr 2016 19:57:37 -0300 Subject: [PATCH 218/449] Preserve commits/diff/comments for PRs that were merged on GitHub --- app/models/merge_request.rb | 10 ++++++- app/models/merge_request_diff.rb | 26 ++++++++++++++----- .../github_import/pull_request_formatter.rb | 2 ++ .../pull_request_formatter_spec.rb | 6 +++++ spec/models/merge_request_spec.rb | 15 +++++++++-- 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5c5e6007aa..45ddcf6812 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -26,6 +26,10 @@ class MergeRequest < ActiveRecord::Base # when creating new merge request attr_accessor :can_be_created, :compare_commits, :compare + # Temporary fields to store target_sha, and base_sha to + # compare when importing pull requests from GitHub + attr_accessor :base_target_sha, :head_source_sha + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -490,10 +494,14 @@ class MergeRequest < ActiveRecord::Base end def target_sha - @target_sha ||= target_project.repository.commit(target_branch).try(:sha) + return @base_target_sha if defined?(@base_target_sha) + + target_project.repository.commit(target_branch).try(:sha) end def source_sha + return @head_source_sha if defined?(@head_source_sha) + last_commit.try(:sha) || source_tip.try(:sha) end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index eb42c07b9b..6ad8fc3f03 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -6,7 +6,7 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request - delegate :target_branch, :source_branch, to: :merge_request, prefix: nil + delegate :head_source_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil state_machine :state, initial: :empty do state :collected @@ -38,8 +38,8 @@ class MergeRequestDiff < ActiveRecord::Base @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha, + self.base, + self.head, ) compare.diffs(options) end @@ -144,7 +144,7 @@ class MergeRequestDiff < ActiveRecord::Base self.st_diffs = new_diffs - self.base_commit_sha = self.repository.merge_base(self.source_sha, self.target_branch) + self.base_commit_sha = self.repository.merge_base(self.head, self.base) self.save end @@ -160,10 +160,24 @@ class MergeRequestDiff < ActiveRecord::Base end def source_sha + return head_source_sha if head_source_sha.present? + source_commit = merge_request.source_project.commit(source_branch) source_commit.try(:sha) end + def target_sha + merge_request.target_sha + end + + def base + self.target_sha || self.target_branch + end + + def head + self.source_sha + end + def compare @compare ||= begin @@ -172,8 +186,8 @@ class MergeRequestDiff < ActiveRecord::Base Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha + self.base, + self.head ) end end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index f242596bdf..3b541a04d5 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -8,8 +8,10 @@ module Gitlab description: description, source_project: source_project, source_branch: source_branch, + head_source_sha: source_sha, target_project: target_project, target_branch: target_branch, + base_target_sha: target_sha, state: state, milestone: milestone, author_id: author_id, diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index b1f3d17373..1d15d3d937 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -41,8 +41,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'opened', milestone: nil, author_id: project.creator_id, @@ -66,8 +68,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'closed', milestone: nil, author_id: project.creator_id, @@ -91,8 +95,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'merged', milestone: nil, author_id: project.creator_id, diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c8578749b2..9eef08c6d0 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -64,7 +64,13 @@ describe MergeRequest, models: true do describe '#target_sha' do context 'when the target branch does not exist anymore' do - subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + let(:project) { create(:project) } + + subject { create(:merge_request, source_project: project, target_project: project) } + + before do + project.repository.raw_repository.delete_branch(subject.target_branch) + end it 'returns nil' do expect(subject.target_sha).to be_nil @@ -289,7 +295,12 @@ describe MergeRequest, models: true do let(:fork_project) { create(:project, forked_from_project: project) } context 'when the target branch does not exist anymore' do - subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + subject { create(:merge_request, source_project: project, target_project: project) } + + before do + project.repository.raw_repository.delete_branch(subject.target_branch) + subject.reload + end it 'does not crash' do expect{ subject.diverged_commits_count }.not_to raise_error From e001bd5e3d813fa72f724fc0e661b36099f678ea Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 9 May 2016 16:17:05 -0500 Subject: [PATCH 219/449] Import PRs where branch names were reused across PRs --- .../github_import/pull_request_formatter.rb | 28 +++++++++++++--- .../pull_request_formatter_spec.rb | 32 +++++++++++++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 3b541a04d5..9f8182f643 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -30,11 +30,19 @@ module Gitlab end def source_branch_exists? - source_project.repository.branch_names.include?(source_branch) + source_project.repository.branch_exists?(source_ref) end def source_branch - raw_data.head.ref + @source_branch ||= if source_branch_exists? + source_ref + else + "#{source_ref}-#{short_id(source_sha)}" + end + end + + def short_id(sha, length = 7) + sha.to_s[0..length] end def source_sha @@ -42,11 +50,15 @@ module Gitlab end def target_branch_exists? - target_project.repository.branch_names.include?(target_branch) + target_project.repository.branch_exists?(target_ref) end def target_branch - raw_data.base.ref + @target_branch ||= if target_branch_exists? + target_ref + else + "#{target_ref}-#{short_id(target_sha)}" + end end def target_sha @@ -99,6 +111,10 @@ module Gitlab raw_data.head.repo end + def source_ref + raw_data.head.ref + end + def target_project project end @@ -107,6 +123,10 @@ module Gitlab raw_data.base.repo end + def target_ref + raw_data.base.ref + end + def state @state ||= case true when raw_data.state == 'closed' && raw_data.merged_at.present? diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 1d15d3d937..adedc57719 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -164,10 +164,20 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end describe '#source_branch' do - let(:raw_data) { double(base_data) } + context 'when source branch exists' do + let(:raw_data) { double(base_data) } - it 'returns head ref' do - expect(pull_request.source_branch).to eq 'feature' + it 'returns head ref' do + expect(pull_request.source_branch).to eq 'feature' + end + end + + context 'when source branch does not exist' do + let(:raw_data) { double(base_data.merge(head: double(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) } + + it 'returns head ref' do + expect(pull_request.source_branch).to eq 'removed-branch-2e5d3239' + end end end @@ -198,10 +208,20 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end describe '#target_branch' do - let(:raw_data) { double(base_data) } + context 'when target branch exists' do + let(:raw_data) { double(base_data) } - it 'returns base ref' do - expect(pull_request.target_branch).to eq 'master' + it 'returns base ref' do + expect(pull_request.target_branch).to eq 'master' + end + end + + context 'when target branch does not exist' do + let(:raw_data) { double(base_data.merge(base: double(ref: 'removed-branch', sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7'))) } + + it 'returns head ref' do + expect(pull_request.target_branch).to eq 'removed-branch-8ffb3c15' + end end end From 795a7ca8f171a1634134e8b8ce5cf80a65381903 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 9 May 2016 17:45:37 -0500 Subject: [PATCH 220/449] Extract GitHub branch formatter --- app/models/repository.rb | 4 + lib/gitlab/github_import/branch_formatter.rb | 25 ++++++ lib/gitlab/github_import/importer.rb | 30 +++---- .../github_import/pull_request_formatter.rb | 74 +++------------- .../github_import/branch_formatter_spec.rb | 57 ++++++++++++ .../pull_request_formatter_spec.rb | 88 ------------------- 6 files changed, 113 insertions(+), 165 deletions(-) create mode 100644 lib/gitlab/github_import/branch_formatter.rb create mode 100644 spec/lib/gitlab/github_import/branch_formatter_spec.rb diff --git a/app/models/repository.rb b/app/models/repository.rb index 0eff74320f..3716ea6ad6 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -195,6 +195,10 @@ class Repository cache.fetch(:branch_names) { branches.map(&:name) } end + def branch_exists?(branch_name) + branch_names.include?(branch_name) + end + def tag_names cache.fetch(:tag_names) { raw_repository.tag_names } end diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb new file mode 100644 index 0000000000..f968ec4b10 --- /dev/null +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -0,0 +1,25 @@ +module Gitlab + module GithubImport + class BranchFormatter < BaseFormatter + delegate :repo, :sha, :ref, to: :raw_data + + def exists? + project.repository.branch_exists?(ref) + end + + def name + @name ||= exists? ? ref : "#{ref}-#{short_id}" + end + + def valid? + repo.present? + end + + private + + def short_id + sha.to_s[0..7] + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 9aea947a81..1f160837c5 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -6,8 +6,8 @@ module Gitlab attr_reader :client, :project, :repo, :repo_url def initialize(project) - @project = project - @repo = project.import_source + @project = project + @repo = project.import_source @repo_url = project.import_url if credentials @@ -30,7 +30,7 @@ module Gitlab end def import_labels - client.labels(project.import_source).each do |raw_data| + client.labels(repo).each do |raw_data| Label.create!(LabelFormatter.new(project, raw_data).attributes) end @@ -40,7 +40,7 @@ module Gitlab end def import_milestones - client.list_milestones(project.import_source, state: :all).each do |raw_data| + client.list_milestones(repo, state: :all).each do |raw_data| Milestone.create!(MilestoneFormatter.new(project, raw_data).attributes) end @@ -50,9 +50,7 @@ module Gitlab end def import_issues - client.list_issues(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| + client.list_issues(repo, state: :all, sort: :created, direction: :asc).each do |raw_data| gh_issue = IssueFormatter.new(project, raw_data) if gh_issue.valid? @@ -75,8 +73,8 @@ module Gitlab .map { |raw| PullRequestFormatter.new(project, raw) } .select(&:valid?) - source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch, pr.source_sha] } - target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch, pr.target_sha] } + source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] } + target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] } branches_removed = source_branches_removed | target_branches_removed create_refs(branches_removed) @@ -101,19 +99,19 @@ module Gitlab end def create_refs(branches) - branches.each do |branch| - client.create_ref(repo, "refs/heads/#{branch.first}", branch.last) + branches.each do |name, sha| + client.create_ref(repo, "refs/heads/#{name}", sha) end end def delete_refs(branches) - branches.each do |branch| - client.delete_ref(repo, "heads/#{branch.first}") + branches.each do |name, _| + client.delete_ref(repo, "heads/#{name}") end end def apply_labels(number, issuable) - issue = client.issue(project.import_source, number) + issue = client.issue(repo, number) if issue.labels.count > 0 label_ids = issue.labels.map do |raw| @@ -125,12 +123,12 @@ module Gitlab end def import_comments(issue_number, noteable) - comments = client.issue_comments(project.import_source, issue_number) + comments = client.issue_comments(repo, issue_number) create_comments(comments, noteable) end def import_comments_on_diff(pull_request_number, merge_request) - comments = client.pull_request_comments(project.import_source, pull_request_number) + comments = client.pull_request_comments(repo, pull_request_number) create_comments(comments, merge_request) end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 9f8182f643..361d15d991 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,17 +1,20 @@ module Gitlab module GithubImport class PullRequestFormatter < BaseFormatter + delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true + delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true + def attributes { iid: number, title: raw_data.title, description: description, - source_project: source_project, - source_branch: source_branch, - head_source_sha: source_sha, - target_project: target_project, - target_branch: target_branch, - base_target_sha: target_sha, + source_project: source_branch_project, + source_branch: source_branch_name, + head_source_sha: source_branch_sha, + target_project: target_branch_project, + target_branch: target_branch_name, + base_target_sha: target_branch_sha, state: state, milestone: milestone, author_id: author_id, @@ -29,40 +32,12 @@ module Gitlab !cross_project? end - def source_branch_exists? - source_project.repository.branch_exists?(source_ref) - end - def source_branch - @source_branch ||= if source_branch_exists? - source_ref - else - "#{source_ref}-#{short_id(source_sha)}" - end - end - - def short_id(sha, length = 7) - sha.to_s[0..length] - end - - def source_sha - raw_data.head.sha - end - - def target_branch_exists? - target_project.repository.branch_exists?(target_ref) + @source_branch ||= BranchFormatter.new(project, raw_data.head) end def target_branch - @target_branch ||= if target_branch_exists? - target_ref - else - "#{target_ref}-#{short_id(target_sha)}" - end - end - - def target_sha - raw_data.base.sha + @target_branch ||= BranchFormatter.new(project, raw_data.base) end private @@ -90,7 +65,8 @@ module Gitlab end def cross_project? - source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + source_branch_repo.present? && target_branch_repo.present? && + source_branch_repo.id != target_branch_repo.id end def description @@ -103,30 +79,6 @@ module Gitlab end end - def source_project - project - end - - def source_repo - raw_data.head.repo - end - - def source_ref - raw_data.head.ref - end - - def target_project - project - end - - def target_repo - raw_data.base.repo - end - - def target_ref - raw_data.base.ref - end - def state @state ||= case true when raw_data.state == 'closed' && raw_data.merged_at.present? diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb new file mode 100644 index 0000000000..58bfdd6c72 --- /dev/null +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::BranchFormatter, lib: true do + let(:project) { create(:project) } + let(:repo) { double } + let(:raw) do + { + ref: 'feature', + repo: repo, + sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + } + end + + describe '#exists?' do + it 'returns true when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.exists?).to eq true + end + + it 'returns false when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.exists?).to eq false + end + end + + describe '#name' do + it 'returns raw ref when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.name).to eq 'feature' + end + + it 'returns formatted ref when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.name).to eq 'removed-branch-2e5d3239' + end + end + + describe '#repo' do + it 'returns raw repo' do + branch = described_class.new(project, double(raw)) + + expect(branch.repo).to eq repo + end + end + + describe '#sha' do + it 'returns raw sha' do + branch = described_class.new(project, double(raw)) + + expect(branch.sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index adedc57719..5fed98e392 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -163,94 +163,6 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end - describe '#source_branch' do - context 'when source branch exists' do - let(:raw_data) { double(base_data) } - - it 'returns head ref' do - expect(pull_request.source_branch).to eq 'feature' - end - end - - context 'when source branch does not exist' do - let(:raw_data) { double(base_data.merge(head: double(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) } - - it 'returns head ref' do - expect(pull_request.source_branch).to eq 'removed-branch-2e5d3239' - end - end - end - - describe '#source_sha' do - let(:raw_data) { double(base_data) } - - it 'returns head sha' do - expect(pull_request.source_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' - end - end - - describe '#source_branch_exists?' do - context 'when source branch exists' do - let(:raw_data) { double(base_data) } - - it 'returns true' do - expect(pull_request.source_branch_exists?).to eq true - end - end - - context 'when source branch does not exist' do - let(:raw_data) { double(base_data.merge(head: double(ref: 'removed-branch').as_null_object)) } - - it 'returns false' do - expect(pull_request.source_branch_exists?).to eq false - end - end - end - - describe '#target_branch' do - context 'when target branch exists' do - let(:raw_data) { double(base_data) } - - it 'returns base ref' do - expect(pull_request.target_branch).to eq 'master' - end - end - - context 'when target branch does not exist' do - let(:raw_data) { double(base_data.merge(base: double(ref: 'removed-branch', sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7'))) } - - it 'returns head ref' do - expect(pull_request.target_branch).to eq 'removed-branch-8ffb3c15' - end - end - end - - describe '#target_sha' do - let(:raw_data) { double(base_data) } - - it 'returns base sha' do - expect(pull_request.target_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7' - end - end - - describe '#target_branch_exists?' do - context 'when target branch exists' do - let(:raw_data) { double(base_data) } - - it 'returns true' do - expect(pull_request.target_branch_exists?).to eq true - end - end - - context 'when target branch does not exist' do - let(:raw_data) { double(base_data.merge(base: double(ref: 'removed-branch').as_null_object)) } - - it 'returns false' do - expect(pull_request.target_branch_exists?).to eq false - end - end - end - describe '#valid?' do context 'when source, and target repos are not a fork' do let(:raw_data) { double(base_data) } From ebaa19c162bec7dce64db25124e448d832c17384 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 10 May 2016 16:23:59 -0500 Subject: [PATCH 221/449] Fix validation method for Gitlab::GithubImport::PullRequestFormatter --- lib/gitlab/github_import/branch_formatter.rb | 4 ++++ lib/gitlab/github_import/pull_request_formatter.rb | 5 ++--- .../gitlab/github_import/branch_formatter_spec.rb | 14 ++++++++++++++ .../github_import/pull_request_formatter_spec.rb | 4 ++-- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index f968ec4b10..a15fc84b41 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -15,6 +15,10 @@ module Gitlab repo.present? end + def valid? + repo.present? + end + private def short_id diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 361d15d991..574737b31c 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -29,7 +29,7 @@ module Gitlab end def valid? - !cross_project? + source_branch.valid? && target_branch.valid? && !cross_project? end def source_branch @@ -65,8 +65,7 @@ module Gitlab end def cross_project? - source_branch_repo.present? && target_branch_repo.present? && - source_branch_repo.id != target_branch_repo.id + source_branch_repo.id != target_branch_repo.id end def description diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index 58bfdd6c72..3cb634ba01 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -54,4 +54,18 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do expect(branch.sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' end end + + describe '#valid?' do + it 'returns true when repository exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.valid?).to eq true + end + + it 'returns false when repository does not exist' do + branch = described_class.new(project, double(raw.merge(repo: nil))) + + expect(branch.valid?).to eq false + end + end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 5fed98e392..120f59e6e7 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -173,7 +173,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when source repo is a fork' do - let(:source_repo) { double(id: 2, fork: true) } + let(:source_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do @@ -182,7 +182,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when target repo is a fork' do - let(:target_repo) { double(id: 2, fork: true) } + let(:target_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do From 290aa75e6f8dfbd92ed11f6c271fb4ad83831e86 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 10 May 2016 17:23:03 -0500 Subject: [PATCH 222/449] Remove branches that does exist on GitHub after PRs were imported --- lib/gitlab/github_import/importer.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 1f160837c5..408d9b7963 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -79,8 +79,6 @@ module Gitlab create_refs(branches_removed) - project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') - pull_requests.each do |pull_request| merge_request = MergeRequest.new(pull_request.attributes) @@ -102,11 +100,14 @@ module Gitlab branches.each do |name, sha| client.create_ref(repo, "refs/heads/#{name}", sha) end + + project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') end def delete_refs(branches) branches.each do |name, _| client.delete_ref(repo, "heads/#{name}") + project.repository.rm_branch(project.creator, name) end end From bd1a6208d9b76922ae07bbe59f9f03a3573fe59d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 10 May 2016 17:34:57 -0500 Subject: [PATCH 223/449] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..accaaaba4e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,6 +51,7 @@ v 8.8.0 (unreleased) - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) - Hide left sidebar on phone screens to give more space for content - Redesign navigation for profile and group pages + - Import pull requests from GitHub where the source or target branches were removed v 8.7.5 - Fix relative links in wiki pages. !4050 From cf6d4e9db507d31feede527cb2dcc196d763c359 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 14 May 2016 00:48:49 -0500 Subject: [PATCH 224/449] Change colors for charcoal and graphite sidebar themes Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/gitlab-theme.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 51a17d1469..f47eb1f233 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -88,8 +88,8 @@ } $theme-blue: #2980b9; -$theme-charcoal: #333c47; -$theme-graphite: #888; +$theme-charcoal: #3d454d; +$theme-graphite: #666; $theme-gray: #373737; $theme-green: #019875; $theme-violet: #548; @@ -100,11 +100,11 @@ body { } &.ui_charcoal { - @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d); + @include gitlab-theme(#d6d7d9, #485157, $theme-charcoal, #353b41); } &.ui_graphite { - @include gitlab-theme(#ccc, $theme-graphite, #777, #666); + @include gitlab-theme(#ccc, #777, $theme-graphite, #555); } &.ui_gray { From 774a5107822d3d451b88ed3a7257aeac8d91c35a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 09:39:33 -0500 Subject: [PATCH 225/449] Fix specs --- .../jwt/container_registry_authentication_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/jwt/container_registry_authentication_service_spec.rb index 1873ea2639..7c87985252 100644 --- a/spec/services/jwt/container_registry_authentication_service_spec.rb +++ b/spec/services/jwt/container_registry_authentication_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe JWT::ContainerRegistryAuthenticationService, services: true do +describe Gitlab::JWT::ContainerRegistryAuthenticationService, services: true do let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } From a6b8d36ae933c3f21e7ba2c17864c6d8bf626d25 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 09:46:38 -0500 Subject: [PATCH 226/449] Fix specs --- app/controllers/projects/pipelines_controller.rb | 3 --- app/models/commit_status.rb | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index bdb5722c55..f3f5338003 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -31,9 +31,6 @@ class Projects::PipelinesController < Projects::ApplicationController end def show - respond_to do |format| - format.html - end end def retry diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 521456bede..213cd20e8d 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -55,7 +55,7 @@ class CommitStatus < ActiveRecord::Base def self.stages # We group by stage name, but order stages by theirs' index - unscoped.where(id: all).group('stage').order('max(stage_idx)', 'stage').pluck('stage') + unscoped.where(id: all.ids).group('stage').order('max(stage_idx)', 'stage').pluck('stage') end def self.status_for_stage(stage) From 63cdf1aeb04b9694c0b6d44b1141868fcc5a0904 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 11:11:48 -0500 Subject: [PATCH 227/449] Use Auth::ContainerRegistryAuthenticationService --- app/controllers/jwt_controller.rb | 2 +- ...ntainer_registry_authentication_service.rb | 69 ++++++++++++++++++ ...ntainer_registry_authentication_service.rb | 71 ------------------- ...er_registry_authentication_service_spec.rb | 2 +- 4 files changed, 71 insertions(+), 73 deletions(-) create mode 100644 app/services/auth/container_registry_authentication_service.rb delete mode 100644 app/services/jwt/container_registry_authentication_service.rb diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index e067f59808..e5affb1adc 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -4,7 +4,7 @@ class JwtController < ApplicationController before_action :authenticate_project_or_user SERVICES = { - 'container_registry' => ::Gitlab::JWT::ContainerRegistryAuthenticationService, + 'container_registry' => Auth::ContainerRegistryAuthenticationService, } def auth diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb new file mode 100644 index 0000000000..0323a42b69 --- /dev/null +++ b/app/services/auth/container_registry_authentication_service.rb @@ -0,0 +1,69 @@ +module Auth + class ContainerRegistryAuthenticationService < BaseService + def execute + if params[:offline_token] + return error('forbidden', 403) unless current_user + end + + return error('forbidden', 401) if scopes.blank? + + { token: authorized_token(scopes).encoded } + end + + private + + def authorized_token(access) + token = ::JWT::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token[:access] = access + token + end + + def scopes + return unless params[:scope] + + @scopes ||= begin + scope = process_scope(params[:scope]) + [scope].compact + end + end + + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + + case type + when 'repository' + process_repository_access(type, name, actions) + end + end + + def process_repository_access(type, name, actions) + requested_project = Project.find_with_namespace(name) + return unless requested_project + + actions = actions.select do |action| + can_access?(requested_project, action) + end + + { type: type, name: name, actions: actions } if actions.present? + end + + def can_access?(requested_project, requested_action) + case requested_action + when 'pull' + requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) + when 'push' + requested_project == project || can?(current_user, :create_container_registry, requested_project) + else + false + end + end + + def registry + Gitlab.config.registry + end + end +end diff --git a/app/services/jwt/container_registry_authentication_service.rb b/app/services/jwt/container_registry_authentication_service.rb deleted file mode 100644 index dd0f295478..0000000000 --- a/app/services/jwt/container_registry_authentication_service.rb +++ /dev/null @@ -1,71 +0,0 @@ -module Gitlab - module JWT - class ContainerRegistryAuthenticationService < BaseService - def execute - if params[:offline_token] - return error('forbidden', 403) unless current_user - end - - return error('forbidden', 401) if scopes.blank? - - { token: authorized_token(scopes).encoded } - end - - private - - def authorized_token(access) - token = ::JWT::RSAToken.new(registry.key) - token.issuer = registry.issuer - token.audience = params[:service] - token.subject = current_user.try(:username) - token[:access] = access - token - end - - def scopes - return unless params[:scope] - - @scopes ||= begin - scope = process_scope(params[:scope]) - [scope].compact - end - end - - def process_scope(scope) - type, name, actions = scope.split(':', 3) - actions = actions.split(',') - - case type - when 'repository' - process_repository_access(type, name, actions) - end - end - - def process_repository_access(type, name, actions) - requested_project = Project.find_with_namespace(name) - return unless requested_project - - actions = actions.select do |action| - can_access?(requested_project, action) - end - - { type: type, name: name, actions: actions } if actions.present? - end - - def can_access?(requested_project, requested_action) - case requested_action - when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) - when 'push' - requested_project == project || can?(current_user, :create_container_registry, requested_project) - else - false - end - end - - def registry - Gitlab.config.registry - end - end - end -end diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/jwt/container_registry_authentication_service_spec.rb index 7c87985252..1873ea2639 100644 --- a/spec/services/jwt/container_registry_authentication_service_spec.rb +++ b/spec/services/jwt/container_registry_authentication_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::JWT::ContainerRegistryAuthenticationService, services: true do +describe JWT::ContainerRegistryAuthenticationService, services: true do let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } From 0a9979d9ef3b5205e36ecfb66ff6322fa824492f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 11:16:11 -0500 Subject: [PATCH 228/449] Rename specs --- .../container_registry_authentication_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spec/services/{jwt => auth}/container_registry_authentication_service_spec.rb (98%) diff --git a/spec/services/jwt/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb similarity index 98% rename from spec/services/jwt/container_registry_authentication_service_spec.rb rename to spec/services/auth/container_registry_authentication_service_spec.rb index 1873ea2639..8dc47a24ee 100644 --- a/spec/services/jwt/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe JWT::ContainerRegistryAuthenticationService, services: true do +describe Auth::ContainerRegistryAuthenticationService, services: true do let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } From 4f1e0014a73e1e310a82a3d8ce52367a1f9d6a9e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:03:41 -0500 Subject: [PATCH 229/449] Simplify registry configuration --- config/gitlab.yml.example | 4 +--- config/initializers/1_settings.rb | 38 ++++--------------------------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 37639e52e6..2e383bc90f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -179,9 +179,7 @@ production: &base registry: # enabled: true # host: localhost - # port: 5000 - # https: false - # internal_host: localhost + # api_url: http://localhost:5000/ # key: config/registry.key # issuer: omnibus-certificate diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index e46be3f420..d1fcb053be 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -27,30 +27,6 @@ class Settings < Settingslogic ].join('') end - def build_registry_api_url - if registry.port.to_i == (registry.https ? 443 : 80) - custom_port = nil - else - custom_port = ":#{registry.port}" - end - [ registry.protocol, - "://", - registry.internal_host, - custom_port - ].join('') - end - - def build_registry_host_with_port - if registry.port.to_i == (registry.https ? 443 : 80) - custom_port = nil - else - custom_port = ":#{registry.port}" - end - [ registry.host, - custom_port - ].join('') - end - def build_gitlab_shell_ssh_path_prefix user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" @@ -271,15 +247,11 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes # Registry # Settings['registry'] ||= Settingslogic.new({}) -Settings.registry['enabled'] = false if Settings.registry['enabled'].nil? -Settings.registry['host'] ||= "example.com" -Settings.registry['internal_host']||= "localhost" -Settings.registry['key'] ||= nil -Settings.registry['https'] = false if Settings.registry['https'].nil? -Settings.registry['port'] ||= Settings.registry.https ? 443 : 80 -Settings.registry['protocol'] ||= Settings.registry.https ? "https" : "http" -Settings.registry['api_url'] ||= Settings.send(:build_registry_api_url) -Settings.registry['host_port'] ||= Settings.send(:build_registry_host_with_port) +Settings.registry['enabled'] ||= false +Settings.registry['host'] ||= "example.com" +Settings.registry['api_url'] ||= "http://localhost:5000/" +Settings.registry['key'] ||= nil +Settings.registry['issuer'] ||= nil # # Git LFS From 393459b2b24fe788764ee787552da055846b9a63 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:04:04 -0500 Subject: [PATCH 230/449] Improve code design after review --- app/controllers/jwt_controller.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index e5affb1adc..0a993bf280 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -12,9 +12,8 @@ class JwtController < ApplicationController head :not_found unless service result = service.new(@project, @user, auth_params).execute - return head result[:http_status] if result[:http_status] - render json: result + render json: result, status: result[:http_status] end private @@ -27,10 +26,8 @@ class JwtController < ApplicationController @user = authenticate_user(login, password) return if @user - end - if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) - head :forbidden + render_403 end end From ee725db8e0f5d95c14031bd939a414e068497bb4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:16:44 -0500 Subject: [PATCH 231/449] Add TODO --- app/controllers/jwt_controller.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index e0e0a98a6c..0edf084e9e 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -42,6 +42,9 @@ class JwtController < ApplicationController end def authenticate_user(login, password) + # TODO: this is a copy and paste from grack_auth, + # it should be refactored in the future + user = Gitlab::Auth.new.find(login, password) # If the user authenticated successfully, we reset the auth failure count From 46cc04ce7a374127dd617c8fd2671efed2819cda Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:16:44 -0500 Subject: [PATCH 232/449] Add TODO --- app/controllers/jwt_controller.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 0a993bf280..bd9d7e4425 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -42,6 +42,9 @@ class JwtController < ApplicationController end def authenticate_user(login, password) + # TODO: this is a copy and paste from grack_auth, + # it should be refactored in the future + user = Gitlab::Auth.new.find(login, password) # If the user authenticated successfully, we reset the auth failure count From 284dc3285a4ec2a626e90b16ca3d372eed82d349 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:22:10 -0500 Subject: [PATCH 233/449] Fix abilities --- app/models/ability.rb | 2 +- app/models/project.rb | 10 +++++----- app/views/projects/container_registry/index.html.haml | 2 +- spec/requests/ci/api/runners_spec.rb | 1 - 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 09dea54689..7432124046 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -292,7 +292,7 @@ class Ability rules += named_abilities('build') end - unless project.container_registry_enabled && Gitlab.config.registry.enabled + unless project.container_registry_enabled rules += named_abilities('container_registry') end diff --git a/app/models/project.rb b/app/models/project.rb index 0206ccc8d3..ed7719ed31 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -332,20 +332,20 @@ class Project < ActiveRecord::Base @container_registry_repository ||= begin token = Auth::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace) url = Gitlab.config.registry.api_url - host_port = Gitlab.config.registry.host_port - registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) + host = Gitlab.config.registry.host + registry = ContainerRegistry::Registry.new(url, token: token, path: host) registry[path_with_namespace] end end def container_registry_repository_url - if container_registry_enabled? && Gitlab.config.registry.enabled - "#{Gitlab.config.registry.host_port}/#{path_with_namespace}" + if Gitlab.config.registry.enabled + "#{Gitlab.config.registry.host}/#{path_with_namespace}" end end def has_container_registry_tags? - if container_registry_enabled? && Gitlab.config.registry.enabled + if Gitlab.config.registry.enabled container_registry_repository.tags.any? end end diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index ea7c4faaae..990253719b 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -16,7 +16,7 @@ To start using container images hosted on GitLab you first need to login: %pre %code - docker login #{Gitlab.config.registry.host_port} + docker login #{Gitlab.config.registry.host} %br Then you are free to create and upload a container image with build and push commands: %pre diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index db8189ffb7..43f9fe89c8 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -7,7 +7,6 @@ describe Ci::API::API do let(:registration_token) { 'abcdefg123456' } before do - stub_gitlab_calls stub_application_setting(runners_registration_token: registration_token) end From d40bd7419f9ca0f7caedef05209b86e7431dd882 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:22:45 -0500 Subject: [PATCH 234/449] Fix authentication service --- app/services/auth/container_registry_authentication_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 0323a42b69..100f7cbae2 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -54,7 +54,7 @@ module Auth def can_access?(requested_project, requested_action) case requested_action when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) + requested_project == project || can?(current_user, :read_container_registry, requested_project) when 'push' requested_project == project || can?(current_user, :create_container_registry, requested_project) else From 715a8cfa2f4639bf36b604f6e3eb2814187367c0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:22:45 -0500 Subject: [PATCH 235/449] Fix authentication service --- app/models/ability.rb | 1 + .../auth/container_registry_authentication_service.rb | 8 +++++++- .../container_registry_authentication_service_spec.rb | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 59d5195f5b..7432124046 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -61,6 +61,7 @@ class Ability :read_merge_request, :read_note, :read_commit_status, + :read_container_registry, :download_code ] diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 0323a42b69..a63e7046fc 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -1,6 +1,10 @@ module Auth class ContainerRegistryAuthenticationService < BaseService + AUDIENCE = 'container_registry' + def execute + return error('not found', 404) unless registry.enabled + if params[:offline_token] return error('forbidden', 403) unless current_user end @@ -52,9 +56,11 @@ module Auth end def can_access?(requested_project, requested_action) + return false unless requested_project.container_registry_enabled? + case requested_action when 'pull' - requested_project.public? || requested_project == project || can?(current_user, :read_container_registry, requested_project) + requested_project == project || can?(current_user, :read_container_registry, requested_project) when 'push' requested_project == project || can?(current_user, :create_container_registry, requested_project) else diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 8dc47a24ee..6e86a3dcf5 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -7,6 +7,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } let(:registry_settings) do { + enabled: true, issuer: 'rspec', key: nil } From 8c93b6051fdc7bb59af2e6bf2f5b3a1817a73ceb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:44:35 -0500 Subject: [PATCH 236/449] Revert `stages` change --- app/models/commit_status.rb | 11 ++++++++--- app/views/projects/ci/commits/_commit.html.haml | 3 ++- spec/models/commit_status_spec.rb | 12 ++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 213cd20e8d..6fef1c038e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -55,11 +55,16 @@ class CommitStatus < ActiveRecord::Base def self.stages # We group by stage name, but order stages by theirs' index - unscoped.where(id: all.ids).group('stage').order('max(stage_idx)', 'stage').pluck('stage') + unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') end - def self.status_for_stage(stage) - where(stage: stage).status + def self.stages_status + # We execute subquery for each stage to calculate a stage status + statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) + statuses.inject({}) do |h, k| + h[k.first] = k.last + h + end end def ignored? diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index 7f9a341783..90ac41666d 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -31,9 +31,10 @@ Cant find HEAD commit for this branch + - stages_status = commit.statuses.stages_status - stages.each do |stage| %td - - if status = commit.statuses.status_for_stage(stage) + - if status = stages_status[stage] - tooltip = "#{stage.titleize}: #{status}" %span.has-tooltip(title="#{tooltip}"){class: "ci-status-icon-#{status}"} = ci_icon_for_status(status) diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index ea60894c41..434e58cfd0 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -185,5 +185,17 @@ describe CommitStatus, models: true do is_expected.to eq(%w(build test deploy)) end end + + context 'stages with statuses' do + subject { CommitStatus.where(commit: commit).stages_status } + + it 'return list of stages with statuses' do + is_expected.to eq({ + 'build' => 'failed', + 'test' => 'success', + 'deploy' => 'running' + }) + end + end end end From 865e8853369b542569693ec23a9b533481228829 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:47:47 -0500 Subject: [PATCH 237/449] Fix specs for MySQL --- app/models/ci/commit.rb | 5 ++--- app/views/projects/pipelines/index.html.haml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 91c5b4c1ca..6675a3f5d5 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -8,8 +8,6 @@ module Ci has_many :builds, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' - delegate :stages, to: :statuses - validates_presence_of :sha validates_presence_of :status validate :valid_commit_sha @@ -22,7 +20,8 @@ module Ci end def self.stages - CommitStatus.where(commit: all).stages + # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries + CommitStatus.where(commit: pluck(:id)).stages end def project_id diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 574941ed5f..af55ef42a6 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -55,7 +55,7 @@ %tbody %th ID %th Commit - - @pipelines.stages.each do |stage| + - stages.each do |stage| %th %span.has-tooltip(title="#{stage.titleize}") = truncate(stage.titleize.pluralize, length: 8) From 7d907acc3805a692fd9f6f29e529c5b5fe15ec11 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 14:51:57 -0500 Subject: [PATCH 238/449] Fix spinach tests --- features/steps/dashboard/dashboard.rb | 2 +- features/steps/project/merge_requests.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index b5980b3510..80ed4c6d64 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I should see "Shop" project CI status' do - expect(page).to have_link "Build skipped" + expect(page).to have_link "Commit: skipped" end step 'I should see last push widget' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 3b1a00f628..b79d19f1c5 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -525,7 +525,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see merge request "Bug NS-05" with CI status' do page.within ".mr-list" do - expect(page).to have_link "Build pending" + expect(page).to have_link "Pipeline: pending" end end From 2566c89a3232d6d4d5aa5d77821819e86424548d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 15:55:00 -0500 Subject: [PATCH 239/449] Remove testing delegate --- app/controllers/projects/pipelines_controller.rb | 4 ++-- spec/models/ci/commit_spec.rb | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index f3f5338003..78b85ea9a7 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -22,10 +22,10 @@ class Projects::PipelinesController < Projects::ApplicationController pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute redirect_to namespace_project_pipeline_path(project.namespace, project, pipeline) rescue ArgumentError => e - @error = e.message + flash[:alert] = e.message render 'new' rescue - @error = 'Undefined error' + flash[:alert] = 'Undefined error' render 'new' end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index dc071ad1c9..1b5940ad5a 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -10,7 +10,6 @@ describe Ci::Commit, models: true do it { is_expected.to have_many(:builds) } it { is_expected.to validate_presence_of :sha } it { is_expected.to validate_presence_of :status } - it { is_expected.to delegate_method(:stages).to(:statuses) } it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_email } From df973df8c536fb9ffe0a9ed4e92f7d9144eb687a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 16:04:26 -0500 Subject: [PATCH 240/449] Simplify JWT::RSAToken::kid --- lib/jwt/rsa_token.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb index 4de89bf0d3..d7df9269e1 100644 --- a/lib/jwt/rsa_token.rb +++ b/lib/jwt/rsa_token.rb @@ -29,10 +29,14 @@ module JWT end def kid - fingerprint = Digest::SHA256.digest(public_key.to_der) - Base32.encode(fingerprint).split('').each_slice(4).each_with_object([]) do |slice, mem| - mem << slice.join - end.join(':') + # calculate sha256 from DER encoded ASN1 + kid = Digest::SHA256.digest(public_key.to_der) + + # we encode only 30 bytes with base32 + kid = Base32.encode(kid[0..29]) + + # insert colon every 4 characters + kid.scan(/.{4}/).join(':') end end end From f4f9184a01bc7442411bbcffd9b6a86784fa5f53 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 18:23:31 -0500 Subject: [PATCH 241/449] Rename JWT to JSONWebToken --- app/controllers/jwt_controller.rb | 2 +- app/models/ability.rb | 12 ++++----- ...ntainer_registry_authentication_service.rb | 27 ++++++++----------- lib/{jwt => json_web_token}/rsa_token.rb | 2 +- lib/{jwt => json_web_token}/token.rb | 2 +- .../{jwt => json_web_token}/rsa_token_spec.rb | 2 +- .../lib/{jwt => json_web_token}/token_spec.rb | 2 +- ...er_registry_authentication_service_spec.rb | 2 +- 8 files changed, 23 insertions(+), 28 deletions(-) rename lib/{jwt => json_web_token}/rsa_token.rb (97%) rename lib/{jwt => json_web_token}/token.rb (97%) rename spec/lib/{jwt => json_web_token}/rsa_token_spec.rb (95%) rename spec/lib/{jwt => json_web_token}/token_spec.rb (92%) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index bd9d7e4425..0edf084e9e 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -4,7 +4,7 @@ class JwtController < ApplicationController before_action :authenticate_project_or_user SERVICES = { - 'container_registry' => Auth::ContainerRegistryAuthenticationService, + Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService, } def auth diff --git a/app/models/ability.rb b/app/models/ability.rb index 7432124046..f70268d313 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -61,7 +61,7 @@ class Ability :read_merge_request, :read_note, :read_commit_status, - :read_container_registry, + :read_container_image, :download_code ] @@ -204,7 +204,7 @@ class Ability :admin_label, :read_commit_status, :read_build, - :read_container_registry, + :read_container_image, ] end @@ -219,8 +219,8 @@ class Ability :create_merge_request, :create_wiki, :push_code, - :create_container_registry, - :update_container_registry, + :create_container_image, + :update_container_image, ] end @@ -247,7 +247,7 @@ class Ability :admin_project, :admin_commit_status, :admin_build, - :admin_container_registry, + :admin_container_image, ] end @@ -293,7 +293,7 @@ class Ability end unless project.container_registry_enabled - rules += named_abilities('container_registry') + rules += named_abilities('container_image') end rules diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index a63e7046fc..bbbc84475c 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -9,39 +9,34 @@ module Auth return error('forbidden', 403) unless current_user end - return error('forbidden', 401) if scopes.blank? + return error('forbidden', 401) unless scope - { token: authorized_token(scopes).encoded } + { token: authorized_token(scope).encoded } end private - def authorized_token(access) - token = ::JWT::RSAToken.new(registry.key) + def authorized_token(*accesses) + token = JSONWebToken::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = params[:service] token.subject = current_user.try(:username) - token[:access] = access + token[:access] = accesses token end - def scopes + def scope return unless params[:scope] - @scopes ||= begin - scope = process_scope(params[:scope]) - [scope].compact - end + @scope ||= process_scope(params[:scope]) end def process_scope(scope) type, name, actions = scope.split(':', 3) actions = actions.split(',') + return unless type == 'repository' - case type - when 'repository' - process_repository_access(type, name, actions) - end + process_repository_access(type, name, actions) end def process_repository_access(type, name, actions) @@ -60,9 +55,9 @@ module Auth case requested_action when 'pull' - requested_project == project || can?(current_user, :read_container_registry, requested_project) + requested_project == project || can?(current_user, :read_container_image, requested_project) when 'push' - requested_project == project || can?(current_user, :create_container_registry, requested_project) + requested_project == project || can?(current_user, :create_container_image, requested_project) else false end diff --git a/lib/jwt/rsa_token.rb b/lib/json_web_token/rsa_token.rb similarity index 97% rename from lib/jwt/rsa_token.rb rename to lib/json_web_token/rsa_token.rb index d7df9269e1..d6d6af7089 100644 --- a/lib/jwt/rsa_token.rb +++ b/lib/json_web_token/rsa_token.rb @@ -1,4 +1,4 @@ -module JWT +module JSONWebToken class RSAToken < Token attr_reader :key_file diff --git a/lib/jwt/token.rb b/lib/json_web_token/token.rb similarity index 97% rename from lib/jwt/token.rb rename to lib/json_web_token/token.rb index f13abf2b71..5b67715b0b 100644 --- a/lib/jwt/token.rb +++ b/lib/json_web_token/token.rb @@ -1,4 +1,4 @@ -module JWT +module JSONWebToken class Token attr_accessor :issuer, :subject, :audience, :id attr_accessor :issued_at, :not_before, :expire_time diff --git a/spec/lib/jwt/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb similarity index 95% rename from spec/lib/jwt/rsa_token_spec.rb rename to spec/lib/json_web_token/rsa_token_spec.rb index a5b1d3a67d..4462cdde9a 100644 --- a/spec/lib/jwt/rsa_token_spec.rb +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -1,4 +1,4 @@ -describe JWT::RSAToken do +describe JSONWebToken::RSAToken do let(:rsa_key) { generate_key } let(:rsa_token) { described_class.new(nil) } let(:rsa_encoded) { rsa_token.encoded } diff --git a/spec/lib/jwt/token_spec.rb b/spec/lib/json_web_token/token_spec.rb similarity index 92% rename from spec/lib/jwt/token_spec.rb rename to spec/lib/json_web_token/token_spec.rb index 92fdc3f1b7..3d955e4d77 100644 --- a/spec/lib/jwt/token_spec.rb +++ b/spec/lib/json_web_token/token_spec.rb @@ -1,4 +1,4 @@ -describe JWT::Token do +describe JSONWebToken::Token do let(:token) { described_class.new } context 'custom parameters' do diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 6e86a3dcf5..a293736813 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -18,7 +18,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do before do allow(Gitlab.config.registry).to receive_messages(registry_settings) - allow_any_instance_of(JWT::RSAToken).to receive(:key).and_return(rsa_key) + allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key) end shared_examples 'an authenticated' do From c275c91373103c10dfe5e21afd6fc54a7a4dfdbe Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 18:26:26 -0500 Subject: [PATCH 242/449] Change abilities from container_registry to container_image --- app/controllers/projects/container_registry_controller.rb | 4 ++-- app/helpers/projects_helper.rb | 2 +- app/views/projects/container_registry/_tag.html.haml | 2 +- app/views/projects/container_registry/index.html.haml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index c470789a5b..e48f205d21 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -1,6 +1,6 @@ class Projects::ContainerRegistryController < Projects::ApplicationController - before_action :authorize_read_container_registry! - before_action :authorize_update_container_registry!, only: [:destroy] + before_action :authorize_read_container_image! + before_action :authorize_update_container_image!, only: [:destroy] layout 'project' def index diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 7113e28924..466929443d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -152,7 +152,7 @@ module ProjectsHelper nav_tabs << :builds end - if can?(current_user, :read_container_registry, project) + if can?(current_user, :read_container_image, project) nav_tabs << :container_registry end diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index bf816d109b..10eabc6cd6 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -14,7 +14,7 @@ = pluralize(tag.layers.size, "layer") %td = time_ago_in_words(tag.created_at) - - if can?(current_user, :update_container_registry, @project) + - if can?(current_user, :update_container_image, @project) %td.content .controls.hidden-xs.pull-right = link_to namespace_project_container_registry_path(@project.namespace, @project, tag.name), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index 990253719b..f3b2881ce0 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -33,7 +33,7 @@ %th Image ID %th Size %th Created - - if can?(current_user, :update_container_registry, @project) + - if can?(current_user, :update_container_image, @project) %th - @tags.each do |tag| From 0d93e93e794d179a4cbc5302aece27296e4abfe4 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Sat, 14 May 2016 23:14:25 +0900 Subject: [PATCH 243/449] Add tests for unintentional filtering bug in MR (cf. !3872) --- CHANGELOG | 2 +- .../user_lists_merge_requests_spec.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..b5ee7681df 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,7 +43,7 @@ v 8.8.0 (unreleased) - API support for the 'since' and 'until' operators on commit requests (Paco Guzman) - Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko) - Expire repository exists? and has_visible_content? caches after a push if necessary - - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) + - Fix unintentional filtering bug in Issue/MR sorted by milestone due (Takuya Noguchi) - Fix adding a todo for private group members (Ahmad Sherif) - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 - Total method execution timings are no longer tracked diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index cc7f78e732..2c7e1c748a 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -38,6 +38,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true expect(page).to have_content 'lfs' expect(page).not_to have_content 'fix' expect(page).not_to have_content 'markdown' + expect(count_merge_requests).to eq(1) end it 'filters on a specific assignee' do @@ -46,6 +47,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true expect(page).not_to have_content 'lfs' expect(page).to have_content 'fix' expect(page).to have_content 'markdown' + expect(count_merge_requests).to eq(2) end it 'sorts by newest' do @@ -53,6 +55,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true expect(first_merge_request).to include('lfs') expect(last_merge_request).to include('fix') + expect(count_merge_requests).to eq(3) end it 'sorts by oldest' do @@ -60,30 +63,35 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true expect(first_merge_request).to include('fix') expect(last_merge_request).to include('lfs') + expect(count_merge_requests).to eq(3) end it 'sorts by last updated' do visit_merge_requests(project, sort: sort_value_recently_updated) expect(first_merge_request).to include('lfs') + expect(count_merge_requests).to eq(3) end it 'sorts by oldest updated' do visit_merge_requests(project, sort: sort_value_oldest_updated) expect(first_merge_request).to include('markdown') + expect(count_merge_requests).to eq(3) end it 'sorts by milestone due soon' do visit_merge_requests(project, sort: sort_value_milestone_soon) expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(3) end it 'sorts by milestone due later' do visit_merge_requests(project, sort: sort_value_milestone_later) expect(first_merge_request).to include('markdown') + expect(count_merge_requests).to eq(3) end it 'filters on one label and sorts by due soon' do @@ -94,6 +102,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true sort: sort_value_due_date_soon) expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) end context 'while filtering on two labels' do @@ -110,6 +119,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true sort: sort_value_due_date_soon) expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) end context 'filter on assignee and' do @@ -119,6 +129,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true sort: sort_value_due_date_soon) expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) end end end @@ -134,4 +145,8 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true def last_merge_request page.all('ul.mr-list > li').last.text end + + def count_merge_requests + page.all('ul.mr-list > li').count + end end From 8d445fe665df313c16a88c319fde9bdfb97339df Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 19:45:33 -0500 Subject: [PATCH 244/449] Improve JwtController --- app/controllers/jwt_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 0edf084e9e..f5aa5397ff 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -9,7 +9,7 @@ class JwtController < ApplicationController def auth service = SERVICES[params[:service]] - head :not_found unless service + return head :not_found unless service result = service.new(@project, @user, auth_params).execute From e8f7e5516bf0449f0d7f435ae720aa35f40e1dd0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 19:45:48 -0500 Subject: [PATCH 245/449] Added specs for JwtController --- spec/requests/jwt_controller_spec.rb | 87 ++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 spec/requests/jwt_controller_spec.rb diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb new file mode 100644 index 0000000000..782d710e1b --- /dev/null +++ b/spec/requests/jwt_controller_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe JwtController do + let(:services) { { 'test' => TestService } } + let(:parameters) { { service: 'test' } } + let(:ok_status) { { status: 'OK' } } + + before { allow_any_instance_of(JwtController).to receive(:SERVICES).and_return services } + + context 'existing service' do + before { expect_any_instance_of(TestService).to receive(:execute).and_return(ok_status) } + + subject! { get '/jwt/auth', parameters } + + it { expect(response.status).to eq(200) } + end + + context 'when using authorized request' do + context 'using CI token' do + let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } + let(:headers) { { HTTP_AUTHENTICATION: authorize('gitlab-ci-token', project.runners_token) } } + + context 'project with enabled CI' do + let(:builds_enabled) { true } + + it do + expect(TestService).to receive(:new).with(project, nil, parameters).and_call_original + + get '/jwt/auth', parameters, headers + end + end + + context 'project with disabled CI' do + let(:builds_enabled) { false } + + it do + expect(TestService).to receive(:new).with(project, nil, parameters).and_call_original + + get '/jwt/auth', parameters, headers + end + end + end + + context 'using User login' do + let(:user) { create(:user) } + let(:headers) { { HTTP_AUTHENTICATION: authorize('user', 'password') } } + + before { expect_any_instance_of(Gitlab::Auth).to receive(:find).with('user', 'password').and_return(user) } + + it do + expect(TestService).to receive(:new).with(nil, user, parameters).and_call_original + + get '/jwt/auth', parameters, headers + end + end + + context 'using invalid login' do + let(:headers) { { HTTP_AUTHENTICATION: authorize('invalid', 'password') } } + + subject! { get '/jwt/auth', parameters, headers } + + it { expect(response.status).to eq(403) } + end + end + + context 'unknown service' do + subject! { get '/jwt/auth', service: 'unknown' } + + it { expect(response.status).to eq(404) } + end + + def authorize(login, password) + ActionController::HttpAuthentication::Basic.encode_credentials(login, password) + end + + class TestService + attr_accessor :project, :current_user, :params + + def initialize(project, user, params = {}) + @project, @current_user, @params = project, user, params.dup + end + + def execute + { status: 'OK' } + end + end +end From 003526e2ee408bc6be3596436288213cc57d1bcd Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 14 May 2016 19:47:16 -0500 Subject: [PATCH 246/449] Add method new_pipeline --- app/services/ci/create_pipeline_service.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 223514968f..e13f4fce13 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -13,6 +13,8 @@ module Ci raise RuntimeError, 'Insufficient permissions to create a new pipeline' end + pipeline = new_pipeline + Ci::Commit.transaction do unless pipeline.config_processor raise ArgumentError, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file' @@ -27,6 +29,10 @@ module Ci private + def new_pipeline + project.ci_commits.new(sha: commit.id, ref: params[:ref], before_sha: Gitlab::Git::BLANK_SHA) + end + def ref_names @ref_names ||= project.repository.ref_names end @@ -34,9 +40,5 @@ module Ci def commit @commit ||= project.commit(params[:ref]) end - - def pipeline - @pipeline ||= project.ci_commits.new(sha: commit.id, ref: params[:ref], before_sha: Gitlab::Git::BLANK_SHA) - end end end From dec6b31c2772f7af792f7739b8b3b86a4dbd75db Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 14 May 2016 10:47:56 -0500 Subject: [PATCH 247/449] Fix Error 500 when attempting to retrieve project license when HEAD points to non-existent ref Closes #17537 --- CHANGELOG | 1 + app/models/repository.rb | 12 ++++++++---- spec/models/repository_spec.rb | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..81dfd1a1eb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.0 (unreleased) - Snippets tab under user profile. !4001 (Long Nguyen) - Fix error when using link to uploads in global snippets + - Fix Error 500 when attempting to retrieve project license when HEAD points to non-existent ref - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) - Use a case-insensitive comparison in sanitizing URI schemes - Project#open_branches has been cleaned up and no longer loads entire records into memory. diff --git a/app/models/repository.rb b/app/models/repository.rb index 0eff74320f..f89a106309 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -475,7 +475,7 @@ class Repository end def license_blob - return nil if !exists? || empty? + return nil unless head_exists? cache.fetch(:license_blob) do tree(:head).blobs.find do |file| @@ -485,7 +485,7 @@ class Repository end def license_key - return nil if !exists? || empty? + return nil unless head_exists? cache.fetch(:license_key) do Licensee.license(path).try(:key) @@ -493,7 +493,7 @@ class Repository end def gitlab_ci_yml - return nil if !exists? || empty? + return nil unless head_exists? @gitlab_ci_yml ||= tree(:head).blobs.find do |file| file.name == '.gitlab-ci.yml' @@ -961,7 +961,7 @@ class Repository end def main_language - return if empty? || rugged.head_unborn? + return unless head_exists? Linguist::Repository.new(rugged, rugged.head.target_id).language end @@ -981,4 +981,8 @@ class Repository def cache @cache ||= RepositoryCache.new(path_with_namespace) end + + def head_exists? + exists? && !empty? && !rugged.head_unborn? + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 34a13f9b5c..2d5238b77f 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -176,6 +176,15 @@ describe Repository, models: true do repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') end + it 'handles when HEAD points to non-existent ref' do + repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + rugged = double('rugged') + expect(rugged).to receive(:head_unborn?).and_return(true) + expect(repository).to receive(:rugged).and_return(rugged) + + expect(repository.license_blob).to be_nil + end + it 'looks in the root_ref only' do repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown') repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false) @@ -204,6 +213,15 @@ describe Repository, models: true do repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') end + it 'handles when HEAD points to non-existent ref' do + repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + rugged = double('rugged') + expect(rugged).to receive(:head_unborn?).and_return(true) + expect(repository).to receive(:rugged).and_return(rugged) + + expect(repository.license_key).to be_nil + end + it 'returns nil when no license is detected' do expect(repository.license_key).to be_nil end From 7b88dca77eeb2a93b5a343d27af513ea28222379 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 00:33:06 -0500 Subject: [PATCH 248/449] Update JwtController specs --- spec/requests/jwt_controller_spec.rb | 57 ++++++++++------------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 782d710e1b..7bb71365a4 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -1,61 +1,58 @@ require 'spec_helper' describe JwtController do - let(:services) { { 'test' => TestService } } - let(:parameters) { { service: 'test' } } - let(:ok_status) { { status: 'OK' } } + let(:service) { double(execute: {}) } + let(:service_class) { double(new: service) } + let(:service_name) { 'test' } + let(:parameters) { { service: service_name } } - before { allow_any_instance_of(JwtController).to receive(:SERVICES).and_return services } + before { stub_const('JwtController::SERVICES', service_name => service_class) } context 'existing service' do - before { expect_any_instance_of(TestService).to receive(:execute).and_return(ok_status) } - subject! { get '/jwt/auth', parameters } it { expect(response.status).to eq(200) } + + context 'returning custom http code' do + let(:service) { double(execute: { http_status: 505 }) } + + it { expect(response.status).to eq(505) } + end end context 'when using authorized request' do context 'using CI token' do let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } - let(:headers) { { HTTP_AUTHENTICATION: authorize('gitlab-ci-token', project.runners_token) } } + let(:headers) { { authorization: credentials('gitlab_ci_token', project.runners_token) } } + + subject! { get '/jwt/auth', parameters, headers } context 'project with enabled CI' do let(:builds_enabled) { true } - it do - expect(TestService).to receive(:new).with(project, nil, parameters).and_call_original - - get '/jwt/auth', parameters, headers - end + it { expect(service_class).to have_received(:new).with(project, nil, parameters) } end context 'project with disabled CI' do let(:builds_enabled) { false } - it do - expect(TestService).to receive(:new).with(project, nil, parameters).and_call_original - - get '/jwt/auth', parameters, headers - end + it { expect(response.status).to eq(403) } end end context 'using User login' do let(:user) { create(:user) } - let(:headers) { { HTTP_AUTHENTICATION: authorize('user', 'password') } } + let(:headers) { { authorization: credentials('user', 'password') } } before { expect_any_instance_of(Gitlab::Auth).to receive(:find).with('user', 'password').and_return(user) } - it do - expect(TestService).to receive(:new).with(nil, user, parameters).and_call_original + subject! { get '/jwt/auth', parameters, headers } - get '/jwt/auth', parameters, headers - end + it { expect(service_class).to have_received(:new).with(nil, user, parameters) } end context 'using invalid login' do - let(:headers) { { HTTP_AUTHENTICATION: authorize('invalid', 'password') } } + let(:headers) { { authorization: credentials('invalid', 'password') } } subject! { get '/jwt/auth', parameters, headers } @@ -69,19 +66,7 @@ describe JwtController do it { expect(response.status).to eq(404) } end - def authorize(login, password) + def credentials(login, password) ActionController::HttpAuthentication::Basic.encode_credentials(login, password) end - - class TestService - attr_accessor :project, :current_user, :params - - def initialize(project, user, params = {}) - @project, @current_user, @params = project, user, params.dup - end - - def execute - { status: 'OK' } - end - end end From b575b2f1ef10c44a59151567aa1aa390f4a94ab1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 08:47:48 -0500 Subject: [PATCH 249/449] Fix Container Service full access token --- app/services/auth/container_registry_authentication_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 69ad634c36..9bfc1085fb 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -16,7 +16,7 @@ module Auth def self.full_access_token(*names) registry = Gitlab.config.registry - token = ::JWT::RSAToken.new(registry.key) + token = JSONWebToken::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = AUDIENCE token[:access] = names.map do |name| From dfd0e2450aabc3b5c322c4a4382edb84caa7101b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 08:52:26 -0500 Subject: [PATCH 250/449] Improve authentication service specs --- ...ntainer_registry_authentication_service.rb | 6 +-- spec/lib/json_web_token/rsa_token_spec.rb | 28 ++++++++---- ...er_registry_authentication_service_spec.rb | 44 +++++++++++++++---- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index bbbc84475c..c61d339ffd 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -7,10 +7,10 @@ module Auth if params[:offline_token] return error('forbidden', 403) unless current_user + else + return error('forbidden', 401) unless scope end - return error('forbidden', 401) unless scope - { token: authorized_token(scope).encoded } end @@ -21,7 +21,7 @@ module Auth token.issuer = registry.issuer token.audience = params[:service] token.subject = current_user.try(:username) - token[:access] = accesses + token[:access] = accesses.compact token end diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb index 4462cdde9a..0c3d3ea701 100644 --- a/spec/lib/json_web_token/rsa_token_spec.rb +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -1,5 +1,17 @@ describe JSONWebToken::RSAToken do - let(:rsa_key) { generate_key } + let(:rsa_key) do + OpenSSL::PKey::RSA.new <<-eos.strip_heredoc + -----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAMA5sXIBE0HwgIB40iNidN4PGWzOyLQK0bsdOBNgpEXkDlZBvnak + OUgAPF+rME4PB0Yl415DabUI40T5UNmlwxcCAwEAAQJAZtY2pSwIFm3JAXIh0cZZ + iXcAfiJ+YzuqinUOS+eW2sBCAEzjcARlU/o6sFQgtsOi4FOMczAd1Yx8UDMXMmrw + 2QIhAPBgVhJiTF09pdmeFWutCvTJDlFFAQNbrbo2X2x/9WF9AiEAzLgqMKeStSRu + H9N16TuDrUoO8R+DPqriCwkKrSHaWyMCIFzMhE4inuKcSywBaLmiG4m3GQzs++Al + A6PRG/PSTpQtAiBxtBg6zdf+JC3GH3zt/dA0/10tL4OF2wORfYQghRzyYQIhAL2l + 0ZQW+yLIZAGrdBFWYEAa52GZosncmzBNlsoTgwE4 + -----END RSA PRIVATE KEY----- + eos + end let(:rsa_token) { described_class.new(nil) } let(:rsa_encoded) { rsa_token.encoded } @@ -13,19 +25,19 @@ describe JSONWebToken::RSAToken do it { expect{subject}.to_not raise_error } it { expect(subject.first).to include('key' => 'value') } + it do + expect(subject.second).to eq( + "typ" => "JWT", + "alg" => "RS256", + "kid" => "OGXY:4TR7:FAVO:WEM2:XXEW:E4FP:TKL7:7ACK:TZAF:D54P:SUIA:P3B2") + end end context 'for invalid key to raise an exception' do - let(:new_key) { generate_key } + let(:new_key) { OpenSSL::PKey::RSA.generate(512) } subject { JWT.decode(rsa_encoded, new_key) } it { expect{subject}.to raise_error(JWT::DecodeError) } end end - - private - - def generate_key - OpenSSL::PKey::RSA.generate(512) - end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index a293736813..4a6cd132e8 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -57,15 +57,28 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end - shared_examples 'a forbidden' do + shared_examples 'a unauthorized' do it { is_expected.to include(http_status: 401) } it { is_expected.to_not include(:token) } end + shared_examples 'a forbidden' do + it { is_expected.to include(http_status: 403) } + it { is_expected.to_not include(:token) } + end + context 'user authorization' do let(:project) { create(:project) } let(:current_user) { create(:user) } + context 'allow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'an authenticated' + end + context 'allow developer to push images' do before { project.team << [current_user, :developer] } @@ -103,13 +116,21 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull,push" } end - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end end context 'project authorization' do let(:current_project) { create(:empty_project) } + context 'disallow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'a forbidden' + end + context 'allow to pull and push images' do let(:current_params) do { scope: "repository:#{current_project.path_with_namespace}:pull,push" } @@ -133,7 +154,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for private' do let(:project) { create(:empty_project, :private) } - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end end @@ -144,20 +165,27 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for all' do let(:project) { create(:empty_project, :public) } - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end end - end end context 'unauthorized' do + context 'disallow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'a forbidden' + end + context 'for invalid scope' do let(:current_params) do { scope: 'invalid:aa:bb' } end - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end context 'for private project' do @@ -167,7 +195,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull" } end - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end context 'for public project' do @@ -186,7 +214,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:push" } end - it_behaves_like 'a forbidden' + it_behaves_like 'a unauthorized' end end end From 938d5b6fe4bb3d7d6d85188fc5fd1aac77803577 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 10:46:54 -0500 Subject: [PATCH 251/449] Fix http status codes for container registry authentication service --- ...container_registry_authentication_service.rb | 4 ++-- ...iner_registry_authentication_service_spec.rb | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index a0cda4adc5..169e0387e8 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -6,9 +6,9 @@ module Auth return error('not found', 404) unless registry.enabled if params[:offline_token] - return error('forbidden', 403) unless current_user + return error('forbidden', 401) unless current_user else - return error('forbidden', 401) unless scope + return error('forbidden', 403) unless scope end { token: authorized_token(scope).encoded } diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 409d08e49f..143d992b6e 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -57,11 +57,6 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end - shared_examples 'a unauthorized' do - it { is_expected.to include(http_status: 401) } - it { is_expected.to_not include(:token) } - end - shared_examples 'a forbidden' do it { is_expected.to include(http_status: 403) } it { is_expected.to_not include(:token) } @@ -116,7 +111,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull,push" } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end @@ -154,7 +149,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for private' do let(:project) { create(:empty_project, :private) } - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end @@ -165,7 +160,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for all' do let(:project) { create(:empty_project, :public) } - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end end @@ -199,7 +194,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: 'invalid:aa:bb' } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end context 'for private project' do @@ -209,7 +204,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull" } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end context 'for public project' do @@ -228,7 +223,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:push" } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end end From 28ed9907047dd07089833d5b7bb8cd800e0ddff6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 10:46:54 -0500 Subject: [PATCH 252/449] Fix http status codes for container registry authentication service --- ...container_registry_authentication_service.rb | 4 ++-- ...iner_registry_authentication_service_spec.rb | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index c61d339ffd..72d28af7f7 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -6,9 +6,9 @@ module Auth return error('not found', 404) unless registry.enabled if params[:offline_token] - return error('forbidden', 403) unless current_user + return error('forbidden', 401) unless current_user else - return error('forbidden', 401) unless scope + return error('forbidden', 403) unless scope end { token: authorized_token(scope).encoded } diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 4a6cd132e8..3ea252ed44 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -57,11 +57,6 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end - shared_examples 'a unauthorized' do - it { is_expected.to include(http_status: 401) } - it { is_expected.to_not include(:token) } - end - shared_examples 'a forbidden' do it { is_expected.to include(http_status: 403) } it { is_expected.to_not include(:token) } @@ -116,7 +111,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull,push" } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end @@ -154,7 +149,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for private' do let(:project) { create(:empty_project, :private) } - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end @@ -165,7 +160,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow for all' do let(:project) { create(:empty_project, :public) } - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end end @@ -185,7 +180,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: 'invalid:aa:bb' } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end context 'for private project' do @@ -195,7 +190,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:pull" } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end context 'for public project' do @@ -214,7 +209,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { scope: "repository:#{project.path_with_namespace}:push" } end - it_behaves_like 'a unauthorized' + it_behaves_like 'a forbidden' end end end From bc32a3ecc7ec2e5ebf6e8c9d9fc1b218a3620f36 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 15 May 2016 10:47:38 -0500 Subject: [PATCH 253/449] Fix offline_token http_status code --- app/services/auth/container_registry_authentication_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 72d28af7f7..b636f55d03 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -6,7 +6,7 @@ module Auth return error('not found', 404) unless registry.enabled if params[:offline_token] - return error('forbidden', 401) unless current_user + return error('forbidden', 403) unless current_user else return error('forbidden', 403) unless scope end From b9306c2e82fec5b74416ccdd5481dfed3f8fbf51 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Date: Sun, 15 May 2016 19:47:41 +0100 Subject: [PATCH 254/449] Add cache count metrics to rails cache --- CHANGELOG | 1 + lib/gitlab/metrics/subscribers/rails_cache.rb | 12 +++++++----- .../metrics/subscribers/rails_cache_spec.rb | 16 +++++++++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 30d4955c9f..ca59f488e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,6 +51,7 @@ v 8.8.0 (unreleased) - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) - Hide left sidebar on phone screens to give more space for content - Redesign navigation for profile and group pages + - Add counter metrics for rails cache v 8.7.5 - Fix relative links in wiki pages. !4050 diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb index 49e5f86e6e..8e345e8ae4 100644 --- a/lib/gitlab/metrics/subscribers/rails_cache.rb +++ b/lib/gitlab/metrics/subscribers/rails_cache.rb @@ -6,26 +6,28 @@ module Gitlab attach_to :active_support def cache_read(event) - increment(:cache_read_duration, event.duration) + increment(:cache_read, event.duration) end def cache_write(event) - increment(:cache_write_duration, event.duration) + increment(:cache_write, event.duration) end def cache_delete(event) - increment(:cache_delete_duration, event.duration) + increment(:cache_delete, event.duration) end def cache_exist?(event) - increment(:cache_exists_duration, event.duration) + increment(:cache_exists, event.duration) end def increment(key, duration) return unless current_transaction current_transaction.increment(:cache_duration, duration) - current_transaction.increment(key, duration) + current_transaction.increment(:cache_count, 1) + current_transaction.increment("#{key}_duration".to_sym, duration) + current_transaction.increment("#{key}_count".to_sym, 1) end private diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb index e01b0b4bd2..d824dc5443 100644 --- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_read' do it 'increments the cache_read duration' do expect(subscriber).to receive(:increment). - with(:cache_read_duration, event.duration) + with(:cache_read, event.duration) subscriber.cache_read(event) end @@ -18,7 +18,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_write' do it 'increments the cache_write duration' do expect(subscriber).to receive(:increment). - with(:cache_write_duration, event.duration) + with(:cache_write, event.duration) subscriber.cache_write(event) end @@ -27,7 +27,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_delete' do it 'increments the cache_delete duration' do expect(subscriber).to receive(:increment). - with(:cache_delete_duration, event.duration) + with(:cache_delete, event.duration) subscriber.cache_delete(event) end @@ -36,7 +36,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_exist?' do it 'increments the cache_exists duration' do expect(subscriber).to receive(:increment). - with(:cache_exists_duration, event.duration) + with(:cache_exists, event.duration) subscriber.cache_exist?(event) end @@ -61,10 +61,16 @@ describe Gitlab::Metrics::Subscribers::RailsCache do expect(transaction).to receive(:increment). with(:cache_duration, event.duration) + expect(transaction).to receive(:increment). + with(:cache_count, 1) + expect(transaction).to receive(:increment). with(:cache_delete_duration, event.duration) - subscriber.increment(:cache_delete_duration, event.duration) + expect(transaction).to receive(:increment). + with(:cache_delete_count, 1) + + subscriber.increment(:cache_delete, event.duration) end end end From bec350528cdc81e26476780f1ca3db8171b3ece8 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 3 May 2016 12:42:55 +0100 Subject: [PATCH 255/449] Force password change after admin reset When an admin changes a user's password for them, force the user to reset the password after logging in by expiring the new password immediately. --- CHANGELOG | 1 + app/controllers/admin/users_controller.rb | 1 + .../admin/users_controller_spec.rb | 76 +++++++++++++++++++ spec/features/admin/admin_users_spec.rb | 3 + 4 files changed, 81 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ca59f488e0..02b94e5a0b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 8.8.0 (unreleased) - Display informative message when new milestone is created - Sanitize milestones and labels titles - Support multi-line tag messages. !3833 (Calin Seciu) + - Force users to reset their password after an admin changes it - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view - Backport GitHub Enterprise import support from EE diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f2f654c7bc..6908a3bf94 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -119,6 +119,7 @@ class Admin::UsersController < Admin::ApplicationController user_params_with_pass.merge!( password: params[:user][:password], password_confirmation: params[:user][:password_confirmation], + password_expires_at: Time.now ) end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index ce2a62ae1f..6caf37ddc2 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -114,6 +114,82 @@ describe Admin::UsersController do end end + describe 'POST update' do + context 'when the password has changed' do + def update_password(user, password, password_confirmation = nil) + params = { + id: user.to_param, + user: { + password: password, + password_confirmation: password_confirmation || password + } + } + + post :update, params + end + + context 'when the new password is valid' do + it 'redirects to the user' do + update_password(user, 'AValidPassword1') + + expect(response).to redirect_to(admin_user_path(user)) + end + + it 'updates the password' do + update_password(user, 'AValidPassword1') + + expect { user.reload }.to change { user.encrypted_password } + end + + it 'sets the new password to expire immediately' do + update_password(user, 'AValidPassword1') + + expect { user.reload }.to change { user.password_expires_at }.to(a_value <= Time.now) + end + end + + context 'when the new password is invalid' do + it 'shows the edit page again' do + update_password(user, 'invalid') + + expect(response).to render_template(:edit) + end + + it 'returns the error message' do + update_password(user, 'invalid') + + expect(assigns[:user].errors).to contain_exactly(a_string_matching(/too short/)) + end + + it 'does not update the password' do + update_password(user, 'invalid') + + expect { user.reload }.not_to change { user.encrypted_password } + end + end + + context 'when the new password does not match the password confirmation' do + it 'shows the edit page again' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect(response).to render_template(:edit) + end + + it 'returns the error message' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect(assigns[:user].errors).to contain_exactly(a_string_matching(/doesn't match/)) + end + + it 'does not update the password' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect { user.reload }.not_to change { user.encrypted_password } + end + end + end + end + describe "POST impersonate" do context "when the user is blocked" do before do diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 4570e40912..6dee0cd8d4 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -210,6 +210,8 @@ describe "Admin::Users", feature: true do before do fill_in "user_name", with: "Big Bang" fill_in "user_email", with: "bigbang@mail.com" + fill_in "user_password", with: "AValidPassword1" + fill_in "user_password_confirmation", with: "AValidPassword1" check "user_admin" click_button "Save changes" end @@ -223,6 +225,7 @@ describe "Admin::Users", feature: true do @simple_user.reload expect(@simple_user.name).to eq('Big Bang') expect(@simple_user.is_admin?).to be_truthy + expect(@simple_user.password_expires_at).to be <= Time.now end end end From bda2c44a0b2f032d8c3eb9b9b8d79933fb40e651 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 27 Apr 2016 09:01:43 +0100 Subject: [PATCH 256/449] Project variables UI Closes #14091 --- .../projects/variables_controller.rb | 30 ++++++++++++-- .../projects/variables/_content.html.haml | 8 ++++ app/views/projects/variables/_form.html.haml | 10 +++++ app/views/projects/variables/_table.html.haml | 24 +++++++++++ app/views/projects/variables/index.html.haml | 17 ++++++++ app/views/projects/variables/show.html.haml | 41 ++++--------------- config/routes.rb | 2 +- 7 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 app/views/projects/variables/_content.html.haml create mode 100644 app/views/projects/variables/_form.html.haml create mode 100644 app/views/projects/variables/_table.html.haml create mode 100644 app/views/projects/variables/index.html.haml diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 0023465457..6f06872939 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -3,20 +3,44 @@ class Projects::VariablesController < Projects::ApplicationController layout 'project_settings' + def index + @variable = Ci::Variable.new + end + def show + @variable = @project.variables.find(params[:id]) end def update - if project.update_attributes(project_params) + @variable = @project.variables.find(params[:id]) + + if @variable.update_attributes(project_params) + redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully updated.' + else + render action: "show" + end + end + + def create + @variable = Ci::Variable.new(project_params) + + if @variable.valid? && @project.variables << @variable redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.' else - render action: 'show' + render action: "index" end end + def destroy + @key = @project.variables.find(params[:id]) + @key.destroy + + redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.' + end + private def project_params - params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) + params.require(:variable).permit([:id, :key, :value, :_destroy]) end end diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml new file mode 100644 index 0000000000..0249e0c1bf --- /dev/null +++ b/app/views/projects/variables/_content.html.haml @@ -0,0 +1,8 @@ +%h4.prepend-top-0 + Secret Variables +%p + These variables will be set to environment by the runner. +%p + So you can use them for passwords, secret keys or whatever you want. +%p + The value of the variable can be visible in build log if explicitly asked to do so. diff --git a/app/views/projects/variables/_form.html.haml b/app/views/projects/variables/_form.html.haml new file mode 100644 index 0000000000..a5bae83e0c --- /dev/null +++ b/app/views/projects/variables/_form.html.haml @@ -0,0 +1,10 @@ += form_for [@project.namespace.becomes(Namespace), @project, @variable] do |f| + = form_errors(@variable) + + .form-group + = f.label :key, "Key", class: "label-light" + = f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true + .form-group + = f.label :value, "Value", class: "label-light" + = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true + = f.submit btn_text, class: "btn btn-save" diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml new file mode 100644 index 0000000000..1a73320009 --- /dev/null +++ b/app/views/projects/variables/_table.html.haml @@ -0,0 +1,24 @@ +.table-responsive + %table.table + %colgroup + %col + %col + %col{ width: 100 } + %thead + %th Key + %th Value + %th + %tbody + - @project.variables.each do |variable| + %tr + %td= variable.key + %td= variable.value + %td + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent" do + %span.sr-only + Update + = icon("pencil") + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent", method: :delete, data: { confirm: "Are you sure?" } do + %span.sr-only + Remove + = icon("trash") diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml new file mode 100644 index 0000000000..89ef79fb2e --- /dev/null +++ b/app/views/projects/variables/index.html.haml @@ -0,0 +1,17 @@ +- page_title "Variables" + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + = render "content" + .col-lg-9 + %h5.prepend-top-0 + Add a variable + = render "form", btn_text: "Add new variable" + %hr + %h5.prepend-top-0 + Your variables (#{@project.variables.size}) + - if @project.variables.empty? + %p.profile-settings-message.text-center.append-bottom-0 + No variables found, add one with the form above. + - else + = render "table" diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index ca284b84d3..297a53ca98 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -1,36 +1,9 @@ - page_title "Variables" -%h3.page-title - Secret Variables -%p.light - These variables will be set to environment by the runner. - %br - So you can use them for passwords, secret keys or whatever you want. - %br - The value of the variable can be visible in build log if explicitly asked to do so. - -%hr - - -= nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| - = form_errors(@project) - - = f.fields_for :variables do |variable_form| - .form-group - = variable_form.label :key, 'Key', class: 'control-label' - .col-sm-10 - = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE" - - .form-group - = variable_form.label :value, 'Value', class: 'control-label' - .col-sm-10 - = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: "" - - = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right prepend-top-10' - %hr - %p - .clearfix - = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right' - - .form-actions - = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url +.row.prepend-top-default.append-bottom-default + .col-lg-3 + = render "content" + .col-lg-9 + %h5.prepend-top-0 + Update variable + = render "form", btn_text: "Save variable" diff --git a/config/routes.rb b/config/routes.rb index 9e776a1f54..511ab5cfcd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -660,7 +660,7 @@ Rails.application.routes.draw do end resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resource :variables, only: [:show, :update] + resources :variables, only: [:index, :show, :update, :create, :destroy] resources :triggers, only: [:index, :create, :destroy] resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do From 99428f193b2fe51b16e43f7cd98029326b5c30f9 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 27 Apr 2016 12:17:17 +0100 Subject: [PATCH 257/449] Updated project variable tests --- app/views/projects/variables/_table.html.haml | 6 +- spec/features/variables_spec.rb | 59 ++++++++++++++----- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml index 1a73320009..f51a52d7cb 100644 --- a/app/views/projects/variables/_table.html.haml +++ b/app/views/projects/variables/_table.html.haml @@ -1,4 +1,4 @@ -.table-responsive +.table-responsive.variables-table %table.table %colgroup %col @@ -14,11 +14,11 @@ %td= variable.key %td= variable.value %td - = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent" do + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do %span.sr-only Update = icon("pencil") - = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent", method: :delete, data: { confirm: "Are you sure?" } do + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do %span.sr-only Remove = icon("trash") diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index afea1840cd..48e2dae488 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -1,24 +1,53 @@ require 'spec_helper' -describe "Variables" do - let(:user) { create(:user) } - before { login_as(user) } +describe 'Project variables', js: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:variable) { create(:ci_variable, key: 'test') } - describe "specific runners" do - before do - @project = FactoryGirl.create :empty_project - @project.team << [user, :master] + before do + login_as(user) + project.team << [user, :master] + project.variables << variable + + visit namespace_project_variables_path(project.namespace, project) + end + + it 'should show list of variables' do + page.within('.variables-table') do + expect(page).to have_content(variable.key) + end + end + + it 'should add new variable' do + fill_in('variable_key', with: 'key') + fill_in('variable_value', with: 'key value') + click_button('Add new variable') + + page.within('.variables-table') do + expect(page).to have_content('key') + end + end + + it 'should delete variable' do + page.within('.variables-table') do + find('.btn-variable-delete').click end - it "creates variable", js: true do - visit namespace_project_variables_path(@project.namespace, @project) - click_on "Add a variable" - fill_in "Key", with: "SECRET_KEY" - fill_in "Value", with: "SECRET_VALUE" - click_on "Save changes" + expect(page).to_not have_selector('variables-table') + end - expect(page).to have_content("Variables were successfully updated.") - expect(@project.variables.count).to eq(1) + it 'should edit variable' do + page.within('.variables-table') do + find('.btn-variable-edit').click + end + + fill_in('variable_key', with: 'key') + fill_in('variable_value', with: 'key value') + click_button('Save variable') + + page.within('.variables-table') do + expect(page).to have_content('key') end end end From 91480e5e7f2fb5e732839958b53c521bf7206939 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 12 May 2016 12:20:09 +0100 Subject: [PATCH 258/449] Tidy up IssuesFinder specs - Don't do setup in spec bodies. - Don't `describe` a symbol. - Don't use 'should'. --- spec/finders/issues_finder_spec.rb | 150 ++++++++++++++++------------- 1 file changed, 81 insertions(+), 69 deletions(-) diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index bc607a2975..f905703dd6 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe IssuesFinder do - let(:user) { create :user } - let(:user2) { create :user } - let(:project1) { create(:project) } - let(:project2) { create(:project) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:project1) { create(:empty_project) } + let(:project2) { create(:empty_project) } let(:milestone) { create(:milestone, project: project1) } let(:label) { create(:label, project: project2) } let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) } @@ -16,101 +16,113 @@ describe IssuesFinder do project1.team << [user, :master] project2.team << [user, :developer] project2.team << [user2, :developer] + + issue1 + issue2 + issue3 end - describe :execute do - before :each do - issue1 - issue2 - issue3 - end + describe '#execute' do + let(:search_user) { user } + let(:params) { {} } + let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute } context 'scope: all' do - it 'should filter by all' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(3) + let(:scope) { 'all' } + + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3) end - it 'should filter by assignee id' do - params = { scope: "all", assignee_id: user.id, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(2) + context 'filtering by assignee ID' do + let(:params) { { assignee_id: user.id } } + + it 'returns issues assigned to that user' do + expect(issues).to contain_exactly(issue1, issue2) + end end - it 'should filter by author id' do - params = { scope: "all", author_id: user2.id, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to eq([issue3]) + context 'filtering by author ID' do + let(:params) { { author_id: user2.id } } + + it 'returns issues created by that user' do + expect(issues).to contain_exactly(issue3) + end end - it 'should filter by milestone id' do - params = { scope: "all", milestone_title: milestone.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to eq([issue1]) + context 'filtering by milestone' do + let(:params) { { milestone_title: milestone.title } } + + it 'returns issues assigned to that milestone' do + expect(issues).to contain_exactly(issue1) + end end - it 'should filter by no milestone id' do - params = { scope: "all", milestone_title: Milestone::None.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to match_array([issue2, issue3]) + context 'filtering by no milestone' do + let(:params) { { milestone_title: Milestone::None.title } } + + it 'returns issues with no milestone' do + expect(issues).to contain_exactly(issue2, issue3) + end end - it 'should filter by label name' do - params = { scope: "all", label_name: label.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to eq([issue2]) + context 'filtering by label' do + let(:params) { { label_name: label.title } } + + it 'returns issues with that label' do + expect(issues).to contain_exactly(issue2) + end end - it 'returns unique issues when filtering by multiple labels' do - label2 = create(:label, project: project2) + context 'filtering by multiple labels' do + let(:params) { { label_name: [label.title, label2.title].join(',') } } + let(:label2) { create(:label, project: project2) } - create(:label_link, label: label2, target: issue2) + before { create(:label_link, label: label2, target: issue2) } - params = { - scope: 'all', - label_name: [label.title, label2.title].join(','), - state: 'opened' - } - - issues = IssuesFinder.new(user, params).execute - - expect(issues).to eq([issue2]) + it 'returns the unique issues with any of those labels' do + expect(issues).to contain_exactly(issue2) + end end - it 'should filter by no label name' do - params = { scope: "all", label_name: Label::None.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to match_array([issue1, issue3]) + context 'filtering by no label' do + let(:params) { { label_name: Label::None.title } } + + it 'returns issues with no labels' do + expect(issues).to contain_exactly(issue1, issue3) + end end - it 'should be empty for unauthorized user' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new(nil, params).execute - expect(issues.size).to be_zero + context 'when the user is unauthorized' do + let(:search_user) { nil } + + it 'returns no results' do + expect(issues).to be_empty + end end - it 'should not include unauthorized issues' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new(user2, params).execute - expect(issues.size).to eq(2) - expect(issues).not_to include(issue1) - expect(issues).to include(issue2) - expect(issues).to include(issue3) + context 'when the user can see some, but not all, issues' do + let(:search_user) { user2 } + + it 'returns only issues they can see' do + expect(issues).to contain_exactly(issue2, issue3) + end end end context 'personal scope' do - it 'should filter by assignee' do - params = { scope: "assigned-to-me", state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(2) + let(:scope) { 'assigned-to-me' } + + it 'returns issue assigned to the user' do + expect(issues).to contain_exactly(issue1, issue2) end - it 'should filter by project' do - params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(1) + context 'filtering by project' do + let(:params) { { project_id: project1.id } } + + it 'returns issues assigned to the user in that project' do + expect(issues).to contain_exactly(issue1) + end end end end From 750b2ff0eec67926e737a40c7975cce2b58e27f7 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 11 May 2016 17:38:34 +0100 Subject: [PATCH 259/449] Make upcoming milestone work across projects Before: we took the next milestone due across all projects in the search and found issues whose milestone title matched that one. Problems: 1. The milestone could be closed. 2. Different projects have milestones with different schedules. 3. Different projects have milestones with different titles. 4. Different projects can have milestones with different schedules, but the _same_ title. That means we could show issues from a past milestone, or one that's far in the future. After: gather the ID of the next milestone on each project we're looking at, and find issues with those milestone IDs. Problems: 1. For a lot of projects, this can return a lot of IDs. 2. The SQL query has to be different between Postgres and MySQL, because MySQL is much more lenient with HAVING: as well as the columns appearing in GROUP BY or in aggregate clauses, MySQL allows them to appear in the SELECT list (un-aggregated). --- CHANGELOG | 1 + app/finders/issuable_finder.rb | 4 ++-- app/models/milestone.rb | 14 ++++++++++-- spec/finders/issues_finder_spec.rb | 34 ++++++++++++++++++++++++++++++ spec/models/milestone_spec.rb | 31 +++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ca59f488e0..50937f5f83 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.8.0 (unreleased) - Make build status canceled if any of the jobs was canceled and none failed - Upgrade Sidekiq to 4.1.2 - Added /health_check endpoint for checking service status + - Make 'upcoming' filter for milestones work better across projects - Sanitize repo paths in new project error message - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f00f3f709e..5849e00662 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -252,8 +252,8 @@ class IssuableFinder if filter_by_no_milestone? items = items.where(milestone_id: [-1, nil]) elsif filter_by_upcoming_milestone? - upcoming = Milestone.where(project_id: projects).upcoming - items = items.joins(:milestone).where(milestones: { title: upcoming.try(:title) }) + upcoming_ids = Milestone.upcoming_ids_by_projects(projects) + items = items.joins(:milestone).where(milestone_id: upcoming_ids) else items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index e4fdd23bad..6b01e48d7f 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -67,8 +67,18 @@ class Milestone < ActiveRecord::Base @link_reference_pattern ||= super("milestones", /(?\d+)/) end - def self.upcoming - self.where('due_date > ?', Time.now).reorder(due_date: :asc).first + def self.upcoming_ids_by_projects(projects) + rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now) + + if Gitlab::Database.postgresql? + rel.order(:project_id, :due_date).pluck('DISTINCT ON (project_id) id') + else + rel. + group(:project_id). + having('due_date = MIN(due_date)'). + pluck(:id, :project_id, :due_date). + map(&:first) + end end def to_reference(from_project = nil) diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index f905703dd6..ec8809e692 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -66,6 +66,40 @@ describe IssuesFinder do end end + context 'filtering by upcoming milestone' do + let(:params) { { milestone_title: Milestone::Upcoming.name } } + + let(:project_no_upcoming_milestones) { create(:empty_project, :public) } + let(:project_next_1_1) { create(:empty_project, :public) } + let(:project_next_8_8) { create(:empty_project, :public) } + + let(:yesterday) { Date.today - 1.day } + let(:tomorrow) { Date.today + 1.day } + let(:two_days_from_now) { Date.today + 2.days } + let(:ten_days_from_now) { Date.today + 10.days } + + let(:milestones) do + [ + create(:milestone, :closed, project: project_no_upcoming_milestones), + create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now), + create(:milestone, project: project_next_1_1, title: '8.8', due_date: ten_days_from_now), + create(:milestone, project: project_next_8_8, title: '1.1', due_date: yesterday), + create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow) + ] + end + + before do + milestones.each do |milestone| + create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user) + end + end + + it 'returns issues in the upcoming milestone for each project' do + expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8') + expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now) + end + end + context 'filtering by label' do let(:params) { { label_name: label.title } } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 247a9fa991..210c5f7eb4 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -204,4 +204,35 @@ describe Milestone, models: true do to eq([milestone]) end end + + describe '.upcoming_ids_by_projects' do + let(:project_1) { create(:empty_project) } + let(:project_2) { create(:empty_project) } + let(:project_3) { create(:empty_project) } + let(:projects) { [project_1, project_2, project_3] } + + let!(:past_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now - 1.day) } + let!(:current_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 1.day) } + let!(:future_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 2.days) } + + let!(:past_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now - 1.day) } + let!(:closed_milestone_project_2) { create(:milestone, :closed, project: project_2, due_date: Time.now + 1.day) } + let!(:current_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now + 2.days) } + + let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) } + + let(:milestone_ids) { Milestone.upcoming_ids_by_projects(projects) } + + it 'returns the next upcoming open milestone ID for each project' do + expect(milestone_ids).to contain_exactly(current_milestone_project_1.id, current_milestone_project_2.id) + end + + context 'when the projects have no open upcoming milestones' do + let(:projects) { [project_3] } + + it 'returns no results' do + expect(milestone_ids).to be_empty + end + end + end end From e8058bd23100949607ac8c353f482067c0ecd25a Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 16 May 2016 10:23:21 +0100 Subject: [PATCH 260/449] Return a relation with Postgres Postgres only needs to select a single column, so that can used as a sub-query where `Milestone.upcoming_ids_by_projects` is actually used in `IssuableFinder`. MySQL needs to select the `due_date` column because it's used in the `HAVING` clause, so it has to return an array of IDs. --- app/models/milestone.rb | 2 +- spec/models/milestone_spec.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 6b01e48d7f..fe9a281f36 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -71,7 +71,7 @@ class Milestone < ActiveRecord::Base rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now) if Gitlab::Database.postgresql? - rel.order(:project_id, :due_date).pluck('DISTINCT ON (project_id) id') + rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id') else rel. group(:project_id). diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 210c5f7eb4..1e18c788b5 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -221,7 +221,9 @@ describe Milestone, models: true do let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) } - let(:milestone_ids) { Milestone.upcoming_ids_by_projects(projects) } + # The call to `#try` is because this returns a relation with a Postgres DB, + # and an array of IDs with a MySQL DB. + let(:milestone_ids) { Milestone.upcoming_ids_by_projects(projects).map { |id| id.try(:id) || id } } it 'returns the next upcoming open milestone ID for each project' do expect(milestone_ids).to contain_exactly(current_milestone_project_1.id, current_milestone_project_2.id) From c9b3c08c21daf421ec36828c990ccceb21e1577f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 16 May 2016 10:26:11 +0100 Subject: [PATCH 261/449] Changed table border color Fixed issue with form errors not displaying --- .../stylesheets/framework/variables.scss | 2 +- app/views/projects/variables/_table.html.haml | 25 ++++++++++--------- app/views/projects/variables/index.html.haml | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 5fa4c26660..7360bdbcc8 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -12,7 +12,7 @@ $gutter_inner_width: 258px; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; -$table-border-color: #ececec; +$table-border-color: #f0f0f0; $background-color: #fafafa; /* diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml index f51a52d7cb..6c43f822db 100644 --- a/app/views/projects/variables/_table.html.haml +++ b/app/views/projects/variables/_table.html.haml @@ -10,15 +10,16 @@ %th %tbody - @project.variables.each do |variable| - %tr - %td= variable.key - %td= variable.value - %td - = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do - %span.sr-only - Update - = icon("pencil") - = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do - %span.sr-only - Remove - = icon("trash") + - if variable.id? + %tr + %td= variable.key + %td= variable.value + %td + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do + %span.sr-only + Update + = icon("pencil") + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do + %span.sr-only + Remove + = icon("trash") diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml index 89ef79fb2e..09bb54600a 100644 --- a/app/views/projects/variables/index.html.haml +++ b/app/views/projects/variables/index.html.haml @@ -11,7 +11,7 @@ %h5.prepend-top-0 Your variables (#{@project.variables.size}) - if @project.variables.empty? - %p.profile-settings-message.text-center.append-bottom-0 + %p.settings-message.text-center.append-bottom-0 No variables found, add one with the form above. - else = render "table" From 5f66bfe7edbdae24008a9ebeaadca9441b011184 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 22 Apr 2016 21:23:55 +0100 Subject: [PATCH 262/449] Added due date calendar to new issue form Closes #15516 --- .../javascripts/issuable_form.js.coffee | 9 ++ app/assets/stylesheets/framework/forms.scss | 8 +- app/views/shared/issuable/_form.html.haml | 82 ++++++++++--------- 3 files changed, 58 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee index 7a788f761b..72ae3bde81 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -20,6 +20,15 @@ class @IssuableForm @initWip() + $issuableDueDate = $('#issuable-due-date') + + if $issuableDueDate.length + $('.datepicker').datepicker( + dateFormat: 'yy-mm-dd', + onSelect: (dateText, inst) -> + $issuableDueDate.val dateText + ).datepicker 'setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val()) + initAutosave: -> new Autosave @titleField, [ document.location.pathname, diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 558b133f59..46acc3b772 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -28,10 +28,6 @@ input[type='text'].danger { } label { - &.control-label { - @extend .col-sm-2; - } - &.inline-label { margin: 0; } @@ -41,6 +37,10 @@ label { } } +.control-label { + @extend .col-sm-2; +} + .inline-input-group { width: 250px; } diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 5c52cc6d1d..9ac5c0d9e4 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -44,45 +44,53 @@ This issue is confidential and should only be visible to team members - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) + - has_due_date = issuable.has_attribute?(:due_date) %hr - .form-group - .issue-assignee - = f.label :assignee_id, "Assignee", class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true, include_blank: true) -   - = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' - .form-group - .issue-milestone - = f.label :milestone_id, "Milestone", class: 'control-label' - .col-sm-10 - - if milestone_options(issuable).present? + .row + %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } + .form-group.issue-assignee + = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - = f.select(:milestone_id, milestone_options(issuable), - { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - - else - .prepend-top-10 - %span.light No open milestones available. -   - - if can? current_user, :admin_milestone, issuable.project - = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank - .form-group - - has_labels = issuable.project.labels.any? - = f.label :label_ids, "Labels", class: 'control-label' - .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) } - - if has_labels - .issuable-form-select-holder - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - - else - %span.light No labels yet. -   - - if can? current_user, :admin_label, issuable.project - = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank + = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", + placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, + selected: issuable.assignee_id, project: @target_project || @project, + first_user: true, current_user: true, include_blank: true) + %br + = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline' + .form-group.issue-milestone + = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } + - if milestone_options(issuable).present? + .issuable-form-select-holder + = f.select(:milestone_id, milestone_options(issuable), + { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) + - else + .prepend-top-10 + %span.light No open milestones available. + - if can? current_user, :admin_milestone, issuable.project + %br + = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + .form-group + - has_labels = issuable.project.labels.any? + = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } + - if has_labels + .issuable-form-select-holder + = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, + { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } + - else + %span.light No labels yet. + - if can? current_user, :admin_label, issuable.project + %br + = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + - if issuable.has_attribute?(:due_date) + .col-lg-6 + .form-group + = f.label :due_date, "Due date", class: "control-label" + = f.hidden_field :due_date, id: "issuable-due-date" + .col-sm-10 + .datepicker - if issuable.can_move?(current_user) %hr From 82ea43555773f9acb5c02dd659ab47cec3d2ebb0 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 22 Apr 2016 21:48:45 +0100 Subject: [PATCH 263/449] Added tests --- spec/features/issues_spec.rb | 60 +++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d5755c293c..93c1fc0d03 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -64,6 +64,64 @@ describe 'Issues', feature: true do end end + describe 'due date', js: true do + context 'on new form' do + before do + visit new_namespace_project_issue_path(project.namespace, project) + end + + it 'should save with due date' do + date = Date.today.at_beginning_of_month + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + + page.within '.datepicker' do + click_link date.day + end + + expect(find('#issuable-due-date', :visible => false).value).to eq date.to_s + + click_button 'Submit issue' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + end + + context 'on edit form' do + let(:issue) { create(:issue, author: @user,project: project, due_date: Date.today.at_beginning_of_month.to_s) } + + before do + visit edit_namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should save with due date' do + date = Date.today.at_beginning_of_month + + expect(find('#issuable-due-date', :visible => false).value).to eq date.to_s + + date = date.tomorrow + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + + page.within '.datepicker' do + click_link date.day + end + + expect(find('#issuable-due-date', :visible => false).value).to eq date.to_s + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + end + end + describe 'Issue info' do it 'excludes award_emoji from comment count' do issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar') @@ -331,7 +389,7 @@ describe 'Issues', feature: true do page.within '.assignee' do click_link 'Edit' end - + page.within '.dropdown-menu-user' do click_link @user.name end From f6d321ae9cd9f751952e0ff2eda9718eecdc4bf1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 22 Apr 2016 22:42:22 +0100 Subject: [PATCH 264/449] Fixed rubocop issues --- spec/features/issues_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 93c1fc0d03..dd114db7e8 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -80,7 +80,7 @@ describe 'Issues', feature: true do click_link date.day end - expect(find('#issuable-due-date', :visible => false).value).to eq date.to_s + expect(find('#issuable-due-date', visible: false).value).to eq date.to_s click_button 'Submit issue' @@ -100,7 +100,7 @@ describe 'Issues', feature: true do it 'should save with due date' do date = Date.today.at_beginning_of_month - expect(find('#issuable-due-date', :visible => false).value).to eq date.to_s + expect(find('#issuable-due-date', visible: false).value).to eq date.to_s date = date.tomorrow @@ -111,7 +111,7 @@ describe 'Issues', feature: true do click_link date.day end - expect(find('#issuable-due-date', :visible => false).value).to eq date.to_s + expect(find('#issuable-due-date', visible: false).value).to eq date.to_s click_button 'Save changes' From d4777cd86b79e5db7b1e351444c26b72cdaad3db Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 25 Apr 2016 08:04:37 +0100 Subject: [PATCH 265/449] Updated if statement --- app/views/shared/issuable/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 9ac5c0d9e4..d06a287281 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -84,7 +84,7 @@ - if can? current_user, :admin_label, issuable.project %br = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" - - if issuable.has_attribute?(:due_date) + - if has_due_date .col-lg-6 .form-group = f.label :due_date, "Due date", class: "control-label" From caca3bb7b8e0d39947607d2704bed8ae4f04965c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 16 May 2016 15:40:44 +0100 Subject: [PATCH 266/449] Fixed project settings alert colors In another MR, the class name was changed making all the alert messages have no background color --- app/views/projects/deploy_keys/index.html.haml | 4 ++-- app/views/projects/hooks/index.html.haml | 2 +- .../projects/protected_branches/_branches_list.html.haml | 2 +- app/views/projects/triggers/index.html.haml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml index e230834e8b..04fbb37d93 100644 --- a/app/views/projects/deploy_keys/index.html.haml +++ b/app/views/projects/deploy_keys/index.html.haml @@ -19,7 +19,7 @@ %ul.well-list = render @enabled_keys - else - .profile-settings-message.text-center + .settings-message.text-center No deploy keys found. Create one with the form above or add existing one below. %h5.prepend-top-default Deploy keys from projects you have access to (#{@available_project_keys.size}) @@ -27,7 +27,7 @@ %ul.well-list = render @available_project_keys - else - .profile-settings-message.text-center + .settings-message.text-center No deploy keys from your projects could be found. Create one with the form above or add existing one below. - if @available_public_keys.any? %h5.prepend-top-default diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 36c1d69f06..cffe9a01a9 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -80,5 +80,5 @@ - @hooks.each do |hook| = render "project_hook", hook: hook - else - %p.profile-settings-message.text-center.append-bottom-0 + %p.settings-message.text-center.append-bottom-0 No webhooks found, add one in the form above. diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index b9e9dd8aae..565905cbe7 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,7 +1,7 @@ %h5.prepend-top-0 Already Protected (#{@branches.size}) - if @branches.empty? - %p.profile-settings-message.text-center + %p.settings-message.text-center No branches are protected, protect a branch with the form above. - else - can_admin_project = can?(current_user, :admin_project, @project) diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index f91885b216..d73ac98716 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -18,7 +18,7 @@ %th = render partial: 'trigger', collection: @triggers, as: :trigger - else - %p.profile-settings-message.text-center.append-bottom-default + %p.settings-message.text-center.append-bottom-default There are no triggers to use, add one by the button below. = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f| From 72577033888a77eeb2f4d362c2ae2331c1cbf6d7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 09:45:18 -0500 Subject: [PATCH 267/449] Bring back port to registry configuration --- config/gitlab.yml.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 2e383bc90f..d935121d88 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -178,7 +178,8 @@ production: &base registry: # enabled: true - # host: localhost + # host: registry.example.com + # port: 5000 # api_url: http://localhost:5000/ # key: config/registry.key # issuer: omnibus-certificate From 623102d4556c42fd386e37724e638156dabca277 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 09:48:39 -0500 Subject: [PATCH 268/449] Use registry.port to construct the registry host --- app/models/project.rb | 6 +++--- app/views/projects/container_registry/index.html.haml | 2 +- config/initializers/1_settings.rb | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index ed7719ed31..a3eb7d83e4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -332,15 +332,15 @@ class Project < ActiveRecord::Base @container_registry_repository ||= begin token = Auth::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace) url = Gitlab.config.registry.api_url - host = Gitlab.config.registry.host - registry = ContainerRegistry::Registry.new(url, token: token, path: host) + host_port = Gitlab.config.registry.host_port + registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) registry[path_with_namespace] end end def container_registry_repository_url if Gitlab.config.registry.enabled - "#{Gitlab.config.registry.host}/#{path_with_namespace}" + "#{Gitlab.config.registry.host_port}/#{path_with_namespace}" end end diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index f3b2881ce0..6a1e46b14b 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -16,7 +16,7 @@ To start using container images hosted on GitLab you first need to login: %pre %code - docker login #{Gitlab.config.registry.host} + docker login #{Gitlab.config.registry.host_port} %br Then you are free to create and upload a container image with build and push commands: %pre diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d1fcb053be..20f920ea85 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -249,9 +249,11 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes Settings['registry'] ||= Settingslogic.new({}) Settings.registry['enabled'] ||= false Settings.registry['host'] ||= "example.com" +Settings.registry['port'] ||= nil Settings.registry['api_url'] ||= "http://localhost:5000/" Settings.registry['key'] ||= nil Settings.registry['issuer'] ||= nil +Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].join(':') # # Git LFS From 5b3ac8c83f19bfa8b897f307c056c521fd1d052a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 09:45:18 -0500 Subject: [PATCH 269/449] Bring back port to registry configuration --- config/gitlab.yml.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 2e383bc90f..d935121d88 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -178,7 +178,8 @@ production: &base registry: # enabled: true - # host: localhost + # host: registry.example.com + # port: 5000 # api_url: http://localhost:5000/ # key: config/registry.key # issuer: omnibus-certificate From e60bbdf9d9609fd0624be61dcf8a3a0432040ad1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 09:49:55 -0500 Subject: [PATCH 270/449] Fix path to tag --- lib/container_registry/registry.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb index a86ddb9326..d3b117eeac 100644 --- a/lib/container_registry/registry.rb +++ b/lib/container_registry/registry.rb @@ -3,7 +3,7 @@ module ContainerRegistry attr_reader :uri, :client, :path def initialize(uri, options = {}) - @path = uri || options[:path] + @path = options[:path] || uri @uri = URI.parse(uri) @client = ContainerRegistry::Client.new(uri, options) end From a26ec8a10660ca6ffcf49abac6ddad57fd99f80d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 09:51:15 -0500 Subject: [PATCH 271/449] Compact host and port --- config/initializers/1_settings.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 20f920ea85..129fd9b79f 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -253,7 +253,7 @@ Settings.registry['port'] ||= nil Settings.registry['api_url'] ||= "http://localhost:5000/" Settings.registry['key'] ||= nil Settings.registry['issuer'] ||= nil -Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].join(':') +Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':') # # Git LFS From 114d023b1b6f0e14adefb72980ec1658946bd897 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Tue, 17 May 2016 01:41:26 +0900 Subject: [PATCH 272/449] Clean up the images from GitLab CI --- app/assets/images/ci/arch.jpg | Bin 25222 -> 0 bytes app/assets/images/ci/favicon.ico | Bin 5430 -> 0 bytes app/assets/images/ci/loader.gif | Bin 4405 -> 0 bytes app/assets/images/ci/no_avatar.png | Bin 1337 -> 0 bytes app/assets/images/ci/rails.png | Bin 6646 -> 0 bytes app/assets/images/ci/service_sample.png | Bin 76024 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/assets/images/ci/arch.jpg delete mode 100644 app/assets/images/ci/favicon.ico delete mode 100644 app/assets/images/ci/loader.gif delete mode 100644 app/assets/images/ci/no_avatar.png delete mode 100644 app/assets/images/ci/rails.png delete mode 100644 app/assets/images/ci/service_sample.png diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg deleted file mode 100644 index 0e05674e840c6946d5a40f340790289af2dc25f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25222 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 diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif deleted file mode 100644 index 2fcb8f2da0d283628878cbc74b9dd848109a4535..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4405 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 AQ~&?~ diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png deleted file mode 100644 index 65d29e3fd891b7b8deba8670cb7d74b03056e3d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76024 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 z 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 Vw Date: Mon, 16 May 2016 12:44:07 -0500 Subject: [PATCH 273/449] Fix left sidebar overlap when scroll on long menu Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/gitlab-theme.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index f47eb1f233..16cf394c42 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -9,6 +9,8 @@ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { .page-with-sidebar { .header-logo { + background: $color-darker; + a { color: $color-light; From 71ca2de7aabf3191c4f486ca15a78a5b7f6abd94 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 21 Apr 2016 15:55:54 -0300 Subject: [PATCH 274/449] Toggle email signup confirmation in admin settings --- CHANGELOG | 1 + .../admin/application_settings_controller.rb | 1 + app/models/user.rb | 5 +++ .../application_settings/_form.html.haml | 6 ++++ ...firmation_email_to_application_settings.rb | 8 +++++ .../registrations_controller_spec.rb | 33 +++++++++++++++++++ 6 files changed, 54 insertions(+) create mode 100644 db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb create mode 100644 spec/controllers/registrations_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index ca59f488e0..7a86263c0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -189,6 +189,7 @@ v 8.7.0 - Add Slack notifications when Wiki is edited (Sebastian Klier) - Diffs load at the correct point when linking from from number - Selected diff rows highlight + - Toggle sign-up confirmation emails in application settings - Fix emoji categories in the emoji picker - API: Properly display annotated tags for GET /projects/:id/repository/tags (Robert Schilling) - Add encrypted credentials for imported projects and migrate old ones diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8c973f0e4a..956d145f02 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -106,6 +106,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :email_author_in_body, :repository_checks_enabled, :metrics_packet_size, + :skip_user_confirmation_email, restricted_visibility_levels: [], import_sources: [], disabled_oauth_sign_in_sources: [] diff --git a/app/models/user.rb b/app/models/user.rb index 489bff3fa4..470734f5c2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -112,6 +112,7 @@ class User < ActiveRecord::Base before_save :ensure_external_user_rights after_save :ensure_namespace_correct after_initialize :set_projects_limit + before_create :check_confirmation_email after_create :post_create_hook after_destroy :post_destroy_hook @@ -307,6 +308,10 @@ class User < ActiveRecord::Base @reset_token end + def check_confirmation_email + skip_confirmation! if current_application_settings.skip_user_confirmation_email + end + def recently_sent_password_reset? reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index f7c799c968..6d6d87cdd5 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -103,6 +103,12 @@ = f.label :signup_enabled do = f.check_box :signup_enabled Sign-up enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :skip_confirmation_email do + = f.check_box :skip_user_confirmation_email + Skip sign-up email confirmation .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb b/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb new file mode 100644 index 0000000000..953f1cea89 --- /dev/null +++ b/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb @@ -0,0 +1,8 @@ +class AddSkipConfirmationEmailToApplicationSettings < ActiveRecord::Migration + def change + #Skip confirmation emails just for new installations + default_value = User.count > 0 ? false : true + + add_column :application_settings, :skip_user_confirmation_email, :boolean, default: default_value + end +end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb new file mode 100644 index 0000000000..b4ab767f73 --- /dev/null +++ b/spec/controllers/registrations_controller_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe RegistrationsController do + describe '#create' do + around(:each) do |example| + perform_enqueued_jobs do + example.run + end + end + + let(:user_params) { { "user"=> {"name"=>"new_user", "username"=>"new_username", "email"=>"new@user.com", "password"=>"Any_password"} } } + + context 'when skipping email confirmation' do + before { allow(current_application_settings).to receive(:skip_user_confirmation_email).and_return(true) } + + it 'logs user in directly' do + post(:create, user_params) + expect(ActionMailer::Base.deliveries.last).to be_nil + expect(subject.current_user).to be + end + end + + context 'when not skipping email confirmation' do + before { allow(current_application_settings).to receive(:skip_user_confirmation_email).and_return(false) } + + it 'does not authenticate user and sends confirmation email' do + post(:create, user_params) + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params["user"]["email"]) + expect(subject.current_user).to be_nil + end + end + end +end From c5526a2d9a946d99d7b4a72fc488fe6e0a9ad60b Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 28 Apr 2016 17:09:15 -0300 Subject: [PATCH 275/449] Change skip_user_confirmation_email to send_user_confirmation_email --- CHANGELOG | 5 ++++- .../admin/application_settings_controller.rb | 2 +- app/models/user.rb | 3 ++- app/views/admin/application_settings/_form.html.haml | 4 ++-- ...end_confirmation_email_to_application_settings.rb | 12 ++++++++++++ ...kip_confirmation_email_to_application_settings.rb | 8 -------- spec/controllers/registrations_controller_spec.rb | 12 ++++++------ spec/features/signup_spec.rb | 2 ++ spec/models/user_spec.rb | 1 + 9 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb delete mode 100644 db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb diff --git a/CHANGELOG b/CHANGELOG index 7a86263c0a..5841da3ac9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,8 @@ v 8.8.0 (unreleased) - Display informative message when new milestone is created - Sanitize milestones and labels titles - Support multi-line tag messages. !3833 (Calin Seciu) + - Toggle sign-up confirmation emails in application settings + - Replace Devise Async with Devise ActiveJob integration. !3902 (Connor Shea) - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view - Backport GitHub Enterprise import support from EE @@ -93,6 +95,8 @@ v 8.7.1 v 8.7.0 - Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented - Fix vulnerability that made it possible to gain access to private labels and milestones + +v 8.7.0 (unreleased) - The number of InfluxDB points stored per UDP packet can now be configured - Fix error when cross-project label reference used with non-existent project - Transactions for /internal/allowed now have an "action" tag set @@ -189,7 +193,6 @@ v 8.7.0 - Add Slack notifications when Wiki is edited (Sebastian Klier) - Diffs load at the correct point when linking from from number - Selected diff rows highlight - - Toggle sign-up confirmation emails in application settings - Fix emoji categories in the emoji picker - API: Properly display annotated tags for GET /projects/:id/repository/tags (Robert Schilling) - Add encrypted credentials for imported projects and migrate old ones diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 956d145f02..ff7a5cad2f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -106,7 +106,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :email_author_in_body, :repository_checks_enabled, :metrics_packet_size, - :skip_user_confirmation_email, + :send_user_confirmation_email, restricted_visibility_levels: [], import_sources: [], disabled_oauth_sign_in_sources: [] diff --git a/app/models/user.rb b/app/models/user.rb index 470734f5c2..bcadbd3e2f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -309,7 +309,8 @@ class User < ActiveRecord::Base end def check_confirmation_email - skip_confirmation! if current_application_settings.skip_user_confirmation_email + byebug + skip_confirmation! unless current_application_settings.send_user_confirmation_email end def recently_sent_password_reset? diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 6d6d87cdd5..289dda9a43 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -107,8 +107,8 @@ .col-sm-offset-2.col-sm-10 .checkbox = f.label :skip_confirmation_email do - = f.check_box :skip_user_confirmation_email - Skip sign-up email confirmation + = f.check_box :send_user_confirmation_email + Send sign-up email confirmation .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb b/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb new file mode 100644 index 0000000000..0fef2a831e --- /dev/null +++ b/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb @@ -0,0 +1,12 @@ +class AddSendConfirmationEmailToApplicationSettings < ActiveRecord::Migration + def up + add_column :application_settings, :send_user_confirmation_email, :boolean, default: false + + #Sets confirmation email to true by default on existing installations. + ApplicationSetting.update_all(send_user_confirmation_email: true) + end + + def down + remove_column :application_settings, :send_user_confirmation_email + end +end diff --git a/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb b/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb deleted file mode 100644 index 953f1cea89..0000000000 --- a/db/migrate/20160421141709_add_skip_confirmation_email_to_application_settings.rb +++ /dev/null @@ -1,8 +0,0 @@ -class AddSkipConfirmationEmailToApplicationSettings < ActiveRecord::Migration - def change - #Skip confirmation emails just for new installations - default_value = User.count > 0 ? false : true - - add_column :application_settings, :skip_user_confirmation_email, :boolean, default: default_value - end -end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index b4ab767f73..29f1847d9a 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -8,10 +8,10 @@ describe RegistrationsController do end end - let(:user_params) { { "user"=> {"name"=>"new_user", "username"=>"new_username", "email"=>"new@user.com", "password"=>"Any_password"} } } + let(:user_params) { { user: { name: "new_user", username: "new_username", email: "new@user.com", password: "Any_password" } } } - context 'when skipping email confirmation' do - before { allow(current_application_settings).to receive(:skip_user_confirmation_email).and_return(true) } + context 'when sending email confirmation' do + before { allow(current_application_settings).to receive(:send_user_confirmation_email).and_return(false) } it 'logs user in directly' do post(:create, user_params) @@ -20,12 +20,12 @@ describe RegistrationsController do end end - context 'when not skipping email confirmation' do - before { allow(current_application_settings).to receive(:skip_user_confirmation_email).and_return(false) } + context 'when not sending email confirmation' do + before { allow(current_application_settings).to receive(:send_user_confirmation_email).and_return(true) } it 'does not authenticate user and sends confirmation email' do post(:create, user_params) - expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params["user"]["email"]) + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) expect(subject.current_user).to be_nil end end diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 58aabd913e..c7840f26d8 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' feature 'Signup', feature: true do describe 'signup with no errors' do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } + it 'creates the user account and sends a confirmation email' do user = build(:user) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 10e7e69357..9581990666 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -141,6 +141,7 @@ describe User, models: true do end describe '#confirm' do + before { allow(current_application_settings).to receive(:send_user_confirmation_email).and_return(true) } let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') } it 'returns unconfirmed' do From 0baadd6cff63d5ac0048043381a0b7daff6e01c3 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 28 Apr 2016 17:22:35 -0300 Subject: [PATCH 276/449] Fix changelog file --- CHANGELOG | 2 -- app/models/user.rb | 1 - 2 files changed, 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5841da3ac9..7ebe10c0f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -95,8 +95,6 @@ v 8.7.1 v 8.7.0 - Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented - Fix vulnerability that made it possible to gain access to private labels and milestones - -v 8.7.0 (unreleased) - The number of InfluxDB points stored per UDP packet can now be configured - Fix error when cross-project label reference used with non-existent project - Transactions for /internal/allowed now have an "action" tag set diff --git a/app/models/user.rb b/app/models/user.rb index bcadbd3e2f..368a3f3cfb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -309,7 +309,6 @@ class User < ActiveRecord::Base end def check_confirmation_email - byebug skip_confirmation! unless current_application_settings.send_user_confirmation_email end From 7bb84e64979edda8e76f077bd58aeb35857aec23 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 6 May 2016 17:59:45 -0300 Subject: [PATCH 277/449] Change landing page when skipping confirmation email and add documentation --- app/controllers/registrations_controller.rb | 4 +- .../application_settings/_form.html.haml | 4 +- ...firmation_email_to_application_settings.rb | 2 +- doc/security/README.md | 1 + doc/security/user_email_confirmation.md | 7 +++ .../registrations_controller_spec.rb | 2 +- spec/features/signup_spec.rb | 45 ++++++++++++++----- 7 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 doc/security/user_email_confirmation.md diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 352bff1938..26eb15f49e 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -37,8 +37,8 @@ class RegistrationsController < Devise::RegistrationsController super end - def after_sign_up_path_for(_resource) - users_almost_there_path + def after_sign_up_path_for(user) + user.confirmed_at.present? ? dashboard_projects_path : users_almost_there_path end def after_inactive_sign_up_path_for(_resource) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 289dda9a43..df286852b9 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -106,9 +106,9 @@ .form-group .col-sm-offset-2.col-sm-10 .checkbox - = f.label :skip_confirmation_email do + = f.label :send_user_confirmation_email do = f.check_box :send_user_confirmation_email - Send sign-up email confirmation + Send confirmation email on sign-up .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb b/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb index 0fef2a831e..f92ef96050 100644 --- a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb +++ b/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb @@ -3,7 +3,7 @@ class AddSendConfirmationEmailToApplicationSettings < ActiveRecord::Migration add_column :application_settings, :send_user_confirmation_email, :boolean, default: false #Sets confirmation email to true by default on existing installations. - ApplicationSetting.update_all(send_user_confirmation_email: true) + execute "UPDATE application_settings SET send_user_confirmation_email=true" end def down diff --git a/doc/security/README.md b/doc/security/README.md index 4cd0fdd409..38706e48ec 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -8,3 +8,4 @@ - [User File Uploads](user_file_uploads.md) - [How we manage the CRIME vulnerability](crime_vulnerability.md) - [Enforce Two-factor authentication](two_factor_authentication.md) +- [Send email confirmation on sign-up](user_email_confirmation.md) diff --git a/doc/security/user_email_confirmation.md b/doc/security/user_email_confirmation.md new file mode 100644 index 0000000000..4293944ae8 --- /dev/null +++ b/doc/security/user_email_confirmation.md @@ -0,0 +1,7 @@ +# User email confirmation at sign-up + +Gitlab admin can enable email confirmation on sign-up, if you want to confirm all +user emails before they are able to sign-in. + +In the Admin area under **Settings** (`/admin/application_settings`), go to section +**Sign-in Restrictions** and look for **Send confirmation email on sign-up** option. diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 29f1847d9a..df70a589a8 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -16,7 +16,7 @@ describe RegistrationsController do it 'logs user in directly' do post(:create, user_params) expect(ActionMailer::Base.deliveries.last).to be_nil - expect(subject.current_user).to be + expect(subject.current_user).to_not be_nil end end diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index c7840f26d8..4229e82b44 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -2,22 +2,45 @@ require 'spec_helper' feature 'Signup', feature: true do describe 'signup with no errors' do - before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } - it 'creates the user account and sends a confirmation email' do - user = build(:user) + context "when sending confirmation email" do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } - visit root_path + it 'creates the user account and sends a confirmation email' do + user = build(:user) - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_password', with: user.password - click_button "Sign up" + visit root_path - expect(current_path).to eq users_almost_there_path - expect(page).to have_content("Please check your email to confirm your account") + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: user.password + click_button "Sign up" + + expect(current_path).to eq users_almost_there_path + expect(page).to have_content("Please check your email to confirm your account") + end end + + context "when not sending confirmation email" do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) } + + it 'creates the user account and goes to dashboard' do + user = build(:user) + + visit root_path + + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: user.password + click_button "Sign up" + + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Welcome! You have signed up successfully.") + end + end + end describe 'signup with errors' do From c9be74e24797c1dab5b443728349bb0c5ce969c3 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 16 May 2016 16:43:19 -0300 Subject: [PATCH 278/449] Fix single note api request --- lib/api/helpers.rb | 4 ++++ lib/api/notes.rb | 10 +++++----- spec/requests/api/notes_spec.rb | 17 ++++++++++++++--- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 40c967453f..1003b596ae 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -397,5 +397,9 @@ module API error!(errors[:access_level], 422) if errors[:access_level].any? not_found!(errors) end + + def noteable_ability_name(noteable) + "read_#{noteable.class.to_s.underscore.downcase}".to_sym + end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index f0116acd90..c49b107d1d 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -20,9 +20,8 @@ module API # GET /projects/:id/snippets/:noteable_id/notes get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) - read_ability_name = "read_#{@noteable.class.to_s.underscore.downcase}".to_sym - if can?(current_user, read_ability_name, @noteable) + if can?(current_user, noteable_ability_name(@noteable), @noteable) # We exclude notes that are cross-references and that cannot be viewed # by the current user. By doing this exclusion at this level and not # at the DB query level (which we cannot in that case), the current @@ -52,11 +51,12 @@ module API get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) @note = @noteable.notes.find(params[:note_id]) + can_read_note = can?(current_user, noteable_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user) - if @note.cross_reference_not_visible_for?(current_user) - not_found!("Note") - else + if can_read_note present @note, with: Entities::Note + else + not_found!("Note") end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index f9bfee9442..ed1ed5aeb9 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace) } + let!(:project) { create(:project, :public, namespace: user.namespace) } let!(:issue) { create(:issue, project: project, author: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } let!(:snippet) { create(:project_snippet, project: project, author: user) } @@ -51,7 +51,7 @@ describe API::API, api: true do expect(response.status).to eq(404) end - context "that references a private issue" do + context "and current user cannot view the notes" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) @@ -142,13 +142,24 @@ describe API::API, api: true do expect(response.status).to eq(404) end - context "that references a private issue" do + context "and current user cannot view the note" do it "should return a 404 error" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) expect(response.status).to eq(404) end + context "when issue is confidential" do + before { issue.update_attributes(confidential: true) } + + it "returns 404" do + get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user) + + expect(response.status).to eq(404) + end + end + + context "and current user can view the note" do it "should return an issue note by id" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) From 6286b28bbc9b62af729c45c45f953f5426f5bf52 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 14:05:45 -0400 Subject: [PATCH 279/449] Add an Event's target's title to its reference link Given an activity feed entry like: > Douwe Maan commented on [issue #123] at [gitlab-org/gitlab-ce] ...the `issue #123` link will now have a `title` attribute. --- app/helpers/events_helper.rb | 2 +- app/models/event.rb | 2 +- app/models/note.rb | 1 + app/views/events/event/_common.html.haml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 0bf328e7d1..2b33889b46 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -162,7 +162,7 @@ module EventsHelper "#{event.note_target_type} #{truncate event.note_target.to_reference}" end else - link_to event_note_target_path(event) do + link_to event_note_target_path(event), title: event.target_title do "#{event.note_target_type} #{truncate event.note_target.to_reference}" end end diff --git a/app/models/event.rb b/app/models/event.rb index 17ee48b91a..b54fbe6f7e 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -80,7 +80,7 @@ class Event < ActiveRecord::Base end def target_title - target.title if target && target.respond_to?(:title) + target.try(:title) end def created? diff --git a/app/models/note.rb b/app/models/note.rb index 7e5bdc09a8..55b9855724 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -19,6 +19,7 @@ class Note < ActiveRecord::Base delegate :gfm_reference, :local_reference, to: :noteable delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true + delegate :title, to: :noteable, allow_nil: true before_validation :set_award! diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index c994e3b997..f9f623cc03 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -4,7 +4,7 @@ = event_action_name(event) - if event.target - %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target] + %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target], title: event.target_title = event_preposition(event) From c05ed99bff637f51d16cc34663b911e46295a473 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 14:46:43 -0400 Subject: [PATCH 280/449] Simplify Event's target type-checking --- app/models/event.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/event.rb b/app/models/event.rb index b54fbe6f7e..a617694b7d 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -279,15 +279,15 @@ class Event < ActiveRecord::Base end def note_commit? - target.noteable_type == "Commit" + target.for_commit? end def issue_note? - note? && target && target.noteable_type == "Issue" + note? && target && target.for_issue? end def note_project_snippet? - target.noteable_type == "Snippet" + target.for_snippet? end def note_target From 3950f8074c078fb9320c3e9a468da15feb2d103e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 15:28:21 -0400 Subject: [PATCH 281/449] Fix titles for commit references in Event feed Prior, the `title` attribute was being included as an argument to the route helper rather than as an argument to `link_to`. --- app/helpers/events_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 2b33889b46..73320d8841 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -150,7 +150,8 @@ module EventsHelper link_to( namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, - anchor: dom_id(event.target), title: h(event.target_title)), + anchor: dom_id(event.target)), + title: h(event.target_title), class: "commit_short_id" ) do "#{event.note_target_type} #{event.note_short_commit_id}" From 8a7b5b17d325e13c989098c7e763de1f43505780 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 8 May 2016 15:37:43 -0400 Subject: [PATCH 282/449] Remove redundant `html_escape` calls from `title` attributes These get escaped automatically. --- app/helpers/events_helper.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 73320d8841..d0a63d5880 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -3,7 +3,7 @@ module EventsHelper author = event.author if author - link_to author.name, user_path(author.username), title: h(author.name) + link_to author.name, user_path(author.username), title: author.name else event.author_name end @@ -151,7 +151,7 @@ module EventsHelper namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, anchor: dom_id(event.target)), - title: h(event.target_title), + title: event.target_title, class: "commit_short_id" ) do "#{event.note_target_type} #{event.note_short_commit_id}" @@ -159,7 +159,7 @@ module EventsHelper elsif event.note_project_snippet? link_to(namespace_project_snippet_path(event.project.namespace, event.project, - event.note_target), title: h(event.project.name)) do + event.note_target), title: event.project.name) do "#{event.note_target_type} #{truncate event.note_target.to_reference}" end else From b2fc0a51a61669b46516c2a21cbd0abd893ff124 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 12 May 2016 15:31:53 -0500 Subject: [PATCH 283/449] Rename `Event#note_commit?` to `commit_note?` --- app/helpers/events_helper.rb | 10 +++++----- app/models/event.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index d0a63d5880..d7ddb31dbb 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -57,7 +57,7 @@ module EventsHelper words << event.ref_name words << "at" elsif event.commented? - if event.note_commit? + if event.commit_note? words << event.note_short_commit_id else words << "##{truncate event.note_target_iid}" @@ -84,12 +84,12 @@ module EventsHelper elsif event.merge_request? namespace_project_merge_request_url(event.project.namespace, event.project, event.merge_request) - elsif event.note? && event.note_commit? + elsif event.note? && event.commit_note? namespace_project_commit_url(event.project.namespace, event.project, event.note_target) elsif event.note? if event.note_target - if event.note_commit? + if event.commit_note? namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, anchor: dom_id(event.target)) @@ -134,7 +134,7 @@ module EventsHelper end def event_note_target_path(event) - if event.note? && event.note_commit? + if event.note? && event.commit_note? namespace_project_commit_path(event.project.namespace, event.project, event.note_target) else @@ -146,7 +146,7 @@ module EventsHelper def event_note_title_html(event) if event.note_target - if event.note_commit? + if event.commit_note? link_to( namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, diff --git a/app/models/event.rb b/app/models/event.rb index a617694b7d..5629f1ee44 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -278,7 +278,7 @@ class Event < ActiveRecord::Base Commit.truncate_sha(note_commit_id) end - def note_commit? + def commit_note? target.for_commit? end @@ -295,7 +295,7 @@ class Event < ActiveRecord::Base end def note_target_id - if note_commit? + if commit_note? target.commit_id else target.noteable_id.to_s From 0ec2ee71b31ea604a677e28ffb84f3ded1701395 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 12 May 2016 15:37:00 -0500 Subject: [PATCH 284/449] Rename `Event#note_project_snippet?` to `project_snippet_note?` --- app/helpers/events_helper.rb | 4 ++-- app/models/event.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index d7ddb31dbb..19281e3f3a 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -93,7 +93,7 @@ module EventsHelper namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, anchor: dom_id(event.target)) - elsif event.note_project_snippet? + elsif event.project_snippet_note? namespace_project_snippet_path(event.project.namespace, event.project, event.note_target) else @@ -156,7 +156,7 @@ module EventsHelper ) do "#{event.note_target_type} #{event.note_short_commit_id}" end - elsif event.note_project_snippet? + elsif event.project_snippet_note? link_to(namespace_project_snippet_path(event.project.namespace, event.project, event.note_target), title: event.project.name) do diff --git a/app/models/event.rb b/app/models/event.rb index 5629f1ee44..2e9da42dea 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -286,7 +286,7 @@ class Event < ActiveRecord::Base note? && target && target.for_issue? end - def note_project_snippet? + def project_snippet_note? target.for_snippet? end From 020e616822f0e634932bf948013f94eb7ef413bb Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 12 May 2016 16:47:45 -0500 Subject: [PATCH 285/449] Reduce duplication and branching logic in EventsHelper Remove unused methods from Event model --- app/helpers/events_helper.rb | 56 ++++++++++-------------------------- app/models/event.rb | 21 ++++++-------- app/models/project.rb | 2 ++ 3 files changed, 25 insertions(+), 54 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 19281e3f3a..e148938170 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -57,11 +57,7 @@ module EventsHelper words << event.ref_name words << "at" elsif event.commented? - if event.commit_note? - words << event.note_short_commit_id - else - words << "##{truncate event.note_target_iid}" - end + words << event.note_target_reference words << "at" elsif event.milestone? words << "##{event.target_iid}" if event.target_iid @@ -89,16 +85,7 @@ module EventsHelper event.note_target) elsif event.note? if event.note_target - if event.commit_note? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target)) - elsif event.project_snippet_note? - namespace_project_snippet_path(event.project.namespace, - event.project, event.note_target) - else - event_note_target_path(event) - end + event_note_target_path(event) end elsif event.push? push_event_feed_url(event) @@ -135,42 +122,29 @@ module EventsHelper def event_note_target_path(event) if event.note? && event.commit_note? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_target) + namespace_project_commit_path(event.project.namespace, + event.project, + event.note_target, + anchor: dom_id(event.target)) + elsif event.project_snippet_note? + namespace_project_snippet_path(event.project.namespace, + event.project, + event.note_target, + anchor: dom_id(event.target)) else polymorphic_path([event.project.namespace.becomes(Namespace), event.project, event.note_target], - anchor: dom_id(event.target)) + anchor: dom_id(event.target)) end end def event_note_title_html(event) if event.note_target - if event.commit_note? - link_to( - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target)), - title: event.target_title, - class: "commit_short_id" - ) do - "#{event.note_target_type} #{event.note_short_commit_id}" - end - elsif event.project_snippet_note? - link_to(namespace_project_snippet_path(event.project.namespace, - event.project, - event.note_target), title: event.project.name) do - "#{event.note_target_type} #{truncate event.note_target.to_reference}" - end - else - link_to event_note_target_path(event), title: event.target_title do - "#{event.note_target_type} #{truncate event.note_target.to_reference}" - end + link_to(event_note_target_path(event), title: event.target_title, class: 'has-tooltip') do + "#{event.note_target_type} #{event.note_target_reference}" end else - content_tag :strong do - "(deleted)" - end + content_tag(:strong, '(deleted)') end end diff --git a/app/models/event.rb b/app/models/event.rb index 2e9da42dea..716039fb54 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -266,18 +266,10 @@ class Event < ActiveRecord::Base branch? && project.default_branch != branch_name end - def note_commit_id - target.commit_id - end - def target_iid target.respond_to?(:iid) ? target.iid : target_id end - def note_short_commit_id - Commit.truncate_sha(note_commit_id) - end - def commit_note? target.for_commit? end @@ -302,12 +294,15 @@ class Event < ActiveRecord::Base end end - def note_target_iid - if note_target.respond_to?(:iid) - note_target.iid + def note_target_reference + return unless note_target + + # Commit#to_reference returns the full SHA, but we want the short one here + if commit_note? + note_target.short_id else - note_target_id - end.to_s + note_target.to_reference + end end def note_target_type diff --git a/app/models/project.rb b/app/models/project.rb index 6e85841db4..a3c4f1d8e9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -50,6 +50,8 @@ class Project < ActiveRecord::Base attr_accessor :new_default_branch attr_accessor :old_path_with_namespace + alias_attribute :title, :name + # Relations belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' From ecce94d3aab57e099a8897a860023b8ec7acac05 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 16 May 2016 12:42:34 -0300 Subject: [PATCH 286/449] fix default send confirmation value --- CHANGELOG | 3 +-- app/models/application_setting.rb | 3 ++- ...dd_send_user_confirmation_email_to_application_settings.rb} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename db/migrate/{20160421141709_add_send_confirmation_email_to_application_settings.rb => 20160516174813_add_send_user_confirmation_email_to_application_settings.rb} (81%) diff --git a/CHANGELOG b/CHANGELOG index 7ebe10c0f7..eb5fee8b07 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 8.8.0 (unreleased) - Fix error when using link to uploads in global snippets - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) - Use a case-insensitive comparison in sanitizing URI schemes + - Toggle sign-up confirmation emails in application settings - Project#open_branches has been cleaned up and no longer loads entire records into memory. - Escape HTML in commit titles in system note messages - Improve multiple branch push performance by memoizing permission checking @@ -28,8 +29,6 @@ v 8.8.0 (unreleased) - Display informative message when new milestone is created - Sanitize milestones and labels titles - Support multi-line tag messages. !3833 (Calin Seciu) - - Toggle sign-up confirmation emails in application settings - - Replace Devise Async with Devise ActiveJob integration. !3902 (Connor Shea) - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view - Backport GitHub Enterprise import support from EE diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 1a10768655..f5079f9244 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -120,7 +120,8 @@ class ApplicationSetting < ActiveRecord::Base recaptcha_enabled: false, akismet_enabled: false, repository_checks_enabled: true, - disabled_oauth_sign_in_sources: [] + disabled_oauth_sign_in_sources: [], + send_user_confirmation_email: false ) end diff --git a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb similarity index 81% rename from db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb rename to db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb index f92ef96050..c34e7ba540 100644 --- a/db/migrate/20160421141709_add_send_confirmation_email_to_application_settings.rb +++ b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb @@ -1,4 +1,4 @@ -class AddSendConfirmationEmailToApplicationSettings < ActiveRecord::Migration +class AddSendUserConfirmationEmailToApplicationSettings < ActiveRecord::Migration def up add_column :application_settings, :send_user_confirmation_email, :boolean, default: false From d71edf0dedb944be62390c0e6eea5f6e6da7a328 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 12 May 2016 17:32:09 -0600 Subject: [PATCH 287/449] Disallow search engines from indexing uploads from a GitLab project. This can sometimes include sensitive information from private projects and confidential issues. It shouldn't be indexed. Resolves #15551. --- public/robots.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/public/robots.txt b/public/robots.txt index 4f616c7f4c..334f4c0353 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -65,3 +65,4 @@ Disallow: /*/*/deploy_keys Disallow: /*/*/hooks Disallow: /*/*/services Disallow: /*/*/protected_branches +Disallow: /*/*/uploads/ From e3e392965ee16f79a80b0f8517ff8c9e445bb907 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 16:39:23 -0500 Subject: [PATCH 288/449] Update permission doc --- doc/permissions/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 30f6c75e1c..b76ce31cba 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -39,7 +39,7 @@ documentation](../workflow/add-user/add-user.md). | Cancel and retry builds | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ | | Update a container registry | | | ✓ | ✓ | ✓ | -| Remove a container registry images | | | ✓ | ✓ | ✓ | +| Remove a container registry image | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | From bf4dc75801176c95b49763ff6ab5e03305c91f73 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 16:34:42 -0500 Subject: [PATCH 289/449] Improve the pipelines design --- app/assets/stylesheets/pages/pipelines.scss | 5 +++++ app/controllers/projects/pipelines_controller.rb | 2 +- app/models/commit_status.rb | 1 + app/views/projects/ci/commits/_commit.html.haml | 6 +++--- app/views/projects/commit/_ci_stage.html.haml | 8 +++----- app/views/projects/commit/_commit_box.html.haml | 4 ++-- app/views/projects/pipelines/index.html.haml | 8 ++++---- app/views/projects/pipelines/new.html.haml | 4 ++-- 8 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 app/assets/stylesheets/pages/pipelines.scss diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss new file mode 100644 index 0000000000..7d90a4bebc --- /dev/null +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -0,0 +1,5 @@ +.pipeline-stage { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 78b85ea9a7..19071cc9e4 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -25,7 +25,7 @@ class Projects::PipelinesController < Projects::ApplicationController flash[:alert] = e.message render 'new' rescue - flash[:alert] = 'Undefined error' + flash[:alert] = 'The pipeline could not be created. Please try again.' render 'new' end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 6fef1c038e..1548a51e94 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -14,6 +14,7 @@ class CommitStatus < ActiveRecord::Base alias_attribute :author, :user scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } + scope :retried, -> { where.not(id: latest) } scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index 90ac41666d..13162b41f9 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -19,7 +19,7 @@ - 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 + %span.label.label-danger.has-tooltip{ title: "#{commit.yaml_errors}" } yaml invalid - if commit.builds.any?(&:stuck?) %span.label.label-warning stuck @@ -36,7 +36,7 @@ %td - if status = stages_status[stage] - tooltip = "#{stage.titleize}: #{status}" - %span.has-tooltip(title="#{tooltip}"){class: "ci-status-icon-#{status}"} + %span.has-tooltip{ title: "#{tooltip}", class: "ci-status-icon-#{status}" } = ci_icon_for_status(status) %td @@ -74,4 +74,4 @@   - 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", class: "cred") + = icon("remove") diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml index bdadf2944c..aaa318e1eb 100644 --- a/app/views/projects/commit/_ci_stage.html.haml +++ b/app/views/projects/commit/_ci_stage.html.haml @@ -1,16 +1,14 @@ -- latest = statuses.latest -- retried = statuses.where.not(id: latest) %tr %th{colspan: 10} %strong - - status = latest.status + - status = statuses.latest.status %span{class: "ci-status-link ci-status-icon-#{status}"} = ci_icon_for_status(status) - if stage   = stage.titleize.pluralize - = render latest.ordered, coverage: @project.build_coverage_enabled?, tage: false, ref: false, allow_retry: true - = render retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true + = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true + = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true %tr %td{colspan: 10}   diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 77fb0bc735..30ec47cc8e 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -27,14 +27,14 @@ .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: + 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) - build: + Build: = ci_label_for_status(@commit.status) %span.light Authored by diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index af55ef42a6..9d5b6d367c 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -27,7 +27,7 @@ - if can? current_user, :create_pipeline, @project = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do = icon('plus') - New + New pipeline - unless @repository.gitlab_ci_yml = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' @@ -36,7 +36,7 @@ = icon('wrench') %span CI Lint -.gray-content-block +.row-content-block - if @scope == 'running' Running pipelines for this project - elsif @scope.nil? @@ -57,8 +57,8 @@ %th Commit - stages.each do |stage| %th - %span.has-tooltip(title="#{stage.titleize}") - = truncate(stage.titleize.pluralize, length: 8) + %span.pipeline-stage.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize.pluralize %th %th = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 39b1571b9c..534a495dd8 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -9,7 +9,7 @@ New Pipeline %hr -= form_tag namespace_project_pipelines_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-new-pipeline-form js-requires-input" do .form-group = label_tag :ref, 'Create for', class: 'control-label' .col-sm-10 @@ -22,4 +22,4 @@ :javascript var availableRefs = #{@project.repository.ref_names.to_json}; - new NewBranchForm($('.js-create-branch-form'), availableRefs) + new NewBranchForm($('.js-new-pipeline-form'), availableRefs) From 7657d202160e04e6b5e5412d949977bc48cdacea Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 17:09:04 -0500 Subject: [PATCH 290/449] Improve design of commit_box with status of builds and pipelines --- app/views/projects/commit/_builds.html.haml | 2 +- .../projects/commit/_ci_commit.html.haml | 29 +++++++-------- .../projects/commit/_commit_box.html.haml | 35 +++++++++++-------- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 5c9a319ede..7f7a15aa21 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,2 +1,2 @@ - @ci_commits.each do |ci_commit| - = render "ci_commit", ci_commit: ci_commit + = render "ci_commit", ci_commit: ci_commit, pipeline_details: true diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 0ff679d0e3..df92a38db3 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -7,20 +7,21 @@ - if ci_commit.builds.running_or_pending.any? = 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 - = 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" - - if ci_commit.duration - in - = time_interval_in_words ci_commit.duration + - if defined?(pipeline_details) && pipeline_details + .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 + = 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" + - if ci_commit.duration + in + = time_interval_in_words ci_commit.duration - if ci_commit.yaml_errors.present? .bs-callout.bs-callout-danger diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 30ec47cc8e..611272b8a7 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -23,20 +23,6 @@ %p .commit-info-row - - 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) - Build: - = ci_label_for_status(@commit.status) - %span.light Authored by %strong = commit_author_link(@commit, avatar: true, size: 24) @@ -60,6 +46,27 @@ %span.commit-info.branches %i.fa.fa-spinner.fa-spin +- if defined?(pipeline) && pipeline + .commit-info-row + Pipeline + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline), class: "ci-status-link ci-status-icon-#{pipeline.status}" do + = ci_icon_for_status(pipeline.status) + = ci_label_for_status(pipeline.status) + - if pipeline.duration + in + = time_interval_in_words 3600 + +- elsif @commit.status + .commit-info-row + Builds for + = pluralize(@commit.ci_commits.count, 'pipeline') + = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do + = ci_icon_for_status(@commit.status) + = ci_label_for_status(@commit.status) + - if @commit.ci_commits.duration + in + = time_interval_in_words @commit.ci_commits.duration + .commit-box.content-block %h3.commit-title = markdown escape_once(@commit.title), pipeline: :single_line From 23d1c454108d50bbb1795b0a5855ee7ae9024e3c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 17:12:45 -0500 Subject: [PATCH 291/449] Fix 401 message --- app/services/auth/container_registry_authentication_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 169e0387e8..3144e96ba3 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -6,7 +6,7 @@ module Auth return error('not found', 404) unless registry.enabled if params[:offline_token] - return error('forbidden', 401) unless current_user + return error('unauthorized', 401) unless current_user else return error('forbidden', 403) unless scope end From 89644edc4e2a9447cc31d4f966c163316dff95fa Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 17:15:38 -0500 Subject: [PATCH 292/449] Bring back path to registry settings --- config/gitlab.yml.example | 1 + config/initializers/1_settings.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index d935121d88..0cef2794f4 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -183,6 +183,7 @@ production: &base # api_url: http://localhost:5000/ # 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 129fd9b79f..796b8178c5 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -254,6 +254,7 @@ Settings.registry['api_url'] ||= "http://localhost:5000/" Settings.registry['key'] ||= nil Settings.registry['issuer'] ||= nil Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':') +Settings.registry['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) # # Git LFS From b933367ab3d8df50f0ebd0690d306536f7a654db Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 17 May 2016 00:10:32 +0200 Subject: [PATCH 293/449] Instrument all Grape API helpers --- CHANGELOG | 1 + config/initializers/metrics.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index e1252d4b94..20e2fcb806 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -57,6 +57,7 @@ v 8.8.0 (unreleased) - Redesign navigation for profile and group pages - Add counter metrics for rails cache - Import pull requests from GitHub where the source or target branches were removed + - All Grape API helpers are now instrumented v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index b2d08d87ba..2338916e9d 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -118,6 +118,8 @@ if Gitlab::Metrics.enabled? # Instrument the classes used for checking if somebody has push access. config.instrument_instance_methods(Gitlab::GitAccess) config.instrument_instance_methods(Gitlab::GitAccessWiki) + + config.instrument_instance_methods(API::Helpers) end GC::Profiler.enable From 143cd58c398b693db1b9d02f7267db39a8acb87c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 17:17:57 -0500 Subject: [PATCH 294/449] Added backup of container registry --- lib/backup/manager.rb | 2 +- lib/backup/registry.rb | 13 +++++++++++++ lib/tasks/gitlab/backup.rake | 21 +++++++++++++++++++++ spec/tasks/gitlab/backup_rake_spec.rb | 14 +++++++++----- 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 lib/backup/registry.rb diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 4962f5e53c..7d0608f09d 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -157,7 +157,7 @@ module Backup end def archives_to_backup - %w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact + %w{uploads builds artifacts lfs registry}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact end def folders_to_backup diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb new file mode 100644 index 0000000000..67fe023108 --- /dev/null +++ b/lib/backup/registry.rb @@ -0,0 +1,13 @@ +require 'backup/files' + +module Backup + class Registry < Files + def initialize + super('registry', Settings.registry.path) + end + + def create_files_dir + Dir.mkdir(app_files_dir, 0700) + end + end +end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 402bb338f2..d97d974ec2 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -14,6 +14,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:builds:create"].invoke Rake::Task["gitlab:backup:artifacts:create"].invoke Rake::Task["gitlab:backup:lfs:create"].invoke + Rake::Task["gitlab:backup:registry:create"].invoke backup = Backup::Manager.new backup.pack @@ -54,6 +55,7 @@ namespace :gitlab do Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds') Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts') Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs') + Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry') Rake::Task['gitlab:shell:setup'].invoke backup.cleanup @@ -173,6 +175,25 @@ namespace :gitlab do end end + namespace :registry do + task create: :environment do + $progress.puts "Dumping container registry images ... ".blue + + if ENV["SKIP"] && ENV["SKIP"].include?("registry") + $progress.puts "[SKIPPED]".cyan + else + Backup::Registry.new.dump + $progress.puts "done".green + end + end + + task restore: :environment do + $progress.puts "Restoring container registry images ... ".blue + Backup::Registry.new.restore + $progress.puts "done".green + end + end + def configure_cron_mode if ENV['CRON'] # We need an object we can say 'puts' and 'print' to; let's use a diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 05fc4c4554..8aeb013eec 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -21,7 +21,7 @@ describe 'gitlab:app namespace rake task' do end def reenable_backup_sub_tasks - %w{db repo uploads builds artifacts lfs}.each do |subtask| + %w{db repo uploads builds artifacts lfs registry}.each do |subtask| Rake::Task["gitlab:backup:#{subtask}:create"].reenable end end @@ -65,6 +65,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke) + expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke) expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke) expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end @@ -122,7 +123,7 @@ describe 'gitlab:app namespace rake task' do it 'should set correct permissions on the tar contents' do tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz} ) expect(exit_status).to eq(0) expect(tar_contents).to match('db/') @@ -131,12 +132,13 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('lfs.tar.gz') - expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/) + expect(tar_contents).to match('registry.tar.gz') + expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/) end it 'should delete temp directories' do temp_dirs = Dir.glob( - File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs}') + File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}') ) expect(temp_dirs).to be_empty @@ -172,7 +174,7 @@ describe 'gitlab:app namespace rake task' do it "does not contain skipped item" do tar_contents, _exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz} ) expect(tar_contents).to match('db/') @@ -180,6 +182,7 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('lfs.tar.gz') + expect(tar_contents).to match('registry.tar.gz') expect(tar_contents).not_to match('repositories/') end @@ -195,6 +198,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke + expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke expect(Rake::Task['gitlab:shell:setup']).to receive :invoke expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end From 7aee713420d5533452613480082660f0647f5598 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 17:29:14 -0500 Subject: [PATCH 295/449] Improve design of the feature --- app/views/projects/container_registry/_tag.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index 10eabc6cd6..4e9f936539 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -4,7 +4,7 @@ = clipboard_button(clipboard_text: "docker pull #{tag.path}") %td - if layer = tag.layers.first - %span.has-tooltip(title="#{layer.revision}") + %span.has-tooltip{ title: "#{layer.revision}" } = layer.short_revision - else \- From 154270a3d4f42f1751502112d0e9cd6b5983032b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 17:37:28 -0500 Subject: [PATCH 296/449] Test container related specs --- spec/models/project_spec.rb | 75 ++++++++++++++++++++++++++++ spec/requests/ci/api/runners_spec.rb | 1 + 2 files changed, 76 insertions(+) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f6e5b13264..6de75af08e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -772,4 +772,79 @@ describe Project, models: true do expect(project.protected_branch?('foo')).to eq(false) end end + + describe '#container_registry_repository' do + let(:project) { create(:empty_project) } + + subject { project.container_registry_repository } + + it { is_expected.to_not be_nil } + end + + describe '#container_registry_repository_url' do + let(:project) { create(:empty_project) } + + subject { project.container_registry_repository_url } + + before { allow(Gitlab.config.registry).to receive_messages(registry_settings) } + + context 'for enabled registry' do + let(:registry_settings) do + { + enabled: true, + host_port: 'example.com', + } + end + + it { is_expected.to_not be_nil } + end + + context 'for disabled registry' do + let(:registry_settings) do + { + enabled: false + } + end + + it { is_expected.to be_nil } + end + end + + describe '#has_container_registry_tags?' do + let(:project) { create(:empty_project) } + + subject { project.has_container_registry_tags? } + + before { allow(Gitlab.config.registry).to receive_messages(registry_settings) } + + context 'for enabled registry' do + let(:registry_settings) do + { + enabled: true + } + end + + context 'with tags' do + before { stub_container_registry('test', 'test2') } + + it { is_expected.to be_truthy } + end + + context 'when no tags' do + before { stub_container_registry } + + it { is_expected.to be_falsey } + end + end + + context 'for disabled registry' do + let(:registry_settings) do + { + enabled: false + } + end + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 43f9fe89c8..db8189ffb7 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -7,6 +7,7 @@ describe Ci::API::API do let(:registration_token) { 'abcdefg123456' } before do + stub_gitlab_calls stub_application_setting(runners_registration_token: registration_token) end From 72a71e9d17c75b11e82623cd9edd22ba70c9ba4f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 17:38:58 -0500 Subject: [PATCH 297/449] Show container registry item only when container registry is enabled --- app/helpers/projects_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 466929443d..b6ba66bf3e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -152,7 +152,7 @@ module ProjectsHelper nav_tabs << :builds end - if can?(current_user, :read_container_image, project) + if project.container_registry_repository_url.present? && can?(current_user, :read_container_image, project) nav_tabs << :container_registry end From 91c4002a0c2f32944ec669cc159c4b1c9176866f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 18:03:55 -0500 Subject: [PATCH 298/449] Improve test coverage --- lib/container_registry/repository.rb | 2 +- spec/features/container_registry_spec.rb | 2 +- spec/models/namespace_spec.rb | 14 +++++++++ spec/models/project_spec.rb | 19 +++++++++--- .../services/projects/destroy_service_spec.rb | 29 +++++++++++++++++++ .../projects/transfer_service_spec.rb | 11 +++++++ spec/support/stub_gitlab_calls.rb | 6 +++- 7 files changed, 76 insertions(+), 7 deletions(-) diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb index b30cb527b6..07cdb78264 100644 --- a/lib/container_registry/repository.rb +++ b/lib/container_registry/repository.rb @@ -39,7 +39,7 @@ module ContainerRegistry def delete_tags return unless tags - tags.each(:delete) + tags.all?(&:delete) end def mount_blob(blob) diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index be5910e4ab..271ef883d1 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -14,7 +14,7 @@ describe "Container Registry" do before do login_as(:user) project.team << [@user, :developer] - stub_container_registry(*tags) + stub_container_registry_tags(*tags) allow(Gitlab.config.registry).to receive_messages(registry_settings) allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 4074f96629..4e68ac5e63 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -70,6 +70,20 @@ describe Namespace, models: true do allow(@namespace).to receive(:path).and_return(new_path) expect(@namespace.move_dir).to be_truthy end + + context "when any project has container tags" do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags('tag') + + create(:empty_project, namespace: @namespace) + + allow(@namespace).to receive(:path_was).and_return(@namespace.path) + allow(@namespace).to receive(:path).and_return('new_path') + end + + it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') } + end end describe :rm_dir do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6de75af08e..262f412222 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -634,11 +634,11 @@ describe Project, models: true do # Project#gitlab_shell returns a new instance of Gitlab::Shell on every # call. This makes testing a bit easier. allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + + allow(project).to receive(:previous_changes).and_return('path' => ['foo']) end it 'renames a repository' do - allow(project).to receive(:previous_changes).and_return('path' => ['foo']) - ns = project.namespace_dir expect(gitlab_shell).to receive(:mv_repository). @@ -663,6 +663,17 @@ describe Project, models: true do project.rename_repo end + + context 'container registry with tags' do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags('tag') + end + + subject { project.rename_repo } + + it { expect{subject}.to raise_error(Exception) } + end end describe '#expire_caches_before_rename' do @@ -825,13 +836,13 @@ describe Project, models: true do end context 'with tags' do - before { stub_container_registry('test', 'test2') } + before { stub_container_registry_tags('test', 'test2') } it { is_expected.to be_truthy } end context 'when no tags' do - before { stub_container_registry } + before { stub_container_registry_tags } it { is_expected.to be_falsey } end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index a5cb6f382e..45b78ccf13 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -28,6 +28,35 @@ describe Projects::DestroyService, services: true do it { expect(Dir.exist?(remove_path)).to be_truthy } end + context 'container registry' do + let(:registry_settings) do + { + enabled: true + } + end + + before do + allow(Gitlab.config.registry).to receive_messages(registry_settings) + stub_container_registry_tags('tag') + end + + context 'tags deletion succeeds' do + it do + expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true) + + destroy_project(project, user, {}) + end + end + + context 'tags deletion fails' do + before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) } + + subject { destroy_project(project, user, {}) } + + it { expect{subject}.to raise_error(Projects::DestroyService::DestroyError) } + end + end + def destroy_project(project, user, params) Projects::DestroyService.new(project, user, params).execute end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 0601731733..d5aa115a07 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -26,6 +26,17 @@ describe Projects::TransferService, services: true do it { expect(project.namespace).to eq(user.namespace) } end + context 'disallow transfering of project with tags' do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags('tag') + end + + subject { transfer_project(project, user, group) } + + it { is_expected.to be_falsey } + end + context 'namespace -> not allowed namespace' do before do @result = transfer_project(project, user, group) diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 2c31cbe3fa..36e234c2e9 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -25,7 +25,11 @@ module StubGitlabCalls allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false) end - def stub_container_registry(*tags) + def stub_container_registry_config(registry_settings) + allow(Gitlab.config.registry).to receive_messages(registry_settings) + end + + def stub_container_registry_tags(*tags) allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_tags).and_return( { "tags" => tags } ) From 8572a6b2d469009f01533b1003e73952bd2b6850 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 18:07:37 -0500 Subject: [PATCH 299/449] Always show documentation how to access images --- .../container_registry/index.html.haml | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index 6a1e46b14b..40957993b2 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -4,26 +4,26 @@ %hr %ul.content-list + .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 image with build and push commands: + %pre + docker build -t #{escape_once(@project.container_registry_repository_url)} . + %br + docker push #{escape_once(@project.container_registry_repository_url)} + - if @tags.blank? %li .nothing-here-block No images in Container Registry for this project. - .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 image with build and push commands: - %pre - docker build -t #{escape_once(@project.container_registry_repository_url)} . - %br - docker push #{escape_once(@project.container_registry_repository_url)} - - else .table-holder %table.table.tags From 04933fd572f60909e8dbd14bd9366e96dc40806e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 18:07:49 -0500 Subject: [PATCH 300/449] Use container registry config stub --- spec/features/container_registry_spec.rb | 7 +------ spec/models/project_spec.rb | 16 +++------------- ...ainer_registry_authentication_service_spec.rb | 9 +-------- spec/services/projects/destroy_service_spec.rb | 8 +------- 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index 271ef883d1..53b4f02711 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -5,17 +5,12 @@ describe "Container Registry" do let(:repository) { project.container_registry_repository } let(:tag_name) { 'latest' } let(:tags) { [tag_name] } - let(:registry_settings) do - { - enabled: true - } - end before do login_as(:user) project.team << [@user, :developer] stub_container_registry_tags(*tags) - allow(Gitlab.config.registry).to receive_messages(registry_settings) + stub_container_registry_config(enabled: true) allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 262f412222..e434d26789 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -797,7 +797,7 @@ describe Project, models: true do subject { project.container_registry_repository_url } - before { allow(Gitlab.config.registry).to receive_messages(registry_settings) } + before { stub_container_registry_config(**registry_settings) } context 'for enabled registry' do let(:registry_settings) do @@ -826,14 +826,8 @@ describe Project, models: true do subject { project.has_container_registry_tags? } - before { allow(Gitlab.config.registry).to receive_messages(registry_settings) } - context 'for enabled registry' do - let(:registry_settings) do - { - enabled: true - } - end + before { stub_container_registry_config(enabled: true) } context 'with tags' do before { stub_container_registry_tags('test', 'test2') } @@ -849,11 +843,7 @@ describe Project, models: true do end context 'for disabled registry' do - let(:registry_settings) do - { - enabled: false - } - end + before { stub_container_registry_config(enabled: false) } it { is_expected.to be_falsey } end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 143d992b6e..d90e2982c4 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -5,19 +5,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do let(:current_user) { nil } let(:current_params) { {} } let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } - let(:registry_settings) do - { - enabled: true, - issuer: 'rspec', - key: nil - } - end let(:payload) { JWT.decode(subject[:token], rsa_key).first } subject { described_class.new(current_project, current_user, current_params).execute } before do - allow(Gitlab.config.registry).to receive_messages(registry_settings) + stub_container_registry_config(enabled: true, issuer: 'rspec', key: nil) allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key) end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 45b78ccf13..29341c5e57 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -29,14 +29,8 @@ describe Projects::DestroyService, services: true do end context 'container registry' do - let(:registry_settings) do - { - enabled: true - } - end - before do - allow(Gitlab.config.registry).to receive_messages(registry_settings) + stub_container_registry_config(enabled: true) stub_container_registry_tags('tag') end From bfc6799c9367a0bb249e1d4eeece5485e16ec6a5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 18:13:44 -0500 Subject: [PATCH 301/449] Fix spinach tests --- features/steps/project/commits/commits.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 93c37bf507..f33f37be95 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -173,7 +173,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I see commit ci info' do - expect(page).to have_content "build: pending" + expect(page).to have_content "Builds for 1 pipeline pending" end step 'I click status link' do @@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I see builds list' do - expect(page).to have_content "build: pending" + expect(page).to have_content "Builds for 1 pipeline pending" expect(page).to have_content "1 build" end From 256e0e8898d0b4a25e7674d2aaa6df06a1a5fe1f Mon Sep 17 00:00:00 2001 From: Carlos Feliciano Date: Mon, 16 May 2016 23:36:42 -0400 Subject: [PATCH 302/449] Remove bottom margin for flash message on home page --- app/assets/stylesheets/pages/projects.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c20f04653f..a3690e40e2 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -7,7 +7,7 @@ } .no-ssh-key-message, .project-limit-message { background-color: #f28d35; - margin-bottom: 16px; + margin-bottom: 0; } .new_project, .edit_project { From 5194a9d9f62d5e9702f3a60e42079ce7bf81ddd4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 23:29:11 -0500 Subject: [PATCH 303/449] Improve the pipeline box --- .../projects/commit/_commit_box.html.haml | 12 +----- app/views/projects/pipelines/_info.html.haml | 37 +++++++++++++++++++ app/views/projects/pipelines/show.html.haml | 2 +- 3 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 app/views/projects/pipelines/_info.html.haml diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 611272b8a7..028564c930 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -46,17 +46,7 @@ %span.commit-info.branches %i.fa.fa-spinner.fa-spin -- if defined?(pipeline) && pipeline - .commit-info-row - Pipeline - = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline), class: "ci-status-link ci-status-icon-#{pipeline.status}" do - = ci_icon_for_status(pipeline.status) - = ci_label_for_status(pipeline.status) - - if pipeline.duration - in - = time_interval_in_words 3600 - -- elsif @commit.status +- if @commit.status .commit-info-row Builds for = pluralize(@commit.ci_commits.count, 'pipeline') diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml new file mode 100644 index 0000000000..8289aefcde --- /dev/null +++ b/app/views/projects/pipelines/_info.html.haml @@ -0,0 +1,37 @@ +%p +.commit-info-row + Pipeline + = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace" + with + = pluralize @pipeline.statuses.count(:id), "build" + - if @pipeline.ref + for + = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" + - if @pipeline.duration + in + = time_interval_in_words @pipeline.duration + + .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) + = ci_label_for_status(@pipeline.status) + +- if @commit + .commit-info-row + %span.light Authored by + %strong + = commit_author_link(@commit, avatar: true, size: 24) + #{time_ago_with_tooltip(@commit.authored_date)} + +.commit-info-row + %span.light Commit + = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace" + = clipboard_button(clipboard_text: @pipeline.sha) + +- if @commit + .commit-box.content-block + %h3.commit-title + = markdown escape_once(@commit.title), pipeline: :single_line + - if @commit.description.present? + %pre.commit-description + = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 8a2e14d8d8..b082d4d5da 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -3,7 +3,7 @@ = render "header_title" .prepend-top-default - if @commit - = render "projects/commit/commit_box", pipeline: @pipeline + = render "projects/pipelines/info" %div.block-connector = render "projects/commit/ci_commit", ci_commit: @pipeline From 0a6c3494b0326de4cdc35d6369f48810d8403f5a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 23:32:48 -0500 Subject: [PATCH 304/449] Fix rubocop offenses --- config/initializers/1_settings.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 796b8178c5..124d63ce3a 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -254,7 +254,7 @@ Settings.registry['api_url'] ||= "http://localhost:5000/" Settings.registry['key'] ||= nil Settings.registry['issuer'] ||= nil Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':') -Settings.registry['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "registry"), Rails.root) +Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root) # # Git LFS From 379dc6fbccc84858a392226111a0abadb54f0c04 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 23:34:42 -0500 Subject: [PATCH 305/449] Wrap text --- app/assets/stylesheets/pages/pipelines.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 7d90a4bebc..546176b65e 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,5 +1,4 @@ .pipeline-stage { - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } From ac6992ba682de08b79e5ddde08dbf566827e2f07 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 16 May 2016 23:40:40 -0500 Subject: [PATCH 306/449] Fix specs --- spec/models/project_spec.rb | 2 ++ .../container_registry_authentication_service_spec.rb | 9 +++++++-- spec/support/stub_gitlab_calls.rb | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e434d26789..60e1ec43f2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -787,6 +787,8 @@ describe Project, models: true do describe '#container_registry_repository' do let(:project) { create(:empty_project) } + before { stub_container_registry_config(enabled: true) } + subject { project.container_registry_repository } it { is_expected.to_not be_nil } diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index d90e2982c4..6c9f56a4fb 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -50,6 +50,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end + shared_examples 'an unauthorized' do + it { is_expected.to include(http_status: 401) } + it { is_expected.to_not include(:token) } + end + shared_examples 'a forbidden' do it { is_expected.to include(http_status: 403) } it { is_expected.to_not include(:token) } @@ -116,7 +121,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { offline_token: true } end - it_behaves_like 'a forbidden' + it_behaves_like 'an unauthorized' end context 'allow to pull and push images' do @@ -179,7 +184,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do { offline_token: true } end - it_behaves_like 'a forbidden' + it_behaves_like 'an unauthorized' end context 'for invalid scope' do diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 36e234c2e9..f73416a3d0 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -27,6 +27,7 @@ module StubGitlabCalls def stub_container_registry_config(registry_settings) allow(Gitlab.config.registry).to receive_messages(registry_settings) + allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') end def stub_container_registry_tags(*tags) From 6b834f2cbcfc26fe3123b6682ed7e20618e31d1b Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 8 Mar 2016 18:22:50 +0000 Subject: [PATCH 307/449] Create a todo on failing MR build When a build fails for a commit, create a todo for the author of the merge request that commit is the HEAD of. If the commit isn't the HEAD commit of any MR, don't do anything. If there already is a todo for that user and MR, don't do anything. Current limitations: - This isn't configurable by project. - The author of a merge request might not be the person who pushed the breaking commit. --- app/finders/todos_finder.rb | 2 +- app/helpers/todos_helper.rb | 8 +- app/models/ci/build.rb | 1 + app/models/commit_status.rb | 4 + app/models/todo.rb | 9 ++- .../add_todo_when_build_fails_service.rb | 17 ++++ app/services/merge_requests/base_service.rb | 25 ++++++ .../merge_when_build_succeeds_service.rb | 23 +----- .../merge_requests/refresh_service.rb | 7 ++ app/services/todo_service.rb | 30 +++++++ app/views/dashboard/todos/_todo.html.haml | 12 +-- spec/factories/todos.rb | 4 + .../add_todo_when_build_fails_service_spec.rb | 81 +++++++++++++++++++ .../merge_requests/refresh_service_spec.rb | 28 +++++++ spec/services/todo_service_spec.rb | 19 +++++ 15 files changed, 237 insertions(+), 33 deletions(-) create mode 100644 app/services/merge_requests/add_todo_when_build_fails_service.rb create mode 100644 spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 3ba27c4050..4bd46a7608 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -36,7 +36,7 @@ class TodosFinder private def action_id? - action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i) + action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i) end def action_id diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 2f06668218..81b9b5d705 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -11,6 +11,7 @@ module TodosHelper case todo.action when Todo::ASSIGNED then 'assigned you' when Todo::MENTIONED then 'mentioned you on' + when Todo::BUILD_FAILED then 'The build failed for your' end end @@ -28,8 +29,11 @@ module TodosHelper namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, todo.target, anchor: anchor) else - polymorphic_path([todo.project.namespace.becomes(Namespace), - todo.project, todo.target], anchor: anchor) + path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] + + path.unshift(:builds) if todo.build_failed? + + polymorphic_path(path, anchor: anchor) end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 92327bdb08..50190101f2 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -53,6 +53,7 @@ module Ci new_build.stage_idx = build.stage_idx new_build.trigger_request = build.trigger_request new_build.save + MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) new_build end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index cacbc13b39..8181416f7d 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -45,6 +45,10 @@ class CommitStatus < ActiveRecord::Base after_transition [:pending, :running] => :success do |commit_status| MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status) end + + after_transition any => :failed do |commit_status| + MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.commit.project, nil).execute(commit_status) + end end delegate :sha, :short_sha, to: :commit diff --git a/app/models/todo.rb b/app/models/todo.rb index f8b59fe412..3a09137332 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,6 +1,7 @@ class Todo < ActiveRecord::Base - ASSIGNED = 1 - MENTIONED = 2 + ASSIGNED = 1 + MENTIONED = 2 + BUILD_FAILED = 3 belongs_to :author, class_name: "User" belongs_to :note @@ -28,6 +29,10 @@ class Todo < ActiveRecord::Base state :done end + def build_failed? + action == BUILD_FAILED + end + def body if note.present? note.note diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb new file mode 100644 index 0000000000..566049525c --- /dev/null +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -0,0 +1,17 @@ +module MergeRequests + class AddTodoWhenBuildFailsService < MergeRequests::BaseService + # Adds a todo to the parent merge_request when a CI build fails + def execute(commit_status) + each_merge_request(commit_status) do |merge_request| + todo_service.merge_request_build_failed(merge_request) + end + end + + # Closes any pending build failed todos for the parent MRs when a build is retried + def close(commit_status) + each_merge_request(commit_status) do |merge_request| + todo_service.merge_request_build_retried(merge_request) + end + end + end +end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index e6837a1869..9d7fca6882 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -38,5 +38,30 @@ module MergeRequests def filter_params super(:merge_request) end + + def merge_request_from(commit_status) + branches = commit_status.ref + + # This is for ref-less builds + branches ||= @project.repository.branch_names_contains(commit_status.sha) + + return [] if branches.blank? + + merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a + merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a + + merge_requests.uniq.select(&:source_project) + end + + def each_merge_request(commit_status) + merge_request_from(commit_status).each do |merge_request| + ci_commit = merge_request.ci_commit + + next unless ci_commit + next unless ci_commit.sha == commit_status.sha + + yield merge_request, ci_commit + end + end end end diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index d6af12f973..8fd6a4ea1f 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -20,15 +20,9 @@ module MergeRequests # Triggers the automatic merge of merge_request once the build succeeds def trigger(commit_status) - merge_requests = merge_request_from(commit_status) - - merge_requests.each do |merge_request| + each_merge_request(commit_status) do |merge_request, ci_commit| next unless merge_request.merge_when_build_succeeds? next unless merge_request.mergeable? - - ci_commit = merge_request.ci_commit - next unless ci_commit - next unless ci_commit.sha == commit_status.sha next unless ci_commit.success? MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) @@ -47,20 +41,5 @@ module MergeRequests end end - private - - def merge_request_from(commit_status) - branches = commit_status.ref - - # This is for ref-less builds - branches ||= @project.repository.branch_names_contains(commit_status.sha) - - return [] if branches.blank? - - merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a - merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a - - merge_requests.uniq.select(&:source_project) - end end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 8b3d56c2b4..fe0579744b 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -12,6 +12,7 @@ module MergeRequests close_merge_requests reload_merge_requests reset_merge_when_build_succeeds + mark_pending_todos_done # Leave a system note if a branch was deleted/added if branch_added? || branch_removed? @@ -80,6 +81,12 @@ module MergeRequests merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds) end + def mark_pending_todos_done + merge_requests_for_source_branch.each do |merge_request| + todo_service.merge_request_push(merge_request, @current_user) + end + end + def find_new_commits if branch_added? @commits = [] diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 42c5bca90f..4bf4e14472 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -80,6 +80,30 @@ class TodoService mark_pending_todos_as_done(merge_request, current_user) end + # When a build fails on the HEAD of a merge request we should: + # + # * create a todo for that user to fix it + # + def merge_request_build_failed(merge_request) + create_build_failed_todo(merge_request) + end + + # When a new commit is pushed to a merge request we should: + # + # * mark all pending todos related to the merge request for that user as done + # + def merge_request_push(merge_request, current_user) + mark_pending_todos_as_done(merge_request, current_user) + end + + # When a build is retried to a merge request we should: + # + # * mark all pending todos related to the merge request for the author as done + # + def merge_request_build_retried(merge_request) + mark_pending_todos_as_done(merge_request, merge_request.author) + end + # When create a note we should: # # * mark all pending todos related to the noteable for the note author as done @@ -145,6 +169,12 @@ class TodoService create_todos(mentioned_users, attributes) end + def create_build_failed_todo(merge_request) + author = merge_request.author + attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED) + create_todos(author, attributes) + end + def attributes_for_target(target) attributes = { project_id: target.project.id, diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index aa0aff86d4..539f1dc603 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,13 +1,13 @@ %li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } .todo-item.todo-block = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' - .todo-title.title - %span.author-name - - if todo.author - = link_to_author(todo) - - else - (removed) + - unless todo.build_failed? + %span.author-name + - if todo.author + = link_to_author(todo) + - else + (removed) %span.todo-label = todo_action_name(todo) - if todo.target diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index e3681ae93a..f426e27afe 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -18,5 +18,9 @@ FactoryGirl.define do commit_id RepoHelpers.sample_commit.id target_type "Commit" end + + trait :build_failed do + action { Todo::BUILD_FAILED } + end end end diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb new file mode 100644 index 0000000000..f70716c9d1 --- /dev/null +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +# Write specs in this file. +describe MergeRequests::AddTodoWhenBuildFailsService do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:project) { create(:project) } + let(:sha) { '1234567890abcdef1234567890abcdef12345678' } + let(:ci_commit) { create(:ci_commit_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) } + let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') } + let(:todo_service) { TodoService.new } + + let(:merge_request) do + create(:merge_request, merge_user: user, source_branch: 'master', + target_branch: 'feature', source_project: project, target_project: project, + state: 'opened') + end + + before do + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow(service).to receive(:todo_service).and_return(todo_service) + end + + describe '#execute' do + context 'commit status with ref' do + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, commit: ci_commit) } + + it 'notifies the todo service' do + expect(todo_service).to receive(:merge_request_build_failed).with(merge_request) + service.execute(commit_status) + end + end + + context 'commit status with non-HEAD ref' do + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) } + + it 'does not notify the todo service' do + expect(todo_service).not_to receive(:merge_request_build_failed) + service.execute(commit_status) + end + end + + context 'commit status without ref' do + let(:commit_status) { create(:generic_commit_status) } + + it 'does not notify the todo service' do + expect(todo_service).not_to receive(:merge_request_build_failed) + service.execute(commit_status) + end + end + end + + describe '#close' do + context 'commit status with ref' do + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, commit: ci_commit) } + + it 'notifies the todo service' do + expect(todo_service).to receive(:merge_request_build_retried).with(merge_request) + service.close(commit_status) + end + end + + context 'commit status with non-HEAD ref' do + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) } + + it 'does not notify the todo service' do + expect(todo_service).not_to receive(:merge_request_build_retried) + service.close(commit_status) + end + end + + context 'commit status without ref' do + let(:commit_status) { create(:generic_commit_status) } + + it 'does not notify the todo service' do + expect(todo_service).not_to receive(:merge_request_build_retried) + service.close(commit_status) + end + end + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index fea8182bd3..31b93850c7 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -27,6 +27,20 @@ describe MergeRequests::RefreshService, services: true do target_branch: 'feature', target_project: @project) + @build_failed_todo = create(:todo, + :build_failed, + user: @user, + project: @project, + target: @merge_request, + author: @user) + + @fork_build_failed_todo = create(:todo, + :build_failed, + user: @user, + project: @project, + target: @merge_request, + author: @user) + @commits = @merge_request.commits @oldrev = @commits.last.id @@ -51,6 +65,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request.merge_when_build_succeeds).to be_falsey} it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@build_failed_todo).to be_done } + it { expect(@fork_build_failed_todo).to be_done } end context 'push to origin repo target branch' do @@ -63,6 +79,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_merged } it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'manual merge of source branch' do @@ -82,6 +100,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request.diffs.size).to be > 0 } it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'push to fork repo source branch' do @@ -101,6 +121,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_open } it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') } it { expect(@fork_merge_request).to be_open } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'push to fork repo target branch' do @@ -113,6 +135,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } it { expect(@fork_merge_request).to be_open } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'push to origin repo target branch after fork project was removed' do @@ -126,6 +150,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_merged } it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'push new branch that exists in a merge request' do @@ -153,6 +179,8 @@ describe MergeRequests::RefreshService, services: true do def reload_mrs @merge_request.reload @fork_merge_request.reload + @build_failed_todo.reload + @fork_build_failed_todo.reload end end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index a075496ee6..4214773653 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -305,6 +305,25 @@ describe TodoService, services: true do expect(second_todo.reload).to be_done end end + + describe '#merge_request_build_failed' do + it 'creates a pending todo for the merge request author' do + service.merge_request_build_failed(mr_unassigned) + + should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED) + end + end + + describe '#merge_request_push' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :build_failed, user: author, project: project, target: mr_assigned, author: john_doe) + second_todo = create(:todo, :build_failed, user: john_doe, project: project, target: mr_assigned, author: john_doe) + service.merge_request_push(mr_assigned, author) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).not_to be_done + end + end end def should_create_todo(attributes = {}) From a9977f2b7a39d57d0633714616b4653aca103993 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 12 May 2016 16:06:14 +0100 Subject: [PATCH 308/449] Syntax-highlight diffs in push emails Based on: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/151 --- .../mailers/repository_push_email.scss | 43 ++++++++++++++ app/helpers/emails_helper.rb | 6 -- app/mailers/emails/projects.rb | 3 +- app/mailers/notify.rb | 2 + app/views/layouts/notify.html.haml | 1 + .../notify/repository_push_email.html.haml | 59 ++++++++++++------- .../notify/repository_push_email.text.haml | 38 ++++++------ app/views/projects/diffs/_file.html.haml | 2 +- app/workers/emails_on_push_worker.rb | 18 +++--- config/application.rb | 1 + lib/gitlab/email/message/repository_push.rb | 7 ++- .../email/message/repository_push_spec.rb | 2 +- spec/mailers/notify_spec.rb | 16 ++--- 13 files changed, 135 insertions(+), 63 deletions(-) create mode 100644 app/assets/stylesheets/mailers/repository_push_email.scss diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss new file mode 100644 index 0000000000..001994db97 --- /dev/null +++ b/app/assets/stylesheets/mailers/repository_push_email.scss @@ -0,0 +1,43 @@ +@import "framework/variables"; + +table.code { + width: 100%; + font-family: monospace; + border: none; + border-collapse: separate; + margin: 0; + padding: 0; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + + td { + line-height: $code_line_height; + font-family: monospace; + font-size: $code_font_size; + } + + td.diff-line-num { + margin: 0; + padding: 0; + border: none; + background: $background-color; + color: rgba(0, 0, 0, 0.3); + padding: 0 5px; + border-right: 1px solid $border-color; + text-align: right; + min-width: 35px; + max-width: 50px; + width: 35px; + } + + td.line_content { + display: block; + margin: 0; + padding: 0 0.5em; + border: none; + white-space: pre; + } +} + +@import "highlight/white"; diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 41b5bd7be9..8466d0aa0b 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -32,12 +32,6 @@ module EmailsHelper nil end - def color_email_diff(diffcontent) - formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github') - lexer = Rouge::Lexers::Diff - raw formatter.format(lexer.lex(diffcontent)) - end - def password_reset_token_valid_time valid_hours = Devise.reset_password_within / 60 / 60 if valid_hours >= 24 diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 5489283432..fdf1e9f5af 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -65,7 +65,8 @@ module Emails # used in notify layout @target_url = @message.target_url - @project = Project.find project_id + @project = Project.find(project_id) + @diff_notes_disabled = true add_project_headers headers['X-GitLab-Author'] = @message.author_username diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 826e5f96fa..1c663bdd52 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -10,6 +10,8 @@ class Notify < BaseMailer include Emails::Builds add_template_helper MergeRequestsHelper + add_template_helper DiffHelper + add_template_helper BlobHelper add_template_helper EmailsHelper def test_email(recipient_email, subject, body) diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 2997f59d94..dde2e2889d 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -4,6 +4,7 @@ %title GitLab = stylesheet_link_tag 'notify' + = yield :head %body %div.content = yield diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index f2e405b14f..f1532371b2 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,3 +1,6 @@ += content_for :head do + = stylesheet_link_tag 'mailers/repository_push_email' + %h3 #{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))} @@ -43,26 +46,38 @@ = diff.new_path - unless @message.disable_diffs? - %h4 Changes: - - @message.diffs.each_with_index do |diff, i| - %li{id: "diff-#{i}"} - %a{href: @message.target_url + "#diff-#{i}"} - - if diff.deleted_file - %strong - = diff.old_path - deleted - - elsif diff.renamed_file - %strong - = diff.old_path - → - %strong - = diff.new_path - - else - %strong - = diff.new_path - %hr - = color_email_diff(diff.diff) - %br + - diff_files = @message.diffs - - if @message.compare_timeout - %h5 Huge diff. To prevent performance issues changes are hidden + - if @message.compare_timeout + %h5 The diff was not included because it is too large. + - else + %h4 Changes: + - diff_files.each_with_index do |diff_file, i| + %li{id: "diff-#{i}"} + %a{href: @message.target_url + "#diff-#{i}"}< + - if diff_file.deleted_file + %strong< + = diff_file.old_path + deleted + - elsif diff_file.renamed_file + %strong< + = diff_file.old_path + → + %strong< + = diff_file.new_path + - else + %strong< + = diff_file.new_path + - if diff_file.too_large? + The diff for this file was not included because it is too large. + - else + %hr + - diff_commit = diff_file.deleted_file ? @message.diff_refs.first : @message.diff_refs.last + - blob = @message.project.repository.blob_for_diff(diff_commit, diff_file) + - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) + %table.code.white + - diff_file.highlighted_diff_lines.each do |line| + = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: nil, plain: true} + - else + No preview for this file type + %br diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 53869e36b2..5ac23aa399 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -25,24 +25,28 @@ - else \- #{diff.new_path} - unless @message.disable_diffs? - \ - \ - Changes: - - @message.diffs.each do |diff| + - if @message.compare_timeout \ - \===================================== - - if diff.deleted_file - #{diff.old_path} deleted - - elsif diff.renamed_file - #{diff.old_path} → #{diff.new_path} - - else - = diff.new_path - \===================================== - != diff.diff - - if @message.compare_timeout - \ - \ - Huge diff. To prevent performance issues it was hidden + \ + The diff was not included because it is too large. + - else + \ + \ + Changes: + - @message.diffs.each do |diff_file| + \ + \===================================== + - if diff_file.deleted_file + #{diff_file.old_path} deleted + - elsif diff_file.renamed_file + #{diff_file.old_path} → #{diff_file.new_path} + - else + = diff_file.new_path + \===================================== + - if diff_file.too_large? + The diff for this file was not included because it is too large. + - else + != diff_file.diff.diff - if @message.target_url \ \ diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 0f04fc5d33..f10b5094ea 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -41,7 +41,7 @@ .diff-content.diff-wrap-lines - # Skip all non non-supported blobs - - return unless blob.respond_to?('text?') + - return unless blob.respond_to?(:text?) - if diff_file.too_large? .nothing-here-block This diff could not be displayed because it is too large. - elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob) diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 6ebcba5f39..fa959fc56e 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -27,15 +27,18 @@ class EmailsOnPushWorker :push end + diff_refs = nil compare = nil reverse_compare = false if action == :push compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) + diff_refs = [project.merge_base_commit(before_sha, after_sha), project.commit(after_sha)] return false if compare.same if compare.commits.empty? compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) + diff_refs = [project.merge_base_commit(after_sha, before_sha), project.commit(before_sha)] reverse_compare = true @@ -48,13 +51,14 @@ class EmailsOnPushWorker send_email( recipient, project_id, - author_id: author_id, - ref: ref, - action: action, - compare: compare, - reverse_compare: reverse_compare, - send_from_committer_email: send_from_committer_email, - disable_diffs: disable_diffs + author_id: author_id, + ref: ref, + action: action, + compare: compare, + reverse_compare: reverse_compare, + diff_refs: diff_refs, + send_from_committer_email: send_from_committer_email, + disable_diffs: disable_diffs ) # These are input errors and won't be corrected even if Sidekiq retries diff --git a/config/application.rb b/config/application.rb index cba80f38f1..a96765a3c9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -78,6 +78,7 @@ module Gitlab config.assets.precompile << "*.png" config.assets.precompile << "print.css" config.assets.precompile << "notify.css" + config.assets.precompile << "mailers/repository_push_email.css" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 2c91a0487c..e2fee6b9f3 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -5,6 +5,7 @@ module Gitlab attr_reader :author_id, :ref, :action include Gitlab::Routing.url_helpers + include DiffHelper delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :name, to: :author, prefix: :author @@ -36,7 +37,7 @@ module Gitlab end def diffs - @diffs ||= (compare.diffs if compare) + @diffs ||= (safe_diff_files(compare.diffs, diff_refs) if compare) end def diffs_count @@ -47,6 +48,10 @@ module Gitlab @opts[:compare] end + def diff_refs + @opts[:diff_refs] + end + def compare_timeout diffs.overflow? if diffs end diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb index 7d6cce6dae..c19f33e222 100644 --- a/spec/lib/gitlab/email/message/repository_push_spec.rb +++ b/spec/lib/gitlab/email/message/repository_push_spec.rb @@ -57,7 +57,7 @@ describe Gitlab::Email::Message::RepositoryPush do describe '#diffs' do subject { message.diffs } - it { is_expected.to all(be_an_instance_of Gitlab::Git::Diff) } + it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) } end describe '#diffs_count' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 5f7e4a526e..b963a3e032 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -693,8 +693,9 @@ describe Notify do let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) } let(:send_from_committer_email) { false } + let(:diff_refs) { [project.merge_base_commit(sample_image_commit.id, sample_commit.id), project.commit(sample_commit.id)] } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -715,15 +716,15 @@ describe Notify do is_expected.to have_body_text /Change some files/ end - it 'includes diffs' do - is_expected.to have_body_text /def archive_formats_regex/ + it 'includes diffs with character-level highlighting' do + is_expected.to have_body_text /def<\/span> archive_formats_regex/ end it 'contains a link to the diff' do is_expected.to have_body_text /#{diff_path}/ end - it 'doesn not contain the misleading footer' do + it 'does not contain the misleading footer' do is_expected.not_to have_body_text /you are a member of/ end @@ -797,8 +798,9 @@ describe Notify do let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } + let(:diff_refs) { [project.merge_base_commit(sample_commit.parent_id, sample_commit.id), project.commit(sample_commit.id)] } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) } it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like "a user cannot unsubscribe through footer link" @@ -819,8 +821,8 @@ describe Notify do is_expected.to have_body_text /Change some files/ end - it 'includes diffs' do - is_expected.to have_body_text /def archive_formats_regex/ + it 'includes diffs with character-level highlighting' do + is_expected.to have_body_text /def<\/span> archive_formats_regex/ end it 'contains a link to the diff' do From 4e9ce5abe2c18089f02ab9f3a3e12c4ccae16af6 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 17 May 2016 15:19:59 +0200 Subject: [PATCH 309/449] fix typo from bad refactoring --- lib/gitlab/gitlab_import/importer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index 96717b42ba..3e51c06877 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -5,7 +5,7 @@ module Gitlab def initialize(project) @project = project - credentials = import_data + credentials = project.import_data if credentials && credentials[:password] @client = Client.new(credentials[:password]) @formatter = Gitlab::ImportFormatter.new From ab2a1d23459294d3ce61e8558538f38cb5f29bb9 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 17 May 2016 15:29:53 +0200 Subject: [PATCH 310/449] added CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 20e2fcb806..bafdc8dbc8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ v 8.8.0 (unreleased) v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) + - Fix import from gitlab.com to a private instance failure v 8.7.5 - Fix relative links in wiki pages. !4050 From a82109eee80bf703ad8e82de2410f490e5fc6d54 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 17 May 2016 09:41:47 -0500 Subject: [PATCH 311/449] Add .gitkeep --- lib/container_registry/config.rb | 1 + lib/container_registry/registry.rb | 10 +++++-- lib/container_registry/repository.rb | 1 + lib/container_registry/tag.rb | 10 +++++++ shared/registry/.gitkeep | 0 spec/lib/container_registry/registry_spec.rb | 28 ++++++++++++++++++++ 6 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 shared/registry/.gitkeep create mode 100644 spec/lib/container_registry/registry_spec.rb diff --git a/lib/container_registry/config.rb b/lib/container_registry/config.rb index 626b36cbaa..589f9f4380 100644 --- a/lib/container_registry/config.rb +++ b/lib/container_registry/config.rb @@ -9,6 +9,7 @@ module ContainerRegistry def [](key) return unless data + data[key] end end diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb index d3b117eeac..07490de94b 100644 --- a/lib/container_registry/registry.rb +++ b/lib/container_registry/registry.rb @@ -3,13 +3,19 @@ module ContainerRegistry attr_reader :uri, :client, :path def initialize(uri, options = {}) - @path = options[:path] || uri - @uri = URI.parse(uri) + @uri = uri + @path = options[:path] || default_path @client = ContainerRegistry::Client.new(uri, options) end def [](name) ContainerRegistry::Repository.new(self, name) end + + private + + def default_path + @uri.sub(/^https?:\/\//, '') + end end end diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb index 07cdb78264..7782505613 100644 --- a/lib/container_registry/repository.rb +++ b/lib/container_registry/repository.rb @@ -20,6 +20,7 @@ module ContainerRegistry def manifest return @manifest if defined?(@manifest) + @manifest = client.repository_tags(name) end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 14cee8be88..f06806db6a 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -12,6 +12,7 @@ module ContainerRegistry def manifest return @manifest if defined?(@manifest) + @manifest = client.repository_manifest(repository.name, name) end @@ -21,33 +22,39 @@ module ContainerRegistry 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_blob return @config_blob if defined?(@config_blob) return unless manifest && manifest['config'] + @config_blob = ContainerRegistry::Blob.new(repository, manifest['config']) end def config return unless config_blob + @config ||= ContainerRegistry::Config.new(self, config_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| ContainerRegistry::Blob.new(repository, layer) end @@ -55,16 +62,19 @@ module ContainerRegistry 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 copy_to(repository) return unless manifest + layers.each do |blob| repository.mount_blob(blob) end diff --git a/shared/registry/.gitkeep b/shared/registry/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb new file mode 100644 index 0000000000..f5f7c3c8bf --- /dev/null +++ b/spec/lib/container_registry/registry_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe ContainerRegistry::Registry do + let(:path) { nil } + let(:registry) { described_class.new('http://example.com', path: path) } + + subject { registry } + + it { is_expected.to respond_to(:client) } + it { is_expected.to respond_to(:uri) } + it { is_expected.to respond_to(:path) } + + it { expect(subject['test']).to_not be_nil } + + context '#path' do + subject { registry.path } + + context 'path from URL' do + it { is_expected.to eq('example.com') } + end + + context 'custom path' do + let(:path) { 'registry.example.com' } + + it { is_expected.to eq(path) } + end + end +end From 24145592e804ffbe58a38b15095808919337d545 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 17 May 2016 13:20:11 -0500 Subject: [PATCH 312/449] Provide full test coverage to lib/container_registry API implementation --- .../projects/container_registry_controller.rb | 2 +- app/models/project.rb | 2 +- lib/container_registry/blob.rb | 10 +-- lib/container_registry/client.rb | 8 -- lib/container_registry/registry.rb | 2 +- lib/container_registry/repository.rb | 28 ++---- lib/container_registry/tag.rb | 20 +---- spec/lib/container_registry/blob_spec.rb | 61 +++++++++++++ spec/lib/container_registry/registry_spec.rb | 2 +- .../lib/container_registry/repository_spec.rb | 65 ++++++++++++++ spec/lib/container_registry/tag_spec.rb | 89 +++++++++++++++++++ 11 files changed, 232 insertions(+), 57 deletions(-) create mode 100644 spec/lib/container_registry/blob_spec.rb create mode 100644 spec/lib/container_registry/repository_spec.rb create mode 100644 spec/lib/container_registry/tag_spec.rb diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index e48f205d21..8ed5a8ff6f 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -22,6 +22,6 @@ class Projects::ContainerRegistryController < Projects::ApplicationController end def tag - @tag ||= container_registry_repository[params[:id]] + @tag ||= container_registry_repository.tag(params[:id]) end end diff --git a/app/models/project.rb b/app/models/project.rb index 8e80a6b5c0..09a214da04 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -336,7 +336,7 @@ class Project < ActiveRecord::Base url = Gitlab.config.registry.api_url host_port = Gitlab.config.registry.host_port registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) - registry[path_with_namespace] + registry.repository(path_with_namespace) end end diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb index d59792a383..4e20dc4f87 100644 --- a/lib/container_registry/blob.rb +++ b/lib/container_registry/blob.rb @@ -2,6 +2,8 @@ module ContainerRegistry class Blob attr_reader :repository, :config + delegate :registry, :client, to: :repository + def initialize(repository, config) @repository = repository @config = config || {} @@ -35,10 +37,6 @@ module ContainerRegistry revision[0..8] end - def client - @client ||= repository.client - end - def delete client.delete_blob(repository.name, digest) end @@ -46,9 +44,5 @@ module ContainerRegistry def 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/container_registry/client.rb b/lib/container_registry/client.rb index c250a4b694..4d726692f4 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -22,14 +22,6 @@ module ContainerRegistry @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}/blobs/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? diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb index 07490de94b..0e634f6b6e 100644 --- a/lib/container_registry/registry.rb +++ b/lib/container_registry/registry.rb @@ -8,7 +8,7 @@ module ContainerRegistry @client = ContainerRegistry::Client.new(uri, options) end - def [](name) + def repository(name) ContainerRegistry::Repository.new(self, name) end diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb index 7782505613..0e4a7cb3cc 100644 --- a/lib/container_registry/repository.rb +++ b/lib/container_registry/repository.rb @@ -2,19 +2,17 @@ module ContainerRegistry class Repository attr_reader :registry, :name + delegate :client, to: :registry + def initialize(registry, name) @registry, @name = registry, name end - def client - @client ||= registry.client - end - def path [registry.path, name].compact.join('/') end - def [](tag) + def tag(tag) ContainerRegistry::Tag.new(self, tag) end @@ -37,26 +35,14 @@ module ContainerRegistry end end + def blob(config) + ContainerRegistry::Blob.new(self, config) + end + def delete_tags return unless tags tags.all?(&: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/container_registry/tag.rb b/lib/container_registry/tag.rb index f06806db6a..43f8d6dc8c 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -2,6 +2,8 @@ module ContainerRegistry class Tag attr_reader :repository, :name + delegate :registry, :client, to: :repository + def initialize(repository, name) @repository, @name = repository, name end @@ -36,7 +38,7 @@ module ContainerRegistry return @config_blob if defined?(@config_blob) return unless manifest && manifest['config'] - @config_blob = ContainerRegistry::Blob.new(repository, manifest['config']) + @config_blob = repository.blob(manifest['config']) end def config @@ -56,7 +58,7 @@ module ContainerRegistry return unless manifest @layers = manifest['layers'].map do |layer| - ContainerRegistry::Blob.new(repository, layer) + repository.blob(layer) end end @@ -71,19 +73,5 @@ module ContainerRegistry 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 end end diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb new file mode 100644 index 0000000000..4d8cb787dd --- /dev/null +++ b/spec/lib/container_registry/blob_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe ContainerRegistry::Blob do + let(:digest) { 'sha256:0123456789012345' } + let(:config) do + { + 'digest' => digest, + 'mediaType' => 'binary', + 'size' => 1000 + } + end + + let(:registry) { ContainerRegistry::Registry.new('http://example.com') } + let(:repository) { registry.repository('group/test') } + let(:blob) { repository.blob(config) } + + it { expect(blob).to respond_to(:repository) } + it { expect(blob).to delegate_method(:registry).to(:repository) } + it { expect(blob).to delegate_method(:client).to(:repository) } + + context '#path' do + subject { blob.path } + + it { is_expected.to eq('example.com/group/test@sha256:0123456789012345') } + end + + context '#digest' do + subject { blob.digest } + + it { is_expected.to eq(digest) } + end + + context '#type' do + subject { blob.type } + + it { is_expected.to eq('binary') } + end + + context '#revision' do + subject { blob.revision } + + it { is_expected.to eq('0123456789012345') } + end + + context '#short_revision' do + subject { blob.short_revision } + + it { is_expected.to eq('012345678') } + end + + context '#delete' do + before do + stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345'). + to_return(status: 200) + end + + subject { blob.delete } + + it { is_expected.to be_truthy } + end +end diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb index f5f7c3c8bf..2638401ae6 100644 --- a/spec/lib/container_registry/registry_spec.rb +++ b/spec/lib/container_registry/registry_spec.rb @@ -10,7 +10,7 @@ describe ContainerRegistry::Registry do it { is_expected.to respond_to(:uri) } it { is_expected.to respond_to(:path) } - it { expect(subject['test']).to_not be_nil } + it { expect(subject.repository('test')).to_not be_nil } context '#path' do subject { registry.path } diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb new file mode 100644 index 0000000000..e6d66b11e4 --- /dev/null +++ b/spec/lib/container_registry/repository_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe ContainerRegistry::Repository do + let(:registry) { ContainerRegistry::Registry.new('http://example.com') } + let(:repository) { registry.repository('group/test') } + + it { expect(repository).to respond_to(:registry) } + it { expect(repository).to delegate_method(:client).to(:registry) } + it { expect(repository.tag('test')).to_not be_nil } + + context '#path' do + subject { repository.path } + + it { is_expected.to eq('example.com/group/test') } + end + + context 'manifest processing' do + before do + stub_request(:get, 'http://example.com/v2/group/test/tags/list'). + with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }). + to_return( + status: 200, + body: JSON.dump(tags: ['test']), + headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' }) + end + + context '#manifest' do + subject { repository.manifest } + + it { is_expected.to_not be_nil } + end + + context '#valid?' do + subject { repository.valid? } + + it { is_expected.to be_truthy } + end + + context '#tags' do + subject { repository.tags } + + it { is_expected.to_not be_empty } + end + end + + context '#delete_tags' do + let(:tag) { ContainerRegistry::Tag.new(repository, 'tag') } + + before { expect(repository).to receive(:tags).twice.and_return([tag]) } + + subject { repository.delete_tags } + + context 'succeeds' do + before { expect(tag).to receive(:delete).and_return(true) } + + it { is_expected.to be_truthy } + end + + context 'any fails' do + before { expect(tag).to receive(:delete).and_return(false) } + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb new file mode 100644 index 0000000000..12cf91127e --- /dev/null +++ b/spec/lib/container_registry/tag_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe ContainerRegistry::Tag do + let(:registry) { ContainerRegistry::Registry.new('http://example.com') } + let(:repository) { registry.repository('group/test') } + let(:tag) { repository.tag('tag') } + let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } } + + it { expect(tag).to respond_to(:repository) } + it { expect(tag).to delegate_method(:registry).to(:repository) } + it { expect(tag).to delegate_method(:client).to(:repository) } + + context '#path' do + subject { tag.path } + + it { is_expected.to eq('example.com/group/test:tag') } + end + + context 'manifest processing' do + before do + stub_request(:get, 'http://example.com/v2/group/test/manifests/tag'). + with(headers: headers). + to_return( + status: 200, + body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'), + headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' }) + end + + context '#layers' do + subject { tag.layers } + + it { expect(subject.length).to eq(1) } + end + + context '#total_size' do + subject { tag.total_size } + + it { is_expected.to eq(2319870) } + end + + context 'config processing' do + before do + stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). + with(headers: { 'Accept' => 'application/octet-stream' }). + to_return( + status: 200, + body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')) + end + + context '#config' do + subject { tag.config } + + it { is_expected.to_not be_nil } + end + + context '#created_at' do + subject { tag.created_at } + + it { is_expected.to_not be_nil } + end + end + end + + context 'manifest digest' do + before do + stub_request(:head, 'http://example.com/v2/group/test/manifests/tag'). + with(headers: headers). + to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' }) + end + + context '#digest' do + subject { tag.digest } + + it { is_expected.to eq('sha256:digest') } + end + + context '#delete' do + before do + stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest'). + with(headers: headers). + to_return(status: 200) + end + + subject { tag.delete } + + it { is_expected.to be_truthy } + end + end +end From 5bf49bb63d88b1cce2d9a44716b54acfa63ea657 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 17 May 2016 21:41:53 -0500 Subject: [PATCH 313/449] Move note helper method to notes entity file --- lib/api/helpers.rb | 4 ---- lib/api/notes.rb | 10 ++++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 1003b596ae..40c967453f 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -397,9 +397,5 @@ module API error!(errors[:access_level], 422) if errors[:access_level].any? not_found!(errors) end - - def noteable_ability_name(noteable) - "read_#{noteable.class.to_s.underscore.downcase}".to_sym - end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index c49b107d1d..d4fcfd3d4d 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -21,7 +21,7 @@ module API get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) - if can?(current_user, noteable_ability_name(@noteable), @noteable) + if can?(current_user, noteable_read_ability_name(@noteable), @noteable) # We exclude notes that are cross-references and that cannot be viewed # by the current user. By doing this exclusion at this level and not # at the DB query level (which we cannot in that case), the current @@ -51,7 +51,7 @@ module API get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) @note = @noteable.notes.find(params[:note_id]) - can_read_note = can?(current_user, noteable_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user) + can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user) if can_read_note present @note, with: Entities::Note @@ -141,5 +141,11 @@ module API end end end + + helpers do + def noteable_read_ability_name(noteable) + "read_#{noteable.class.to_s.underscore.downcase}".to_sym + end + end end end From 07ff874f572a947d7730787492a604dd3f44d496 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 29 Apr 2016 18:06:22 -0300 Subject: [PATCH 314/449] Let users set notification levels in projects which they are not members --- app/controllers/projects_controller.rb | 4 +-- app/models/user.rb | 9 ++++- .../notification_settings_controller_spec.rb | 14 ++++++++ spec/controllers/projects_controller_spec.rb | 35 ++++++++++++++++++- .../merge_requests/update_service_spec.rb | 1 + spec/services/notification_service_spec.rb | 8 ++++- 6 files changed, 65 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index f4ec60ad2c..55632308b4 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -102,9 +102,7 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do if current_user - @membership = @project.team.find_member(current_user.id) - - if @membership + if can?(current_user, :read_project, @project) @notification_setting = current_user.notification_settings_for(@project) end end diff --git a/app/models/user.rb b/app/models/user.rb index 368a3f3cfb..cc312a291c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -777,7 +777,14 @@ class User < ActiveRecord::Base end def notification_settings_for(source) - notification_settings.find_or_initialize_by(source: source) + notification_setting = notification_settings.find_or_initialize_by({ source: source }) + + if source.is_a?(Project) + membership = source.team.find_member(self.id) + notification_setting.level = :disabled unless membership.present? || notification_setting.persisted? + end + + notification_setting end private diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb index 4908b54564..c5d17d97ec 100644 --- a/spec/controllers/projects/notification_settings_controller_spec.rb +++ b/spec/controllers/projects/notification_settings_controller_spec.rb @@ -34,5 +34,19 @@ describe Projects::NotificationSettingsController do expect(response.status).to eq 200 end end + + context 'not authorized' do + let(:private_project) { create(:project, :private) } + before { sign_in(user) } + + it 'returns 404' do + put :update, + namespace_id: private_project.namespace.to_param, + project_id: private_project.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq(404) + end + end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 069cd917e5..685fe9ffe0 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -8,10 +8,43 @@ describe ProjectsController do let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } describe "GET show" do + context "user not project member" do + before { sign_in(user) } + + context "user does not have access to project" do + let(:private_project) { create(:project, :private) } + + it "does not initialize notification setting" do + get :show, namespace_id: private_project.namespace.path, id: private_project.path + expect(assigns(:notification_setting)).to be_nil + end + end + + context "user has access to project" do + context "and does not have notification setting" do + it "initializes notification as disabled" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path + expect(assigns(:notification_setting).level).to eq("disabled") + end + end + + context "and has notification setting" do + before do + setting = user.notification_settings_for(public_project) + setting.level = :global + setting.save + end + + it "shows current notification setting" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path + expect(assigns(:notification_setting).level).to eq("global") + end + end + end + end context "rendering default project view" do render_views - it "renders the activity view" do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:project_view).and_return('activity') diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 213e8c2eb3..e872fc0eed 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -17,6 +17,7 @@ describe MergeRequests::UpdateService, services: true do before do project.team << [user, :master] project.team << [user2, :developer] + project.team << [user3, :developer] end describe 'execute' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 4bbc4ddc3a..d557b2e65b 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -119,7 +119,10 @@ describe NotificationService, services: true do let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") } it 'filters out users that can not read the issue' do + project.team << [admin, :master] + project.team << [author, :developer] project.team << [member, :developer] + project.team << [assignee, :developer] expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times @@ -143,7 +146,8 @@ describe NotificationService, services: true do before do build_team(note.project) - note.project.team << [note.author, :master] + note.project.team << [[note.author, note.noteable.author, note.noteable.assignee], :master] + ActionMailer::Base.deliveries.clear end @@ -260,6 +264,7 @@ describe NotificationService, services: true do before do build_team(issue.project) add_users_with_subscription(issue.project, issue) + project.team << [[issue.assignee, issue.author], :developer] ActionMailer::Base.deliveries.clear end @@ -491,6 +496,7 @@ describe NotificationService, services: true do before do build_team(merge_request.target_project) add_users_with_subscription(merge_request.target_project, merge_request) + project.team << [merge_request.assignee, :developer] ActionMailer::Base.deliveries.clear end From c2c7014e017a36e2819335653f5d3fc04cc2c054 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 6 May 2016 16:40:27 -0300 Subject: [PATCH 315/449] Improve documentation and add changelog --- app/controllers/projects_controller.rb | 6 +---- app/models/user.rb | 7 +++-- spec/services/notification_service_spec.rb | 30 +++++++++++++++++++++- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 55632308b4..6d2b6b60f1 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -101,11 +101,7 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do - if current_user - if can?(current_user, :read_project, @project) - @notification_setting = current_user.notification_settings_for(@project) - end - end + @notification_setting = current_user.notification_settings_for(@project) if current_user if @project.repository_exists? if @project.empty_repo? diff --git a/app/models/user.rb b/app/models/user.rb index cc312a291c..09fb6c978f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -777,11 +777,10 @@ class User < ActiveRecord::Base end def notification_settings_for(source) - notification_setting = notification_settings.find_or_initialize_by({ source: source }) + notification_setting = notification_settings.find_or_initialize_by(source: source) - if source.is_a?(Project) - membership = source.team.find_member(self.id) - notification_setting.level = :disabled unless membership.present? || notification_setting.persisted? + if source.is_a?(Project) && !source.team.member?(id) && !notification_setting.persisted? + notification_setting.level = :disabled end notification_setting diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index d557b2e65b..6a84dcfa01 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -106,6 +106,35 @@ describe NotificationService, services: true do should_not_email(@u_disabled) end end + + context 'when user is not project member' do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project, assignee: create(:user)) } + let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything') } + + before { ActionMailer::Base.deliveries.clear } + + context "and has notification setting" do + before do + setting = user.notification_settings_for(project) + setting.level = :watch + setting.save + end + + it "sends user email" do + notification.new_note(note) + should_email(user) + end + end + + context "and does note have notification setting" do + it "does not send email" do + notification.new_note(note) + should_not_email(user) + end + end + end end context 'confidential issue note' do @@ -147,7 +176,6 @@ describe NotificationService, services: true do before do build_team(note.project) note.project.team << [[note.author, note.noteable.author, note.noteable.assignee], :master] - ActionMailer::Base.deliveries.clear end From fc9844e8e1955359eae5b159069a90a993c667d1 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 16 May 2016 13:34:10 -0300 Subject: [PATCH 316/449] Update documentation --- CHANGELOG | 1 + app/models/user.rb | 2 +- doc/workflow/notifications.md | 2 +- spec/services/notification_service_spec.rb | 67 +++++++++++----------- 4 files changed, 37 insertions(+), 35 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bafdc8dbc8..fc7d1a710a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,6 +36,7 @@ v 8.8.0 (unreleased) - Added button to toggle whitespaces changes on diff view - Backport GitHub Enterprise import support from EE - Create tags using Rugged for performance reasons. !3745 + - Allow guests to set notification level in projects - API: Expose Issue#user_notes_count. !3126 (Anton Popov) - Don't show forks button when user can't view forks - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 diff --git a/app/models/user.rb b/app/models/user.rb index 09fb6c978f..831fbcbb3f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -777,7 +777,7 @@ class User < ActiveRecord::Base end def notification_settings_for(source) - notification_setting = notification_settings.find_or_initialize_by(source: source) + notification_setting = notification_settings.find_or_initialize_by(source: source) if source.is_a?(Project) && !source.team.member?(id) && !notification_setting.persisted? notification_setting.level = :disabled diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 80817c98d2..cbca94c0b5 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -69,7 +69,7 @@ In all of the below cases, the notification will be sent to: ...with notification level "Participating" or higher -- Watchers: project members with notification level "Watch" +- Watchers: users with notification level "Watch" - Subscribers: anyone who manually subscribed to the issue/merge request | Event | Sent to | diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 6a84dcfa01..4bf8fcde4d 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -35,7 +35,7 @@ describe NotificationService, services: true do describe 'Notes' do context 'issue note' do - let(:project) { create(:empty_project, :private) } + let(:project) { create(:empty_project, :internal) } let(:issue) { create(:issue, project: project, assignee: create(:user)) } let(:mentioned_issue) { create(:issue, assignee: issue.assignee) } let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @outsider also') } @@ -52,8 +52,8 @@ describe NotificationService, services: true do it do add_users_with_subscription(note.project, issue) - # Ensure create SentNotification by noteable = issue 6 times, not noteable = note - expect(SentNotification).to receive(:record).with(issue, any_args).exactly(7).times + # Ensure create SentNotification by noteable = issue 7 times, not noteable = note + expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times ActionMailer::Base.deliveries.clear @@ -66,6 +66,7 @@ describe NotificationService, services: true do should_email(@subscriber) should_email(@watcher_and_subscriber) should_email(@subscribed_participant) + should_email(@u_guest_watcher) should_not_email(note.author) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -100,41 +101,13 @@ describe NotificationService, services: true do should_email(note.noteable.author) should_email(note.noteable.assignee) should_email(@u_mentioned) + should_email(@u_guest_watcher) should_not_email(@u_watcher) should_not_email(note.author) should_not_email(@u_participating) should_not_email(@u_disabled) end end - - context 'when user is not project member' do - let(:user) { create(:user) } - let(:project) { create(:empty_project, :public) } - let(:issue) { create(:issue, project: project, assignee: create(:user)) } - let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything') } - - before { ActionMailer::Base.deliveries.clear } - - context "and has notification setting" do - before do - setting = user.notification_settings_for(project) - setting.level = :watch - setting.save - end - - it "sends user email" do - notification.new_note(note) - should_email(user) - end - end - - context "and does note have notification setting" do - it "does not send email" do - notification.new_note(note) - should_not_email(user) - end - end - end end context 'confidential issue note' do @@ -192,6 +165,7 @@ describe NotificationService, services: true do should_email(member) end + should_email(@u_guest_watcher) should_email(note.noteable.author) should_email(note.noteable.assignee) should_not_email(note.author) @@ -216,7 +190,7 @@ describe NotificationService, services: true do before do build_team(note.project) - note.project.team << [note.author, :master] + note.project.team << [[note.author, note.noteable.author], :master] ActionMailer::Base.deliveries.clear end @@ -233,6 +207,7 @@ describe NotificationService, services: true do should_email(member) end + should_email(@u_guest_watcher) should_email(note.noteable.author) should_not_email(note.author) should_email(@u_mentioned) @@ -256,6 +231,7 @@ describe NotificationService, services: true do it do notification.new_note(note) + should_email(@u_guest_watcher) should_email(@u_committer) should_email(@u_watcher) should_not_email(@u_mentioned) @@ -268,6 +244,7 @@ describe NotificationService, services: true do note.update_attribute(:note, '@mention referenced') notification.new_note(note) + should_email(@u_guest_watcher) should_email(@u_committer) should_email(@u_watcher) should_email(@u_mentioned) @@ -302,6 +279,7 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_not_email(@u_mentioned) should_not_email(@u_participating) @@ -361,6 +339,7 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(@unsubscriber) @@ -375,6 +354,7 @@ describe NotificationService, services: true do should_email(@u_mentioned) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(@unsubscriber) @@ -389,6 +369,7 @@ describe NotificationService, services: true do expect(issue.assignee).to be @u_mentioned should_email(issue.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(@unsubscriber) @@ -403,6 +384,7 @@ describe NotificationService, services: true do expect(issue.assignee).to be @u_mentioned should_email(issue.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(@unsubscriber) @@ -416,6 +398,7 @@ describe NotificationService, services: true do expect(issue.assignee).to be @u_mentioned should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(issue.assignee) @@ -444,6 +427,7 @@ describe NotificationService, services: true do should_not_email(issue.assignee) should_not_email(issue.author) should_not_email(@u_watcher) + should_not_email(@u_guest_watcher) should_not_email(@u_participant_mentioned) should_not_email(@subscriber) should_not_email(@watcher_and_subscriber) @@ -492,6 +476,7 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(issue.author) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) @@ -508,6 +493,7 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(issue.author) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) @@ -536,6 +522,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@watcher_and_subscriber) should_email(@u_participant_mentioned) + should_email(@u_guest_watcher) should_not_email(@u_participating) should_not_email(@u_disabled) end @@ -559,6 +546,7 @@ describe NotificationService, services: true do should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) + should_email(@u_guest_watcher) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -600,6 +588,7 @@ describe NotificationService, services: true do should_email(merge_request.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) @@ -618,6 +607,7 @@ describe NotificationService, services: true do should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) + should_email(@u_guest_watcher) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -633,6 +623,7 @@ describe NotificationService, services: true do should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) + should_email(@u_guest_watcher) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -654,6 +645,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participating) + should_not_email(@u_guest_watcher) should_not_email(@u_disabled) end end @@ -669,6 +661,8 @@ describe NotificationService, services: true do @u_not_mentioned = create(:user, username: 'regular', notification_level: :participating) @u_outsider_mentioned = create(:user, username: 'outsider') + create_guest_watcher + project.team << [@u_watcher, :master] project.team << [@u_participating, :master] project.team << [@u_participant_mentioned, :master] @@ -678,6 +672,13 @@ describe NotificationService, services: true do project.team << [@u_not_mentioned, :master] end + def create_guest_watcher + @u_guest_watcher = create(:user, username: 'guest_watching') + setting = @u_guest_watcher.notification_settings_for(project) + setting.level = :watch + setting.save + end + def add_users_with_subscription(project, issuable) @subscriber = create :user @unsubscriber = create :user From 4e58aa40b98aa1cb758b1386b961f3bf15671bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 17 May 2016 23:44:54 -0500 Subject: [PATCH 317/449] Remove back-compat code that's not needed anymore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/helpers/projects_helper.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index e1ab78df69..5f18ac7ea0 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -124,11 +124,7 @@ module ProjectsHelper end def license_short_name(project) - no_license_key = project.repository.license_key.nil? || - # Back-compat if cache contains 'no-license', can be removed in a few weeks - project.repository.license_key == 'no-license' - - return 'LICENSE' if no_license_key + return 'LICENSE' if project.repository.license_key.nil? license = Licensee::License.new(project.repository.license_key) From f7bdbc0b55dfed9f1bfabf052436bac2997e78f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 00:07:51 -0500 Subject: [PATCH 318/449] Make it clearer that /licenses is the OS license templates endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- doc/api/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/README.md b/doc/api/README.md index ff039f1886..27c5962dec 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -33,7 +33,7 @@ following locations: - [Build triggers](build_triggers.md) - [Build Variables](build_variables.md) - [Runners](runners.md) -- [Licenses](licenses.md) +- [Open source license templates](licenses.md) ## Authentication From 483c034b86262fb9b2c47df999d995f500b38eb4 Mon Sep 17 00:00:00 2001 From: Alex Moore-Niemi Date: Wed, 20 Apr 2016 18:41:11 -0400 Subject: [PATCH 319/449] Changing the confidentiality of an issue now creates a new system note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CHANGELOG | 1 + app/services/issuable_base_service.rb | 6 ++++++ app/services/issues/update_service.rb | 6 +++++- app/services/system_note_service.rb | 19 +++++++++++++++++++ spec/services/issues/update_service_spec.rb | 20 ++++++++++++++------ spec/services/system_note_service_spec.rb | 12 ++++++++++++ 6 files changed, 57 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 48e3889960..8b1752c30f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ v 8.8.0 (unreleased) - Escape HTML in commit titles in system note messages - Improve multiple branch push performance by memoizing permission checking - Log to application.log when an admin starts and stops impersonating a user + - Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi) - Updated gitlab_git to 10.1.0 - GitAccess#protected_tag? no longer loads all tags just to check if a single one exists - Reduce delay in destroying a project from 1-minute to immediately diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 2b16089df1..1f59281e27 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -36,6 +36,12 @@ class IssuableBaseService < BaseService end end + def create_confidentiality_note(issuable) + SystemNoteService.change_confidentiality( + issuable, issuable.project, current_user + ) + end + def filter_params(issuable_ability_name = :issue) filter_assignee filter_milestone diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 3563cbaa99..3a9bbf8a78 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -10,7 +10,7 @@ module Issues end if issue.previous_changes.include?('title') || - issue.previous_changes.include?('description') + issue.previous_changes.include?('description') todo_service.update_issue(issue, current_user) end @@ -24,6 +24,10 @@ module Issues todo_service.reassigned_issue(issue, current_user) end + if issue.previous_changes.include?('confidential') + create_confidentiality_note(issue) + end + added_labels = issue.labels - old_labels if added_labels.present? notification_service.relabeled_issue(issue, added_labels, current_user) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 4bdb1b0c07..e9e3c472e6 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -175,6 +175,25 @@ class SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when the confidentiality changes + # + # noteable - Noteable object that responds to 'confidential' + # project - Project owning noteable + # author - User performing the change + # + # Example Note text: + # + # "Marked as confidential" + # + # Returns the created Note object + def self.change_confidentiality(noteable, project, author) + return unless noteable.respond_to?(:confidential) + + confidentiality_status = noteable.confidential ? "confidential" : "not confidential" + body = "Marked as #{confidentiality_status}" + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when a branch in Noteable is changed # # noteable - Noteable object diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 52f6930699..c9d8f1b382 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -27,11 +27,6 @@ describe Issues::UpdateService, services: true do end end - def update_issue(opts) - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) - @issue.reload - end - context "valid params" do before do opts = { @@ -39,7 +34,8 @@ describe Issues::UpdateService, services: true do description: 'Also please fix', assignee_id: user2.id, state_event: 'close', - label_ids: [label.id] + label_ids: [label.id], + confidential: true } perform_enqueued_jobs do @@ -84,6 +80,18 @@ describe Issues::UpdateService, services: true do expect(note).not_to be_nil expect(note.note).to eq 'Title changed from **Old title** to **New title**' end + + it 'creates system note about confidentiality change' do + note = find_note('Marked as confidential') + + expect(note).not_to be_nil + expect(note.note).to eq 'Marked as confidential' + end + end + + def update_issue(opts) + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue.reload end context 'todos' do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 5fbf2ae524..4fa6043615 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -254,6 +254,18 @@ describe SystemNoteService, services: true do end end + describe '.change_confidentiality' do + subject { described_class.change_confidentiality(noteable, project, author) } + + context 'when noteable responds to `confidential`' do + it_behaves_like 'a system note' + + it 'sets the note text' do + expect(subject.note).to eq "Marked as not confidential" + end + end + end + describe '.change_branch' do subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) } let(:old_branch) { 'old_branch'} From cf3b149e6506c516d6f691f6e36c599f0beb0590 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 18 May 2016 10:14:53 +0100 Subject: [PATCH 320/449] Point group's issues atom feed to group This was using the user dashboard atom builder template, not one for the group's issues. --- app/views/groups/issues.atom.builder | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 486d1d8587..a6eb9abada 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@user.name} issues" - xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml" - xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" - xml.id issues_dashboard_url + xml.title "#{@group.name} issues" + xml.link href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: issues_group_url, rel: "alternate", type: "text/html" + xml.id issues_group_url xml.updated @issues.first.created_at.xmlschema if @issues.any? @issues.each do |issue| From 3eacb19781d7812b8164b8ef65d290b09682ba57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 10:38:01 -0500 Subject: [PATCH 321/449] Fix 8.7.6 CHANGELOG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable --- CHANGELOG | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b4d960c584..2dd8077954 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -62,10 +62,8 @@ v 8.8.0 (unreleased) v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) - - Fix import from gitlab.com to a private instance failure - -v 8.7.6 - - Fix external imports not finding the import data + - Fix import from GitLab.com to a private instance failure. !4181 + - Fix external imports not finding the import data. !4106 v 8.7.5 - Fix relative links in wiki pages. !4050 From 80817644a74e6ead62c2c66d4bd2826ecf634393 Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Wed, 16 Sep 2015 13:19:46 +0200 Subject: [PATCH 322/449] Improve issue formatting in Slack service Signed-off-by: Jeroen van Baarsen --- CHANGELOG | 3 +++ .../slack_service/issue_message.rb | 19 ++++++++++++++++--- .../slack_service/issue_message_spec.rb | 11 ++++++----- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2dd8077954..d30c0e2a3f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -894,6 +894,9 @@ v 8.1.3 - Add Facebook authentication v 8.1.2 + - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) + +v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) - Add migration to remove satellites directory - Fix specific runners visibility diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index 438ff33fdf..88e053ec19 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -34,7 +34,12 @@ class SlackService private def message - "#{user_name} #{state} #{issue_link} in #{project_link}: *#{title}*" + case state + when "opened" + "[#{project_link}] Issue #{state} by #{user_name}" + else + "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" + end end def opened_issue? @@ -42,7 +47,11 @@ class SlackService end def description_message - [{ text: format(description), color: attachment_color }] + [{ + title: issue_title, + title_link: issue_url, + text: format(description), + color: "#C95823" }] end def project_link @@ -50,7 +59,11 @@ class SlackService end def issue_link - "[issue ##{issue_iid}](#{issue_url})" + "[#{issue_title}](#{issue_url})" + end + + def issue_title + "##{issue_iid} #{title}" end end end diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb index f648cbe2de..0f8889bdf3 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -25,7 +25,7 @@ describe SlackService::IssueMessage, models: true do } end - let(:color) { '#345' } + let(:color) { '#C95823' } context '#initialize' do before do @@ -40,10 +40,11 @@ describe SlackService::IssueMessage, models: true do context 'open' do it 'returns a message regarding opening of issues' do expect(subject.pretext).to eq( - 'Test User opened in : '\ - '*Issue title*') + '] Issue opened by Test User') expect(subject.attachments).to eq([ { + title: "#100 Issue title", + title_link: "url", text: "issue description", color: color, } @@ -56,10 +57,10 @@ describe SlackService::IssueMessage, models: true do args[:object_attributes][:action] = 'close' args[:object_attributes][:state] = 'closed' end + it 'returns a message regarding closing of issues' do expect(subject.pretext). to eq( - 'Test User closed in : '\ - '*Issue title*') + '] Issue closed by Test User') expect(subject.attachments).to be_empty end end From afa3e1ded1ba6b2cb3837cf3da29ff191a18e2bd Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Tue, 10 May 2016 22:08:06 +0200 Subject: [PATCH 323/449] Add changelog entry --- CHANGELOG | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d30c0e2a3f..f060643c06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -70,6 +70,7 @@ v 8.7.5 - Fix always showing build notification message when switching between merge requests !4086 - Fix an issue when filtering merge requests with more than one label. !3886 - Fix short note for the default scope on build page (Takuya Noguchi) + - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) @@ -893,9 +894,6 @@ v 8.1.3 - Use issue editor as cross reference comment author when issue is edited with a new mention - Add Facebook authentication -v 8.1.2 - - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) - v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) - Add migration to remove satellites directory From 78e2d0ac3c6d4e4477625840063e0d4a54f36594 Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Wed, 18 May 2016 11:12:21 -0500 Subject: [PATCH 324/449] Move to the latest version in the CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f060643c06..9baab4734c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,6 +59,7 @@ v 8.8.0 (unreleased) - Add counter metrics for rails cache - Import pull requests from GitHub where the source or target branches were removed - All Grape API helpers are now instrumented + - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) @@ -70,7 +71,6 @@ v 8.7.5 - Fix always showing build notification message when switching between merge requests !4086 - Fix an issue when filtering merge requests with more than one label. !3886 - Fix short note for the default scope on build page (Takuya Noguchi) - - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) v 8.7.4 - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) From 27fdd3fe50bd576dc5372c9877eee0216d71c2f1 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 18 May 2016 17:14:55 +0100 Subject: [PATCH 325/449] Maintain commit order in MRs `Gitlab::Git::Compare` will already have the correct order; sorting in Ruby can only ruin that. (The correct order being the same as `git log` - reverse chronological while maintaining the commit graph.) As an example, imagine a branch where someone has their system clock set wrong for some of the commits. Not only will those commits be in the wrong order - which is maybe reasonable - but sorting in Ruby can also put commits with the same timestamp out of order, as Ruby's sorting isn't stable. --- app/models/merge_request_diff.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 6ad8fc3f03..7d5103748f 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -98,9 +98,7 @@ class MergeRequestDiff < ActiveRecord::Base commits = compare.commits if commits.present? - commits = Commit.decorate(commits, merge_request.source_project). - sort_by(&:created_at). - reverse + commits = Commit.decorate(commits, merge_request.source_project).reverse end commits From 8a8b5497c5142f225fb4fa3f5441526a0652869a Mon Sep 17 00:00:00 2001 From: Adam Butler Date: Wed, 6 Apr 2016 20:37:09 +0100 Subject: [PATCH 326/449] Create DiffFilter and change SystemNoteService#change_title to use Gitlab::Diff::InlineDiff --- CHANGELOG | 4 ++ app/assets/stylesheets/framework/files.scss | 16 ----- .../stylesheets/framework/typography.scss | 8 +++ .../stylesheets/framework/variables.scss | 3 + app/helpers/diff_helper.rb | 4 +- app/services/system_note_service.rb | 9 ++- app/views/projects/diffs/_file.html.haml | 6 +- lib/banzai/filter/inline_diff_filter.rb | 22 ++++++ lib/banzai/pipeline/gfm_pipeline.rb | 3 +- lib/gitlab/diff/inline_diff_marker.rb | 26 +++++-- spec/features/markdown_spec.rb | 4 ++ spec/fixtures/markdown.md.erb | 13 ++++ spec/helpers/diff_helper_spec.rb | 4 +- .../banzai/filter/inline_diff_filter_spec.rb | 68 +++++++++++++++++++ spec/services/issues/update_service_spec.rb | 4 +- .../merge_requests/update_service_spec.rb | 4 +- spec/services/system_note_service_spec.rb | 2 +- spec/support/matchers/markdown_matchers.rb | 10 +++ 18 files changed, 172 insertions(+), 38 deletions(-) create mode 100644 lib/banzai/filter/inline_diff_filter.rb create mode 100644 spec/lib/banzai/filter/inline_diff_filter_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 2dd8077954..71964051bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -100,6 +100,10 @@ v 8.7.1 - Prevent users from deleting Webhooks via API they do not own - Fix Error 500 due to stale cache when projects are renamed or transferred - Update width of search box to fix Safari bug. !3900 (Jedidiah) + - Added inline diff styling for `change_title` system notes. !3576 (Adam Butler) + - Added `InlineDiffFilter` to the markdown parser. !3576 (Adam Butler) + +v 8.7.1 (unreleased) - Use the `can?` helper instead of `current_user.can?` v 8.7.0 diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 61d9954c6c..8c96c7a9c3 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -36,22 +36,6 @@ } } - .filename { - &.old { - display: inline-block; - span.idiff { - background-color: #f8cbcb; - } - } - - &.new { - display: inline-block; - span.idiff { - background-color: #a6f3a6; - } - } - } - a:not(.btn) { color: $gl-dark-link-color; } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 2779cd5678..487081c3c7 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -269,3 +269,11 @@ h1, h2, h3, h4 { text-align: right; } } + +.idiff.deletion { + background: $gl-idiff-deletion; +} + +.idiff.addition { + background: $gl-idiff-addition; +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 5fa4c26660..b7d8f61162 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -178,6 +178,9 @@ $table-border-gray: #f0f0f0; $line-target-blue: #eaf3fc; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; +$gl-idiff-deletion: #f8cbcb; +$gl-idiff-addition: #a6f3a6; + /* * Fonts */ diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 5f311f3780..ea383f9b0f 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -2,8 +2,8 @@ module DiffHelper def mark_inline_diffs(old_line, new_line) old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs - marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs) - marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs) + marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs, mode: :deletion) + marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs, mode: :addition) [marked_old_line, marked_new_line] end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 4bdb1b0c07..758abd7f54 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -171,7 +171,14 @@ class SystemNoteService def self.change_title(noteable, project, author, old_title) return unless noteable.respond_to?(:title) - body = "Title changed from **#{old_title}** to **#{noteable.title}**" + new_title = noteable.title.dup + + old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs + + marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true) + marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true) + + body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**" create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 0f04fc5d33..7194d9c3eb 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -11,11 +11,9 @@ = link_to "#diff-#{i}" do - if diff_file.renamed_file - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - .filename.old - = old_path + = old_path → - .filename.new - = new_path + = new_path - else %span = diff_file.new_path diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb new file mode 100644 index 0000000000..9e75edd4d4 --- /dev/null +++ b/lib/banzai/filter/inline_diff_filter.rb @@ -0,0 +1,22 @@ +module Banzai + module Filter + class InlineDiffFilter < HTML::Pipeline::Filter + IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set + + def call + search_text_nodes(doc).each do |node| + next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) + + content = node.to_html + content = content.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '\1\2') + content = content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '\1\2') + + next if html == content + + node.replace(content) + end + doc + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index ed3cfd6b02..b27ecf3c92 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -23,7 +23,8 @@ module Banzai Filter::LabelReferenceFilter, Filter::MilestoneReferenceFilter, - Filter::TaskListFilter + Filter::TaskListFilter, + Filter::InlineDiffFilter ] end diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index dccb717e95..c9601d42d5 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -1,6 +1,11 @@ module Gitlab module Diff class InlineDiffMarker + MARKDOWN_SYMBOLS = { + addition: "+", + deletion: "-" + } + attr_accessor :raw_line, :rich_line def initialize(raw_line, rich_line = raw_line) @@ -8,7 +13,7 @@ module Gitlab @rich_line = ERB::Util.html_escape(rich_line) end - def mark(line_inline_diffs) + def mark(line_inline_diffs, mode: nil, markdown: false) return rich_line unless line_inline_diffs marker_ranges = [] @@ -20,13 +25,12 @@ module Gitlab end offset = 0 - # Mark each range - marker_ranges.each_with_index do |range, i| - class_names = ["idiff"] - class_names << "left" if i == 0 - class_names << "right" if i == marker_ranges.length - 1 - offset = insert_around_range(rich_line, range, "", "", offset) + # Mark each range + marker_ranges.each_with_index do |range, index| + before_content = markdown ? "{#{MARKDOWN_SYMBOLS[mode]}" : "" + after_content = markdown ? "#{MARKDOWN_SYMBOLS[mode]}}" : "" + offset = insert_around_range(rich_line, range, before_content, after_content, offset) end rich_line.html_safe @@ -34,6 +38,14 @@ module Gitlab private + def html_class_names(marker_ranges, mode, index) + class_names = ["idiff"] + class_names << "left" if index == 0 + class_names << "right" if index == marker_ranges.length - 1 + class_names << mode if mode + class_names.join(" ") + end + # Mapping of character positions in the raw line, to the rich (highlighted) line def position_mapping @position_mapping ||= begin diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 0148c87084..1d892fe1a5 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -278,6 +278,10 @@ describe 'GitLab Markdown', feature: true do it 'includes GollumTagsFilter' do expect(doc).to parse_gollum_tags end + + it 'includes InlineDiffFilter' do + expect(doc).to parse_inline_diffs + end end # Fake a `current_user` helper diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 1772cc3f6a..92c7fdf27b 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -239,3 +239,16 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - [[link-text|http://example.com/pdfs/gollum.pdf]] - [[images/example.jpg]] - [[http://example.com/images/example.jpg]] + +### Inline Diffs + +With inline diffs tags you can display {+ additions +} or [- deletions -]. + +The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}. + +However the wrapping tags can not be mixed as such - + +- {+ additions +] +- [+ additions +} +- {- delletions -] +- [- delletions -} diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index b7810185d1..52764f41e0 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -93,9 +93,9 @@ describe DiffHelper do it "returns strings with marked inline diffs" do marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) - expect(marked_old_line).to eq("abc 'def'") + expect(marked_old_line).to eq("abc 'def'") expect(marked_old_line).to be_html_safe - expect(marked_new_line).to eq("abc "def"") + expect(marked_new_line).to eq("abc "def"") expect(marked_new_line).to be_html_safe end end diff --git a/spec/lib/banzai/filter/inline_diff_filter_spec.rb b/spec/lib/banzai/filter/inline_diff_filter_spec.rb new file mode 100644 index 0000000000..9e52637129 --- /dev/null +++ b/spec/lib/banzai/filter/inline_diff_filter_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Banzai::Filter::InlineDiffFilter, lib: true do + include FilterSpecHelper + + it 'adds inline diff span tags for deletions when using square brackets' do + doc = "START [-something deleted-] END" + expect(filter(doc).to_html).to eq('START something deleted END') + end + + it 'adds inline diff span tags for deletions when using curley braces' do + doc = "START {-something deleted-} END" + expect(filter(doc).to_html).to eq('START something deleted END') + end + + it 'does not add inline diff span tags when a closing tag is not provided' do + doc = "START [- END" + expect(filter(doc).to_html).to eq(doc) + end + + it 'adds inline span tags for additions when using square brackets' do + doc = "START [+something added+] END" + expect(filter(doc).to_html).to eq('START something added END') + end + + it 'adds inline span tags for additions when using curley braces' do + doc = "START {+something added+} END" + expect(filter(doc).to_html).to eq('START something added END') + end + + it 'does not add inline diff span tags when a closing addition tag is not provided' do + doc = "START {+ END" + expect(filter(doc).to_html).to eq(doc) + end + + it 'does not add inline diff span tags when the tags do not match' do + examples = [ + "{+ additions +]", + "[+ additions +}", + "{- delletions -]", + "[- delletions -}" + ] + + examples.each do |doc| + expect(filter(doc).to_html).to eq(doc) + end + end + + it 'prevents user-land html being injected' do + doc = "START {+<script>alert('I steal cookies')</script>+} END" + expect(filter(doc).to_html).to eq("START <script>alert('I steal cookies')</script> END") + end + + it 'preserves content inside pre tags' do + doc = "
    START {+something added+} END
    " + expect(filter(doc).to_html).to eq(doc) + end + + it 'preserves content inside code tags' do + doc = "START {+something added+} END" + expect(filter(doc).to_html).to eq(doc) + end + + it 'preserves content inside tt tags' do + doc = "START {+something added+} END" + expect(filter(doc).to_html).to eq(doc) + end +end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 52f6930699..ff63f2ce8d 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -79,10 +79,10 @@ describe Issues::UpdateService, services: true do end it 'creates system note about title change' do - note = find_note('Title changed') + note = find_note('Changed title:') expect(note).not_to be_nil - expect(note.note).to eq 'Title changed from **Old title** to **New title**' + expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 213e8c2eb3..56ee3e4305 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -90,10 +90,10 @@ describe MergeRequests::UpdateService, services: true do end it 'creates system note about title change' do - note = find_note('Title changed') + note = find_note('Changed title:') expect(note).not_to be_nil - expect(note.note).to eq 'Title changed from **Old title** to **New title**' + expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' end it 'creates system note about branch change' do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 5fbf2ae524..3957546b18 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -241,7 +241,7 @@ describe SystemNoteService, services: true do it 'sets the note text' do expect(subject.note). - to eq "Title changed from **Old title** to **#{noteable.title}**" + to eq "Changed title: **{-Old title-}** → **{+#{noteable.title}+}**" end end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 43cb6ef43f..259aa469a1 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -168,6 +168,16 @@ module MarkdownMatchers expect(actual).to have_selector('input[checked]', count: 3) end end + + # InlineDiffFilter + matcher :parse_inline_diffs do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('span.idiff.addition', count: 2) + expect(actual).to have_selector('span.idiff.deletion', count: 2) + end + end end # Monkeypatch the matcher DSL so that we can reduce some noisy duplication for From 6daec9e6f1a83a2a38b791a24f39e8e4b539c873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 11:05:51 -0500 Subject: [PATCH 327/449] Add documentation for Inline Diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CHANGELOG | 6 ++---- doc/markdown/markdown.md | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 71964051bb..3e1f8efa89 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,8 @@ v 8.8.0 (unreleased) - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) - Use a case-insensitive comparison in sanitizing URI schemes - Toggle sign-up confirmation emails in application settings + - Added `InlineDiffFilter` to the markdown parser. (Adam Butler) + - Added inline diff styling for `change_title` system notes. (Adam Butler) - Project#open_branches has been cleaned up and no longer loads entire records into memory. - Escape HTML in commit titles in system note messages - Improve multiple branch push performance by memoizing permission checking @@ -100,10 +102,6 @@ v 8.7.1 - Prevent users from deleting Webhooks via API they do not own - Fix Error 500 due to stale cache when projects are renamed or transferred - Update width of search box to fix Safari bug. !3900 (Jedidiah) - - Added inline diff styling for `change_title` system notes. !3576 (Adam Butler) - - Added `InlineDiffFilter` to the markdown parser. !3576 (Adam Butler) - -v 8.7.1 (unreleased) - Use the `can?` helper instead of `current_user.can?` v 8.7.0 diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 4f199b6af6..b0c6d6a4bb 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -8,6 +8,7 @@ * [Multiple underscores in words](#multiple-underscores-in-words) * [URL auto-linking](#url-auto-linking) * [Code and Syntax Highlighting](#code-and-syntax-highlighting) +* [Inline Diff](#inline-diff) * [Emoji](#emoji) * [Special GitLab references](#special-gitlab-references) * [Task lists](#task-lists) @@ -153,6 +154,19 @@ s = "There is no highlighting for this." But let's throw in a tag. ``` +## Inline Diff + +With inline diffs tags you can display {+ additions +} or [- deletions -]. + +The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}. + +However the wrapping tags cannot be mixed as such: + +- {+ additions +] +- [+ additions +} +- {- deletions -] +- [- deletions -} + ## Emoji Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: From 2f1cb7ce0439de003e92c55a27c6a8db7f0f1803 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 18 May 2016 11:22:28 -0500 Subject: [PATCH 328/449] Improve code design --- app/controllers/projects/container_registry_controller.rb | 6 ++++-- app/models/project.rb | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index 8ed5a8ff6f..3648f8894a 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -8,10 +8,12 @@ class Projects::ContainerRegistryController < Projects::ApplicationController end def destroy + url = namespace_project_container_registry_index_path(project.namespace, project) + if tag.delete - redirect_to namespace_project_container_registry_index_path(project.namespace, project) + redirect_to url else - redirect_to namespace_project_container_registry_index_path(project.namespace, project), alert: 'Failed to remove tag' + redirect_to url, alert: 'Failed to remove tag' end end diff --git a/app/models/project.rb b/app/models/project.rb index 09a214da04..321a1932a2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -331,6 +331,8 @@ class Project < ActiveRecord::Base end def container_registry_repository + return unless Gitlab.config.registry.enabled + @container_registry_repository ||= begin token = Auth::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace) url = Gitlab.config.registry.api_url @@ -347,9 +349,9 @@ class Project < ActiveRecord::Base end def has_container_registry_tags? - if Gitlab.config.registry.enabled - container_registry_repository.tags.any? - end + return unless container_registry_repository + + container_registry_repository.tags.any? end def commit(id = 'HEAD') From 66c421c0a788ab88f871103a26dceefc67585eec Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 18 May 2016 10:51:11 +0100 Subject: [PATCH 329/449] Link to group feed from group activity page --- CHANGELOG | 1 + app/views/groups/_activities.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 48e3889960..61c6f80af2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -39,6 +39,7 @@ v 8.8.0 (unreleased) - Create tags using Rugged for performance reasons. !3745 - API: Expose Issue#user_notes_count. !3126 (Anton Popov) - Don't show forks button when user can't view forks + - Fix atom feed links and rendering - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) - Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724 diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index dc76599b77..71cc4d87b1 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -4,7 +4,7 @@ .nav-block - if current_user .controls - = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do + = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do %i.fa.fa-rss = render 'shared/event_filter' From 98a7486ba76eb7235174b4b6f74794c664df29da Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 18 May 2016 12:28:48 -0500 Subject: [PATCH 330/449] Verify if registry is enabled in registry --- app/controllers/projects/container_registry_controller.rb | 5 +++++ app/helpers/projects_helper.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index 3648f8894a..d1f4649720 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -1,4 +1,5 @@ class Projects::ContainerRegistryController < Projects::ApplicationController + before_action :verify_registry_enabled before_action :authorize_read_container_image! before_action :authorize_update_container_image!, only: [:destroy] layout 'project' @@ -19,6 +20,10 @@ class Projects::ContainerRegistryController < Projects::ApplicationController private + def verify_registry_enabled + render_404 unless Gitlab.config.registry.enabled + end + def container_registry_repository @container_registry_repository ||= project.container_registry_repository end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b6ba66bf3e..a140bbc3c6 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -152,7 +152,7 @@ module ProjectsHelper nav_tabs << :builds end - if project.container_registry_repository_url.present? && can?(current_user, :read_container_image, project) + if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) nav_tabs << :container_registry end From ef60b8e1685a8761477e822b3190a3a0cf4b0cfa Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 18 May 2016 13:02:10 -0500 Subject: [PATCH 331/449] Use pipelines.errors when communicating the error --- .../projects/pipelines_controller.rb | 14 ++++---- app/services/ci/create_pipeline_service.rb | 36 +++++++++++-------- app/views/projects/pipelines/new.html.haml | 7 ++-- spec/features/pipelines_spec.rb | 15 +++++--- 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 19071cc9e4..1282913d98 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -15,19 +15,17 @@ class Projects::PipelinesController < Projects::ApplicationController end def new + @pipeline = project.ci_commits.new end def create - begin - pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute - redirect_to namespace_project_pipeline_path(project.namespace, project, pipeline) - rescue ArgumentError => e - flash[:alert] = e.message - render 'new' - rescue - flash[:alert] = 'The pipeline could not be created. Please try again.' + @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute + unless @pipeline.persisted? render 'new' + return end + + redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline) end def show diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index e13f4fce13..b864807ec3 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -1,27 +1,39 @@ module Ci class CreatePipelineService < BaseService def execute + pipeline = project.ci_commits.new + unless ref_names.include?(params[:ref]) - raise ArgumentError, 'Reference not found' + pipeline.errors.add(:base, 'Reference not found') + return pipeline end unless commit - raise ArgumentError, 'Commit not found' + pipeline.errors.add(:base, 'Commit not found') + return pipeline end unless can?(current_user, :create_pipeline, project) - raise RuntimeError, 'Insufficient permissions to create a new pipeline' + pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline') + return pipeline end - pipeline = new_pipeline + begin + Ci::Commit.transaction do + pipeline.sha = commit.id + pipeline.ref = params[:ref] + pipeline.before_sha = Gitlab::Git::BLANK_SHA - Ci::Commit.transaction do - unless pipeline.config_processor - raise ArgumentError, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file' + unless pipeline.config_processor + pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file') + raise ActiveRecord::Rollback + end + + pipeline.save! + pipeline.create_builds(current_user) end - - pipeline.save! - pipeline.create_builds(current_user) + rescue + pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.') end pipeline @@ -29,10 +41,6 @@ module Ci private - def new_pipeline - project.ci_commits.new(sha: commit.id, ref: params[:ref], before_sha: Gitlab::Git::BLANK_SHA) - end - def ref_names @ref_names ||= project.repository.ref_names end diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 534a495dd8..1050b28b38 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -1,15 +1,12 @@ - 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_pipelines_path, method: :post, id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" do += form_for @pipeline, url: namespace_project_pipelines_path(@project.namespace, @project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do + = form_errors(@pipeline) .form-group = label_tag :ref, 'Create for', class: 'control-label' .col-sm-10 diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 1df516eafd..32665aadd2 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -126,12 +126,19 @@ describe "Pipelines" do before { visit new_namespace_project_pipeline_path(project.namespace, project) } context 'for valid commit' do - before do - fill_in('Create for', with: 'master') - stub_ci_commit_to_return_yaml_file + before { fill_in('Create for', with: 'master') } + + context 'with gitlab-ci.yml' do + before { stub_ci_commit_to_return_yaml_file } + + it { expect{ click_on 'Create pipeline' }.to change{ Ci::Commit.count }.by(1) } end - it { expect{ click_on 'Create pipeline' }.to change{ Ci::Commit.count }.by(1) } + context 'without gitlab-ci.yml' do + before { click_on 'Create pipeline' } + + it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + end end context 'for invalid commit' do From 5341b16fc92425d557a1c62d0329b276170eba73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 13:20:46 -0500 Subject: [PATCH 332/449] Reuse existing SCSS variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/assets/stylesheets/framework/typography.scss | 4 ++-- app/assets/stylesheets/framework/variables.scss | 2 -- lib/gitlab/diff/inline_diff_marker.rb | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 487081c3c7..3575984b22 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -271,9 +271,9 @@ h1, h2, h3, h4 { } .idiff.deletion { - background: $gl-idiff-deletion; + background: $line-removed-dark; } .idiff.addition { - background: $gl-idiff-addition; + background: $line-added-dark; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b7d8f61162..542c0c53f6 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -178,8 +178,6 @@ $table-border-gray: #f0f0f0; $line-target-blue: #eaf3fc; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; -$gl-idiff-deletion: #f8cbcb; -$gl-idiff-addition: #a6f3a6; /* * Fonts diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index c9601d42d5..87a9b1e23a 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -28,8 +28,18 @@ module Gitlab # Mark each range marker_ranges.each_with_index do |range, index| - before_content = markdown ? "{#{MARKDOWN_SYMBOLS[mode]}" : "" - after_content = markdown ? "#{MARKDOWN_SYMBOLS[mode]}}" : "" + before_content = + if markdown + "{#{MARKDOWN_SYMBOLS[mode]}" + else + "" + end + after_content = + if markdown + "#{MARKDOWN_SYMBOLS[mode]}}" + else + "" + end offset = insert_around_range(rich_line, range, before_content, after_content, offset) end From b5552bc4e57f73a80e990ee4182fb48fa4851ad3 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 18 May 2016 08:48:48 -0700 Subject: [PATCH 333/449] Add health check feature documentation --- doc/README.md | 1 + doc/monitoring/health_check.md | 57 ++++++++++++++++++++++ doc/monitoring/img/health_check_token.png | Bin 0 -> 10884 bytes 3 files changed, 58 insertions(+) create mode 100644 doc/monitoring/health_check.md create mode 100644 doc/monitoring/img/health_check_token.png diff --git a/doc/README.md b/doc/README.md index e358da1c42..8aed060d24 100644 --- a/doc/README.md +++ b/doc/README.md @@ -41,6 +41,7 @@ - [Git LFS configuration](workflow/lfs/lfs_administration.md) - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. - [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics +- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint - [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs - [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md new file mode 100644 index 0000000000..bbfb63dd84 --- /dev/null +++ b/doc/monitoring/health_check.md @@ -0,0 +1,57 @@ +# Health Check +_**Note:** This feature was [introduced][ce-3888] in GitLab 8.8_ + +GitLab provides a health check endpoint for uptime monitoring on the `health_check` web +endpoint. The health check reports on the overall system status based on the status of +the database connection, the state of the database migrations, and the ability to write +and access the cache. This endpoint can be provided to uptime monitoring services like +[Pingdom][pindom], [Nagios][nagios-health], and [NewRelic][newrelic-health]. + +## Access Token + +A access token needs to be provided while accessing the health check endpoint. The current +accepted token can be found on the `admin/heath_check` page of your GitLab instance. + +![access token](img/health_check_token.png) + +The access token can be passed as a url parameter: + +`https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN` + +or as a http header: + +```bash +curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +``` + +## Using the Endpoint + +Once you have the access token, health information can be retrieved as plain text, JSON, +or XML using the `health_check` endpoint: + +- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN` + +You can also ask for the status of specific services: + +- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN` + +Example output: +```bash +curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +{"healthy":true,"message":"success"} +``` + +## Status + +On failure the endpoint will return a `500` http status code. On success the endpoint +will return a valid successful http status code, and a `success` message. Ideally your +uptime monitoring should look for the success message. + +[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888 +[pingdom]: https://www.pingdom.com +[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html +[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring diff --git a/doc/monitoring/img/health_check_token.png b/doc/monitoring/img/health_check_token.png new file mode 100644 index 0000000000000000000000000000000000000000..2daf8606b009aedda120af03baa13c36a195f07d GIT binary patch literal 10884 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 literal 0 HcmV?d00001 From b340ca356a652a53c30103e41a5c854a9ba1650f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 13:43:33 -0500 Subject: [PATCH 334/449] Remove User#tm_in_authorized_projects and User#tm_of MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These methods seems to be unused. Signed-off-by: Rémy Coutable --- app/models/user.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 368a3f3cfb..6a09b78455 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -397,11 +397,6 @@ class User < ActiveRecord::Base owned_groups.select(:id), namespace.id).joins(:namespace) end - # Team membership in authorized projects - def tm_in_authorized_projects - ProjectMember.where(source_id: authorized_projects.map(&:id), user_id: self.id) - end - def is_admin? admin end @@ -491,10 +486,6 @@ class User < ActiveRecord::Base "#{name} (#{username})" end - def tm_of(project) - project.project_member_by_id(self.id) - end - def already_forked?(project) !!fork_of(project) end From 35430f9ef72664218a3cdc0f7c06852a5d6e90ab Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 18 May 2016 12:00:26 -0700 Subject: [PATCH 335/449] Fix broken pingdom link in the health_check docs --- doc/monitoring/health_check.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md index bbfb63dd84..22c53a9aec 100644 --- a/doc/monitoring/health_check.md +++ b/doc/monitoring/health_check.md @@ -5,7 +5,7 @@ GitLab provides a health check endpoint for uptime monitoring on the `health_che endpoint. The health check reports on the overall system status based on the status of the database connection, the state of the database migrations, and the ability to write and access the cache. This endpoint can be provided to uptime monitoring services like -[Pingdom][pindom], [Nagios][nagios-health], and [NewRelic][newrelic-health]. +[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health]. ## Access Token From b330d5d0164d86ab06679e4304a7b57449af58ee Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 18 May 2016 23:35:31 +0200 Subject: [PATCH 336/449] Fix changelog for 7.9.3 [ci-skip] --- CHANGELOG | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 70846be1c1..cdb88cafe1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1520,20 +1520,17 @@ v 7.10.0 - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller) - Fix merge request comments on files with multiple commits - Fix Resource Owner Password Authentication Flow - -v 7.9.4 - - Security: Fix project import URL regex to prevent arbitary local repos from being imported - - Fixed issue where only 25 commits would load in file listings - - Fix LDAP identities after config update - -v 7.9.3 - - Contains no changes - Add icons to Add dropdown items. - Allow admin to create public deploy keys that are accessible to any project. - Warn when gitlab-shell version doesn't match requirement. - Skip email confirmation when set by admin or via LDAP. - Only allow users to reference groups, projects, issues, MRs, commits they have access to. +v 7.9.4 + - Security: Fix project import URL regex to prevent arbitary local repos from being imported + - Fixed issue where only 25 commits would load in file listings + - Fix LDAP identities after config update + v 7.9.3 - Contains no changes From 4f1c63683175fa88ca41ba2180b68e266d7118e4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 18 May 2016 17:01:42 -0500 Subject: [PATCH 337/449] Create pipeline objects with parameters --- app/controllers/projects/pipelines_controller.rb | 4 ++-- app/services/ci/create_pipeline_service.rb | 4 +--- app/views/projects/commit/_ci_commit.html.haml | 4 ++-- app/views/projects/pipelines/new.html.haml | 8 ++++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 1282913d98..b36081205d 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -15,7 +15,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def new - @pipeline = project.ci_commits.new + @pipeline = project.ci_commits.new(ref: @project.default_branch) end def create @@ -46,7 +46,7 @@ class Projects::PipelinesController < Projects::ApplicationController private def create_params - params.permit(:ref) + params.require(:pipeline).permit(:ref) end def pipeline diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index b864807ec3..5bc0c31cb4 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -1,7 +1,7 @@ module Ci class CreatePipelineService < BaseService def execute - pipeline = project.ci_commits.new + pipeline = project.ci_commits.new(params) unless ref_names.include?(params[:ref]) pipeline.errors.add(:base, 'Reference not found') @@ -21,8 +21,6 @@ module Ci begin Ci::Commit.transaction do pipeline.sha = commit.id - pipeline.ref = params[:ref] - pipeline.before_sha = Gitlab::Git::BLANK_SHA unless pipeline.config_processor pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file') diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index df92a38db3..8228c067be 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -7,8 +7,8 @@ - if ci_commit.builds.running_or_pending.any? = 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 - - if defined?(pipeline_details) && pipeline_details - .oneline + .oneline.clearfix + - if defined?(pipeline_details) && pipeline_details Pipeline = link_to "##{ci_commit.id}", namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), class: "monospace" with diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 1050b28b38..b97c9f5f3b 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -5,15 +5,15 @@ New Pipeline %hr -= form_for @pipeline, url: namespace_project_pipelines_path(@project.namespace, @project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do += form_for @pipeline, as: :pipeline, url: namespace_project_pipelines_path(@project.namespace, @project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f| = form_errors(@pipeline) .form-group - = label_tag :ref, 'Create for', class: 'control-label' + = f.label :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' + = f.text_field :ref, 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 + = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_pipelines_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript From ce3958ebae0332ddc295c7fe234b6a8af6f25da8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 18 May 2016 17:03:12 -0500 Subject: [PATCH 338/449] Use extended regexp flag for `git grep`-ing files Closes #3055 --- app/models/repository.rb | 2 +- spec/models/repository_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 3716ea6ad6..ca62fbbdf0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -854,7 +854,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 34a13f9b5c..7c94f3639d 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -100,6 +100,12 @@ describe Repository, models: true do expect(results.first).not_to start_with('fatal:') end + it 'properly handles an unmatched parenthesis' do + results = repository.search_files("test(", 'master') + + expect(results.first).not_to start_with('fatal:') + end + describe 'result' do subject { results.first } From 958ff11f971823aa11ac547d7e83ccf9603566d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 17:13:54 -0500 Subject: [PATCH 339/449] Remove unused Project scopes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/models/project.rb | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 2a6b0839b6..907d80c068 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -171,17 +171,17 @@ class Project < ActiveRecord::Base scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } - scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') } - scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) } - scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped } - scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } - scope :in_group_namespace, -> { joins(:group) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } + scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) } scope :non_archived, -> { where(archived: false) } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } + scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } + + scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } + scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } state_machine :import_status, initial: :none do event :import_start do @@ -208,18 +208,6 @@ class Project < ActiveRecord::Base end class << self - def abandoned - where('projects.last_activity_at < ?', 6.months.ago) - end - - def with_push - joins(:events).where('events.action = ?', Event::PUSHED) - end - - def active - joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') - end - # Searches for a list of projects based on the query given in `query`. # # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive @@ -281,10 +269,6 @@ class Project < ActiveRecord::Base projects.iwhere('projects.path' => project_path).take end - def find_by_ci_id(id) - find_by(ci_id: id.to_i) - end - def visibility_levels Gitlab::VisibilityLevel.options end @@ -315,10 +299,6 @@ class Project < ActiveRecord::Base joins(join_body).reorder('join_note_counts.amount DESC') end - - def visible_to_user(user) - where(id: user.authorized_projects.select(:id).reorder(nil)) - end end def team From 0698113cabe68f23ac38d7b2fde6c5cb7e5bec4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 12:56:13 -0500 Subject: [PATCH 340/449] Move #create_confidentiality_note to Issues::UpdateService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/services/issuable_base_service.rb | 6 ------ app/services/issues/update_service.rb | 8 +++++++- app/services/system_note_service.rb | 19 +++++++------------ spec/services/issues/update_service_spec.rb | 4 ++-- spec/services/system_note_service_spec.rb | 14 +++----------- 5 files changed, 19 insertions(+), 32 deletions(-) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 1f59281e27..2b16089df1 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -36,12 +36,6 @@ class IssuableBaseService < BaseService end end - def create_confidentiality_note(issuable) - SystemNoteService.change_confidentiality( - issuable, issuable.project, current_user - ) - end - def filter_params(issuable_ability_name = :issue) filter_assignee filter_milestone diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 3a9bbf8a78..c7d406cc33 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -10,7 +10,7 @@ module Issues end if issue.previous_changes.include?('title') || - issue.previous_changes.include?('description') + issue.previous_changes.include?('description') todo_service.update_issue(issue, current_user) end @@ -41,5 +41,11 @@ module Issues def close_service Issues::CloseService end + + private + + def create_confidentiality_note(issue) + SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user) + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e9e3c472e6..972f8b2012 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -169,29 +169,24 @@ class SystemNoteService # # Returns the created Note object def self.change_title(noteable, project, author, old_title) - return unless noteable.respond_to?(:title) - body = "Title changed from **#{old_title}** to **#{noteable.title}**" create_note(noteable: noteable, project: project, author: author, note: body) end # Called when the confidentiality changes # - # noteable - Noteable object that responds to 'confidential' - # project - Project owning noteable - # author - User performing the change + # issue - Issue object + # project - Project owning the issue + # author - User performing the change # # Example Note text: # - # "Marked as confidential" + # "Made the issue confidential" # # Returns the created Note object - def self.change_confidentiality(noteable, project, author) - return unless noteable.respond_to?(:confidential) - - confidentiality_status = noteable.confidential ? "confidential" : "not confidential" - body = "Marked as #{confidentiality_status}" - create_note(noteable: noteable, project: project, author: author, note: body) + def self.change_issue_confidentiality(issue, project, author) + body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible' + create_note(noteable: issue, project: project, author: author, note: body) end # Called when a branch in Noteable is changed diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index c9d8f1b382..810a1f2d66 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -82,10 +82,10 @@ describe Issues::UpdateService, services: true do end it 'creates system note about confidentiality change' do - note = find_note('Marked as confidential') + note = find_note('Made the issue confidential') expect(note).not_to be_nil - expect(note.note).to eq 'Marked as confidential' + expect(note.note).to eq 'Made the issue confidential' end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 4fa6043615..bffad59b8b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -244,24 +244,16 @@ describe SystemNoteService, services: true do to eq "Title changed from **Old title** to **#{noteable.title}**" end end - - context 'when noteable does not respond to `title' do - let(:noteable) { double('noteable') } - - it 'returns nil' do - expect(subject).to be_nil - end - end end - describe '.change_confidentiality' do - subject { described_class.change_confidentiality(noteable, project, author) } + describe '.change_issue_confidentiality' do + subject { described_class.change_issue_confidentiality(noteable, project, author) } context 'when noteable responds to `confidential`' do it_behaves_like 'a system note' it 'sets the note text' do - expect(subject.note).to eq "Marked as not confidential" + expect(subject.note).to eq 'Made the issue visible' end end end From 0d9c351b47dedd973620531ff3412d6da6190b2f Mon Sep 17 00:00:00 2001 From: Karlo Soriano Date: Fri, 13 May 2016 13:42:07 +0800 Subject: [PATCH 341/449] Wrap "No groups found" message with a .nothing-here-block --- app/views/shared/groups/_list.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml index 1aa7ed1f2e..427595c47a 100644 --- a/app/views/shared/groups/_list.html.haml +++ b/app/views/shared/groups/_list.html.haml @@ -3,4 +3,4 @@ - groups.each_with_index do |group, i| = render "shared/groups/group", group: group - else - %h3 No groups found + .nothing-here-block No groups found From 0c47b68d0474b595bb03a49ce755c96f5d00fbf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 18 May 2016 21:16:36 -0500 Subject: [PATCH 342/449] Mask credentials from URL when import of project has failed. --- app/models/project.rb | 4 +- app/workers/repository_import_worker.rb | 2 +- ...8_remove_wrong_import_url_from_projects.rb | 2 +- .../{import_url.rb => url_sanitizer.rb} | 17 ++++- spec/lib/gitlab/import_url_spec.rb | 21 ------ spec/lib/gitlab/url_sanitizer_spec.rb | 68 +++++++++++++++++++ spec/workers/repository_import_worker_spec.rb | 26 +++++-- 7 files changed, 107 insertions(+), 33 deletions(-) rename lib/gitlab/{import_url.rb => url_sanitizer.rb} (65%) delete mode 100644 spec/lib/gitlab/import_url_spec.rb create mode 100644 spec/lib/gitlab/url_sanitizer_spec.rb diff --git a/app/models/project.rb b/app/models/project.rb index a3c4f1d8e9..b464335597 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -376,14 +376,14 @@ class Project < ActiveRecord::Base end def import_url=(value) - import_url = Gitlab::ImportUrl.new(value) + import_url = Gitlab::UrlSanitizer.new(value) create_or_update_import_data(credentials: import_url.credentials) super(import_url.sanitized_url) end def import_url if import_data && super - import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials) + import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials) import_url.full_url else super diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 2937493c61..fbc7ed63c6 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -13,7 +13,7 @@ class RepositoryImportWorker result = Projects::ImportService.new(project, current_user).execute if result[:status] == :error - project.update(import_error: result[:message]) + project.update(import_error: Gitlab::UrlSanitizer.sanitize(result[:message])) project.import_fail return end diff --git a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb index 8a351cf27a..561c18a577 100644 --- a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb +++ b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb @@ -24,7 +24,7 @@ class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration def process_projects_with_wrong_url projects_with_wrong_import_url.each do |project| begin - import_url = Gitlab::ImportUrl.new(project["import_url"]) + import_url = Gitlab::UrlSanitizer.new(project["import_url"]) update_import_url(import_url, project) update_import_data(import_url, project) diff --git a/lib/gitlab/import_url.rb b/lib/gitlab/url_sanitizer.rb similarity index 65% rename from lib/gitlab/import_url.rb rename to lib/gitlab/url_sanitizer.rb index d23b013c1f..c59d53b941 100644 --- a/lib/gitlab/import_url.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -1,7 +1,13 @@ module Gitlab - class ImportUrl + class UrlSanitizer + def self.sanitize(content) + regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git']) + + content.gsub(regexp) { |url| new(url).masked_url } + end + def initialize(url, credentials: nil) - @url = URI.parse(URI.encode(url)) + @url = Addressable::URI.parse(URI.encode(url)) @credentials = credentials end @@ -9,6 +15,13 @@ module Gitlab @sanitized_url ||= safe_url.to_s end + def masked_url + url = @url.dup + url.password = "*****" unless url.password.nil? + url.user = "*****" unless url.user.nil? + url.to_s + end + def credentials @credentials ||= { user: @url.user, password: @url.password } end diff --git a/spec/lib/gitlab/import_url_spec.rb b/spec/lib/gitlab/import_url_spec.rb deleted file mode 100644 index f758cb8693..0000000000 --- a/spec/lib/gitlab/import_url_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ImportUrl do - - let(:credentials) { { user: 'blah', password: 'password' } } - let(:import_url) do - Gitlab::ImportUrl.new("https://github.com/me/project.git", credentials: credentials) - end - - describe :full_url do - it { expect(import_url.full_url).to eq("https://blah:password@github.com/me/project.git") } - end - - describe :sanitized_url do - it { expect(import_url.sanitized_url).to eq("https://github.com/me/project.git") } - end - - describe :credentials do - it { expect(import_url.credentials).to eq(credentials) } - end -end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb new file mode 100644 index 0000000000..de55334118 --- /dev/null +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Gitlab::UrlSanitizer, lib: true do + let(:credentials) { { user: 'blah', password: 'password' } } + let(:url_sanitizer) do + described_class.new("https://github.com/me/project.git", credentials: credentials) + end + + describe '.sanitize' do + def sanitize_url(url) + # We want to try with multi-line content because is how error messages are formatted + described_class.sanitize(%Q{ + remote: Not Found + fatal: repository '#{url}' not found + }) + end + + it 'mask the credentials from HTTP URLs' do + filtered_content = sanitize_url('http://user:pass@test.com/root/repoC.git/') + + expect(filtered_content).to include("http://*****:*****@test.com/root/repoC.git/") + end + + it 'mask the credentials from HTTPS URLs' do + filtered_content = sanitize_url('https://user:pass@test.com/root/repoA.git/') + + expect(filtered_content).to include("https://*****:*****@test.com/root/repoA.git/") + end + + it 'mask credentials from SSH URLs' do + filtered_content = sanitize_url('ssh://user@host.test/path/to/repo.git') + + expect(filtered_content).to include("ssh://*****@host.test/path/to/repo.git") + end + + it 'does not modify Git URLs' do + # git protocol does not support authentication + filtered_content = sanitize_url('git://host.test/path/to/repo.git') + + expect(filtered_content).to include("git://host.test/path/to/repo.git") + end + + it 'does not modify scp-like URLs' do + filtered_content = sanitize_url('user@server:project.git') + + expect(filtered_content).to include("user@server:project.git") + end + end + + describe '#sanitized_url' do + it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") } + end + + describe '#credentials' do + it { expect(url_sanitizer.credentials).to eq(credentials) } + end + + describe '#full_url' do + it { expect(url_sanitizer.full_url).to eq("https://blah:password@github.com/me/project.git") } + + it 'supports scp-like URLs' do + sanitizer = described_class.new('user@server:project.git') + + expect(sanitizer.full_url).to eq('user@server:project.git') + end + end + +end diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index 6739063543..f1b1574abf 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -6,14 +6,28 @@ describe RepositoryImportWorker do subject { described_class.new } describe '#perform' do - it 'imports a project' do - expect_any_instance_of(Projects::ImportService).to receive(:execute). - and_return({ status: :ok }) + context 'when the import was successful' do + it 'imports a project' do + expect_any_instance_of(Projects::ImportService).to receive(:execute). + and_return({ status: :ok }) - expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) - expect_any_instance_of(Project).to receive(:import_finish) + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) + expect_any_instance_of(Project).to receive(:import_finish) - subject.perform(project.id) + subject.perform(project.id) + end + end + + context 'when the import has failed' do + it 'hide the credentials that were used in the import URL' do + error = %Q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } + expect_any_instance_of(Projects::ImportService).to receive(:execute). + and_return({ status: :error, message: error }) + + subject.perform(project.id) + + expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/") + end end end end From 01256eecfa39cf3edc3d0eb43e624f62e45df552 Mon Sep 17 00:00:00 2001 From: Tomasz Maczukin Date: Wed, 18 May 2016 21:34:58 -0500 Subject: [PATCH 343/449] Update 'after_script' introduction note --- doc/ci/yaml/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7e9bced761..63866d8c71 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -128,7 +128,7 @@ builds, including deploy builds. This can be an array or a multi-line string. ### after_script >**Note:** -Introduced in GitLab 8.7 and GitLab Runner v1.2. +Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 (not yet released) `after_script` is used to define the command that will be run after for all builds. This has to be an array or a multi-line string. From 129bb6c2a71c8499daeb5d55f657b0eda8366bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 18 May 2016 23:45:25 -0500 Subject: [PATCH 344/449] Address Yorick's feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CHANGELOG | 2 -- app/models/milestone.rb | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b00cb9064a..3466b98c4c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,8 +2,6 @@ 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) - Snippets tab under user profile. !4001 (Long Nguyen) - Fix error when using link to uploads in global snippets - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 31a54f4445..e0c8454a99 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -67,7 +67,7 @@ class Milestone < ActiveRecord::Base # 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{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: @@ -195,7 +195,7 @@ class Milestone < ActiveRecord::Base private def milestone_format_reference(format = :iid) - raise StandardError, 'Unknown format' unless [:iid, :name].include?(format) + raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format) if format == :name && !name.include?('"') %("#{name}") From e9ecbd3d4e5c1a566c99b6508657ec4be9dcc704 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 19 May 2016 01:35:04 -0500 Subject: [PATCH 345/449] Fix warning messages with SVG ALLOWED_ELEMENTS Eliminates "warning: already initialized constant Gitlab::Sanitizers::SVG::ALLOWED_ELEMENTS" --- lib/gitlab/sanitizers/svg.rb | 8 +- lib/gitlab/sanitizers/svg/whitelist.rb | 170 +++++++++++++------------ 2 files changed, 89 insertions(+), 89 deletions(-) diff --git a/lib/gitlab/sanitizers/svg.rb b/lib/gitlab/sanitizers/svg.rb index b98589dff8..5e95f6c052 100644 --- a/lib/gitlab/sanitizers/svg.rb +++ b/lib/gitlab/sanitizers/svg.rb @@ -1,5 +1,3 @@ -require_relative "svg/whitelist" - module Gitlab module Sanitizers module SVG @@ -12,14 +10,14 @@ module Gitlab DATA_ATTR_PATTERN = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u def scrub(node) - unless ALLOWED_ELEMENTS.include?(node.name) + unless Whitelist::ALLOWED_ELEMENTS.include?(node.name) node.unlink else node.attributes.each do |attr_name, attr| - valid_attributes = ALLOWED_ATTRIBUTES[node.name] + valid_attributes = Whitelist::ALLOWED_ATTRIBUTES[node.name] unless valid_attributes && valid_attributes.include?(attr_name) - if ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name) && + if Whitelist::ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name) && attr_name.start_with?('data-') # Arbitrary data attributes are allowed. Verify that the attribute # is a valid data attribute. diff --git a/lib/gitlab/sanitizers/svg/whitelist.rb b/lib/gitlab/sanitizers/svg/whitelist.rb index 917e795b29..7b6b70d8db 100644 --- a/lib/gitlab/sanitizers/svg/whitelist.rb +++ b/lib/gitlab/sanitizers/svg/whitelist.rb @@ -4,7 +4,8 @@ module Gitlab module Sanitizers module SVG - ALLOWED_ELEMENTS = %w[ + class Whitelist + ALLOWED_ELEMENTS = %w[ a altGlyph altGlyphDef altGlyphItem animate animateColor animateMotion animateTransform circle clipPath color-profile cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer @@ -18,90 +19,91 @@ module Gitlab script set stop style svg switch symbol text textPath title tref tspan use view vkern].freeze - ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze + ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze - ALLOWED_ATTRIBUTES = { - 'a' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage target text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'altGlyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'altGlyphDef' => %w[id xml:base xml:lang xml:space], - 'altGlyphItem' => %w[id xml:base xml:lang xml:space], - 'animate' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'animateColor' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'animateMotion' => %w[accumulate additive begin by calcMode dur end externalResourcesRequired fill from id keyPoints keySplines keyTimes max min onbegin onend onload onrepeat origin path repeatCount repeatDur requiredExtensions requiredFeatures restart rotate systemLanguage to values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'animateTransform' => %w[accumulate additive attributeName attributeType begin by calcMode dur end externalResourcesRequired fill from id keySplines keyTimes max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to type values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'circle' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events r requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'clipPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule clipPathUnits color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'color-profile' => %w[id local name rendering-intent xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'cursor' => %w[externalResourcesRequired id requiredExtensions requiredFeatures systemLanguage x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'defs' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'desc' => %w[class id style xml:base xml:lang xml:space], - 'ellipse' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'feBlend' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask mode opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feColorMatrix' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi values visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feComponentTransfer' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feComposite' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 k1 k2 k3 k4 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feConvolveMatrix' => %w[alignment-baseline baseline-shift bias class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display divisor dominant-baseline edgeMode enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelMatrix kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity order overflow pointer-events preserveAlpha result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style targetX targetY text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feDiffuseLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor diffuseConstant direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feDisplacementMap' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result scale shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xChannelSelector xml:base xml:lang xml:space y yChannelSelector], - 'feDistantLight' => %w[azimuth elevation id xml:base xml:lang xml:space], - 'feFlood' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feFuncA' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feFuncB' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feFuncG' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feFuncR' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feGaussianBlur' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stdDeviation stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feImage' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events preserveAspectRatio result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'feMerge' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feMergeNode' => %w[id xml:base xml:lang xml:space], - 'feMorphology' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events radius result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feOffset' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'fePointLight' => %w[id x xml:base xml:lang xml:space y z], - 'feSpecularLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering specularConstant specularExponent stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feSpotLight' => %w[id limitingConeAngle pointsAtX pointsAtY pointsAtZ specularExponent x xml:base xml:lang xml:space y z], - 'feTile' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feTurbulence' => %w[alignment-baseline baseFrequency baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask numOctaves opacity overflow pointer-events result seed shape-rendering stitchTiles stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'filter' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events primitiveUnits shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'font' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x horiz-origin-y id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'font-face' => %w[accent-height alphabetic ascent bbox cap-height descent font-family font-size font-stretch font-style font-variant font-weight hanging id ideographic mathematical overline-position overline-thickness panose-1 slope stemh stemv strikethrough-position strikethrough-thickness underline-position underline-thickness unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical widths x-height xml:base xml:lang xml:space], - 'font-face-format' => %w[id string xml:base xml:lang xml:space], - 'font-face-name' => %w[id name xml:base xml:lang xml:space], - 'font-face-src' => %w[id xml:base xml:lang xml:space], - 'font-face-uri' => %w[id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'foreignObject' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'g' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'glyph' => %w[alignment-baseline arabic-form baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning lang letter-spacing lighting-color marker-end marker-mid marker-start mask opacity orientation overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'glyphRef' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'hkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space], - 'image' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'line' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode x1 x2 xml:base xml:lang xml:space y1 y2], - 'linearGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x1 x2 xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y1 y2], - 'marker' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask opacity orient overflow pointer-events preserveAspectRatio refX refY shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'mask' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask maskContentUnits maskUnits opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'metadata' => %w[id xml:base xml:lang xml:space], - 'missing-glyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'mpath' => %w[externalResourcesRequired id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'path' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pathLength pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'pattern' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow patternContentUnits patternTransform patternUnits pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi viewBox visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'polygon' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'polyline' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'radialGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events r shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], - 'rect' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'script' => %w[externalResourcesRequired id type xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'set' => %w[attributeName attributeType begin dur end externalResourcesRequired fill id max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'stop' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask offset opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'style' => %w[id media title type xml:base xml:lang xml:space], - 'svg' => %w[alignment-baseline baseProfile baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onabort onactivate onclick onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onresize onscroll onunload onzoom opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi version viewBox visibility width word-spacing writing-mode x xml:base xml:lang xml:space xmlns y zoomAndPan], - 'switch' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'symbol' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'text' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength transform unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], - 'textPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask method onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering spacing startOffset stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], - 'title' => %w[class id style xml:base xml:lang xml:space], - 'tref' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y], - 'tspan' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], - 'use' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'view' => %w[externalResourcesRequired id preserveAspectRatio viewBox viewTarget xml:base xml:lang xml:space zoomAndPan], - 'vkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space] - }.freeze + ALLOWED_ATTRIBUTES = { + 'a' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage target text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'altGlyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'altGlyphDef' => %w[id xml:base xml:lang xml:space], + 'altGlyphItem' => %w[id xml:base xml:lang xml:space], + 'animate' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateColor' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateMotion' => %w[accumulate additive begin by calcMode dur end externalResourcesRequired fill from id keyPoints keySplines keyTimes max min onbegin onend onload onrepeat origin path repeatCount repeatDur requiredExtensions requiredFeatures restart rotate systemLanguage to values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateTransform' => %w[accumulate additive attributeName attributeType begin by calcMode dur end externalResourcesRequired fill from id keySplines keyTimes max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to type values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'circle' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events r requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'clipPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule clipPathUnits color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'color-profile' => %w[id local name rendering-intent xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'cursor' => %w[externalResourcesRequired id requiredExtensions requiredFeatures systemLanguage x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'defs' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'desc' => %w[class id style xml:base xml:lang xml:space], + 'ellipse' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'feBlend' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask mode opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feColorMatrix' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi values visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feComponentTransfer' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feComposite' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 k1 k2 k3 k4 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feConvolveMatrix' => %w[alignment-baseline baseline-shift bias class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display divisor dominant-baseline edgeMode enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelMatrix kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity order overflow pointer-events preserveAlpha result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style targetX targetY text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feDiffuseLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor diffuseConstant direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feDisplacementMap' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result scale shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xChannelSelector xml:base xml:lang xml:space y yChannelSelector], + 'feDistantLight' => %w[azimuth elevation id xml:base xml:lang xml:space], + 'feFlood' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feFuncA' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncB' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncG' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncR' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feGaussianBlur' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stdDeviation stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feImage' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events preserveAspectRatio result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'feMerge' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feMergeNode' => %w[id xml:base xml:lang xml:space], + 'feMorphology' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events radius result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feOffset' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'fePointLight' => %w[id x xml:base xml:lang xml:space y z], + 'feSpecularLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering specularConstant specularExponent stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feSpotLight' => %w[id limitingConeAngle pointsAtX pointsAtY pointsAtZ specularExponent x xml:base xml:lang xml:space y z], + 'feTile' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feTurbulence' => %w[alignment-baseline baseFrequency baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask numOctaves opacity overflow pointer-events result seed shape-rendering stitchTiles stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'filter' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events primitiveUnits shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'font' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x horiz-origin-y id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'font-face' => %w[accent-height alphabetic ascent bbox cap-height descent font-family font-size font-stretch font-style font-variant font-weight hanging id ideographic mathematical overline-position overline-thickness panose-1 slope stemh stemv strikethrough-position strikethrough-thickness underline-position underline-thickness unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical widths x-height xml:base xml:lang xml:space], + 'font-face-format' => %w[id string xml:base xml:lang xml:space], + 'font-face-name' => %w[id name xml:base xml:lang xml:space], + 'font-face-src' => %w[id xml:base xml:lang xml:space], + 'font-face-uri' => %w[id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'foreignObject' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'g' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'glyph' => %w[alignment-baseline arabic-form baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning lang letter-spacing lighting-color marker-end marker-mid marker-start mask opacity orientation overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'glyphRef' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'hkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space], + 'image' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'line' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode x1 x2 xml:base xml:lang xml:space y1 y2], + 'linearGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x1 x2 xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y1 y2], + 'marker' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask opacity orient overflow pointer-events preserveAspectRatio refX refY shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'mask' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask maskContentUnits maskUnits opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'metadata' => %w[id xml:base xml:lang xml:space], + 'missing-glyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'mpath' => %w[externalResourcesRequired id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'path' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pathLength pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'pattern' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow patternContentUnits patternTransform patternUnits pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi viewBox visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'polygon' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'polyline' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'radialGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events r shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], + 'rect' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'script' => %w[externalResourcesRequired id type xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'set' => %w[attributeName attributeType begin dur end externalResourcesRequired fill id max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'stop' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask offset opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'style' => %w[id media title type xml:base xml:lang xml:space], + 'svg' => %w[alignment-baseline baseProfile baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onabort onactivate onclick onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onresize onscroll onunload onzoom opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi version viewBox visibility width word-spacing writing-mode x xml:base xml:lang xml:space xmlns y zoomAndPan], + 'switch' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'symbol' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'text' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength transform unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], + 'textPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask method onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering spacing startOffset stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], + 'title' => %w[class id style xml:base xml:lang xml:space], + 'tref' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y], + 'tspan' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], + 'use' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'view' => %w[externalResourcesRequired id preserveAspectRatio viewBox viewTarget xml:base xml:lang xml:space zoomAndPan], + 'vkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space] + }.freeze + end end end end From 37415e58f84fb3419eb26673e3eba909e1cb63c1 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 18 May 2016 17:38:00 +0100 Subject: [PATCH 346/449] Chunk commits by date in lists It's possible to construct a commit graph where the output of `git log` isn't in timestamp order. Grouping the commits in the list by date then gives dramatically wrong results. Instead, go for the more pragmatic approach: use the commits in the order they're given, and just show the date line each time the date changes. This means that the same date header can show up multiple times, but at least the ordering is preserved. --- CHANGELOG | 2 +- app/views/projects/commits/_commits.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2dd8077954..49c9bbd51f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,7 +43,7 @@ v 8.8.0 (unreleased) - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) - Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724 - Added multiple colors for labels in dropdowns when dups happen. - - Always group commits by server timezone, not commit timestamp + - Show commits in the same order as `git log` - Improve description for the Two-factor Authentication sign-in screen. (Connor Shea) - API support for the 'since' and 'until' operators on commit requests (Paco Guzman) - Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko) diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 82f39e5928..7283a78a64 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -3,7 +3,7 @@ - commits, hidden = limited_commits(@commits) -- commits.group_by { |c| c.committed_date.in_time_zone.to_date }.sort.reverse.each do |day, commits| +- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| .row.commits-row .col-md-2.hidden-xs.hidden-sm %h5.commits-row-date From d46e47a5b637c02136489dd89f7277454a5f8c28 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 19 May 2016 14:09:24 +0000 Subject: [PATCH 347/449] Revert "Merge branch 'issues/15127' into 'master'" This reverts merge request !3647 --- app/models/user.rb | 5 +++++ config/initializers/devise_async.rb | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 02e1f461fa..6a09b78455 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -789,6 +789,11 @@ 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 fa602cbe55..05a1852cdb 100644 --- a/config/initializers/devise_async.rb +++ b/config/initializers/devise_async.rb @@ -1,2 +1 @@ Devise::Async.backend = :sidekiq -Devise::Async.queue = :mailers From 8eb1748ab20efba43f28428799f98dc6807705a3 Mon Sep 17 00:00:00 2001 From: Jazz Date: Tue, 23 Feb 2016 21:30:45 +0000 Subject: [PATCH 348/449] Fixed advice on invalid permissions on upload path --- lib/tasks/gitlab/check.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index effb8eb600..fad89c7376 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -303,7 +303,7 @@ namespace :gitlab do else puts "no".red try_fixing_it( - "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;" + "sudo chmod 700 #{upload_path}" ) for_more_information( see_installation_guide_section "GitLab" From 59640866cee3d80a2144cb1992de36ba6678d4f7 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 19 May 2016 10:04:46 -0500 Subject: [PATCH 349/449] Remove left-over use of strip_heredoc --- lib/gitlab/database/migration_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 74c87fa290..31a87b73fb 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -75,7 +75,7 @@ module Gitlab query += " AND id < #{stop_row['id']}" end - execute(query.strip_heredoc) + execute(query) processed += batch_size end From 7e7764139d171ceeee5200b2677bdab0f8ab7c2d Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 19 May 2016 10:05:39 -0500 Subject: [PATCH 350/449] Fixed typos in migration templates --- lib/templates/active_record/migration/create_table_migration.rb | 2 +- lib/templates/active_record/migration/migration.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/templates/active_record/migration/create_table_migration.rb b/lib/templates/active_record/migration/create_table_migration.rb index 4b7e39e057..27acc75dcc 100644 --- a/lib/templates/active_record/migration/create_table_migration.rb +++ b/lib/templates/active_record/migration/create_table_migration.rb @@ -9,7 +9,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration # existing transaction. When using "add_concurrent_index" make sure that this # method is the _only_ method called in the migration, any other changes # should go in a separate migration. This ensures that upon failure _only_ the - # index creation failes and can be re-tried or reverted easily. + # index creation fails and can be retried or reverted easily. # # To disable transactions uncomment the following line and remove these # comments: diff --git a/lib/templates/active_record/migration/migration.rb b/lib/templates/active_record/migration/migration.rb index ae88593cd1..06bdea1136 100644 --- a/lib/templates/active_record/migration/migration.rb +++ b/lib/templates/active_record/migration/migration.rb @@ -9,7 +9,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration # existing transaction. When using "add_concurrent_index" make sure that this # method is the _only_ method called in the migration, any other changes # should go in a separate migration. This ensures that upon failure _only_ the - # index creation failes and can be re-tried or reverted easily. + # index creation fails and can be retried or reverted easily. # # To disable transactions uncomment the following line and remove these # comments: From 92df8f5787de2a6ee63eb6ae47eb7155081eaa54 Mon Sep 17 00:00:00 2001 From: Ludovic Perrine Date: Thu, 19 May 2016 17:24:55 +0200 Subject: [PATCH 351/449] Added CHANGELOG entry --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index cdb88cafe1..3112164589 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ v 8.8.0 (unreleased) - Import pull requests from GitHub where the source or target branches were removed - All Grape API helpers are now instrumented - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) + - Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine) v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) From c22be75794e0843d1c5de9a15593ccefcc1c09dc Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 19 May 2016 12:36:02 -0500 Subject: [PATCH 352/449] Removed outdated comment from migration helpers --- lib/gitlab/database/migration_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 31a87b73fb..9b662d163f 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -47,7 +47,7 @@ module Gitlab first['count']. to_i - # Update in batches of 5% with an upper limit of 5000 rows. + # Update in batches of 5% batch_size = ((total / 100.0) * 5.0).ceil while processed < total From 9429da3ae2d118ca1edcc20e992d0b2863cadeed Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 19 May 2016 14:58:35 -0500 Subject: [PATCH 353/449] Move generator templates to generator_templates/ Since we eager load everything in lib/ putting ERB code in .rb files will result in syntax errors. By moving the templates to ./generator_templates we can work around this. --- config/application.rb | 4 +++- .../active_record/migration/create_table_migration.rb | 0 .../active_record/migration/migration.rb | 0 3 files changed, 3 insertions(+), 1 deletion(-) rename {lib/templates => generator_templates}/active_record/migration/create_table_migration.rb (100%) rename {lib/templates => generator_templates}/active_record/migration/migration.rb (100%) diff --git a/config/application.rb b/config/application.rb index cba80f38f1..06114bf34e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -26,6 +26,8 @@ module Gitlab #{config.root}/app/models/members #{config.root}/app/models/project_services)) + config.generators.templates.push("#{config.root}/generator_templates") + # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. # config.plugins = [ :exception_notification, :ssl_requirement, :all ] @@ -39,7 +41,7 @@ module Gitlab config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - # + # # Parameters filtered: # - Password (:password, :password_confirmation) # - Private tokens (:private_token) diff --git a/lib/templates/active_record/migration/create_table_migration.rb b/generator_templates/active_record/migration/create_table_migration.rb similarity index 100% rename from lib/templates/active_record/migration/create_table_migration.rb rename to generator_templates/active_record/migration/create_table_migration.rb diff --git a/lib/templates/active_record/migration/migration.rb b/generator_templates/active_record/migration/migration.rb similarity index 100% rename from lib/templates/active_record/migration/migration.rb rename to generator_templates/active_record/migration/migration.rb From d0032935a1d720c8e3cfd2597667c62a0c4015a1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 4 May 2016 14:34:11 +0200 Subject: [PATCH 354/449] Add runner db field for ability to run untagged jobs --- .../20160504112519_add_run_untagged_to_ci_runner.rb | 11 +++++++++++ db/schema.rb | 1 + 2 files changed, 12 insertions(+) create mode 100644 db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb diff --git a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb new file mode 100644 index 0000000000..38fee17f90 --- /dev/null +++ b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb @@ -0,0 +1,11 @@ +class AddRunUntaggedToCiRunner < ActiveRecord::Migration + ## + # Downtime expected! + # + # This migration will cause downtime due to exclusive lock + # caused by the default value. + # + def change + add_column :ci_runners, :run_untagged, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index af4f4c609e..5b9fbe79fa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -269,6 +269,7 @@ ActiveRecord::Schema.define(version: 20160509201028) do t.string "revision" t.string "platform" t.string "architecture" + t.boolean "run_untagged", default: true end add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} From a8a205cd5c3f2840e7e8eab16bceff608c288f33 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 4 May 2016 14:40:03 +0200 Subject: [PATCH 355/449] Add form for runner config to run untagged jobs --- app/views/projects/runners/_form.html.haml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index 2d6c964ae9..147f1a2ebc 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -5,6 +5,12 @@ .checkbox = f.check_box :active %span.light Paused runners don't accept new builds + .form-group + = label :run_untagged, 'Run untagged jobs', class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :run_untagged + %span.light This runner can pick jobs without tags .form-group = label_tag :token, class: 'control-label' do Token From 2aa57071947e562f302b3858a249339024ef7119 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 5 May 2016 12:20:33 +0200 Subject: [PATCH 356/449] Extend runner config options for untagged jobs --- app/models/ci/runner.rb | 2 +- app/views/admin/runners/show.html.haml | 2 - app/views/projects/runners/_runner.html.haml | 2 +- app/views/projects/runners/show.html.haml | 51 ++++++++------------ spec/features/runners_spec.rb | 29 +++++++++++ 5 files changed, 51 insertions(+), 35 deletions(-) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 819064f99b..23b1556712 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -4,7 +4,7 @@ module Ci LAST_CONTACT_TIME = 5.minutes.ago AVAILABLE_SCOPES = %w[specific shared active paused online] - FORM_EDITABLE = %i[description tag_list active] + FORM_EDITABLE = %i[description tag_list active run_untagged] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 4dfb3ed05b..c3784bf719 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -9,8 +9,6 @@ %span.runner-state.runner-state-specific Specific - - - if @runner.shared? .bs-callout.bs-callout-success %h4 This runner will process builds from ALL UNASSIGNED projects diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 47ec420189..96e2aac451 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -5,7 +5,7 @@ - if @runners.include?(runner) = link_to runner.short_sha, runner_path(runner) %small - =link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do + = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do %i.fa.fa-edit.btn - else = runner.short_sha diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index 5bf4c09ca2..f24e1b9144 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -17,50 +17,39 @@ %th Property Name %th Value %tr - %td - Tags + %td Active + %td= @runner.active? ? 'Yes' : 'No' + %tr + %td Can run untagged jobs + %td= @runner.run_untagged? ? 'Yes' : 'No' + %tr + %td Tags %td - @runner.tag_list.each do |tag| %span.label.label-primary = tag %tr - %td - Name - %td - = @runner.name + %td Name + %td= @runner.name %tr - %td - Version - %td - = @runner.version + %td Version + %td= @runner.version %tr - %td - Revision - %td - = @runner.revision + %td Revision + %td= @runner.revision %tr - %td - Platform - %td - = @runner.platform + %td Platform + %td= @runner.platform %tr - %td - Architecture - %td - = @runner.architecture + %td Architecture + %td= @runner.architecture %tr - %td - Description - %td - = @runner.description + %td Description + %td= @runner.description %tr - %td - Last contact + %td Last contact %td - if @runner.contacted_at #{time_ago_in_words(@runner.contacted_at)} ago - else Never - - - diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 8edeb8d18a..4f942c3ab1 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -110,4 +110,33 @@ describe "Runners" do expect(page).to have_content(@specific_runner.platform) end end + + feature 'configuring runners ability to picking untagged jobs' do + given(:project) { create(:empty_project) } + given(:runner) { create(:ci_runner) } + + background do + project.team << [user, :master] + project.runners << runner + end + + scenario 'user checks default configuration' do + visit namespace_project_runner_path(project.namespace, project, runner) + + expect(page).to have_content 'Can run untagged jobs Yes' + end + + scenario 'user want to prevent runner from running untagged job' do + visit runners_path(project) + page.within('.activated-specific-runners') do + first('small > a').click + end + + uncheck 'runner_run_untagged' + click_button 'Save changes' + + expect(page).to have_content 'Can run untagged jobs No' + expect(runner.reload.run_untagged?).to eq false + end + end end From 83df6384558c27d3ff7282e6d66b06fa7e9c0c60 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 5 May 2016 13:08:17 +0200 Subject: [PATCH 357/449] Disallow runner to pick untagged build if configured --- app/models/ci/build.rb | 4 ++++ spec/models/build_spec.rb | 38 +++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 92327bdb08..77a2dec4f7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -290,6 +290,10 @@ module Ci end def can_be_served?(runner) + if tag_list.empty? && !runner.run_untagged? + return false + end + (tag_list - runner.tag_list).empty? end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index b5d356aa06..5da54e07de 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -259,11 +259,11 @@ describe Ci::Build, models: true do end describe '#can_be_served?' do - let(:runner) { FactoryGirl.create :ci_runner } + let(:runner) { create(:ci_runner) } before { build.project.runners << runner } - context 'runner without tags' do + context 'when runner does not have tags' do it 'can handle builds without tags' do expect(build.can_be_served?(runner)).to be_truthy end @@ -274,21 +274,37 @@ describe Ci::Build, models: true do end end - context 'runner with tags' do + context 'when runner has tags' do before { runner.tag_list = ['bb', 'cc'] } - it 'can handle builds without tags' do - expect(build.can_be_served?(runner)).to be_truthy + shared_examples 'tagged build picker' do + it 'can handle build with matching tags' do + build.tag_list = ['bb'] + expect(build.can_be_served?(runner)).to be_truthy + end + + it 'cannot handle build without matching tags' do + build.tag_list = ['aa'] + expect(build.can_be_served?(runner)).to be_falsey + end end - it 'can handle build with matching tags' do - build.tag_list = ['bb'] - expect(build.can_be_served?(runner)).to be_truthy + context 'when runner can pick untagged jobs' do + it 'can handle builds without tags' do + expect(build.can_be_served?(runner)).to be_truthy + end + + it_behaves_like 'tagged build picker' end - it 'cannot handle build with not matching tags' do - build.tag_list = ['aa'] - expect(build.can_be_served?(runner)).to be_falsey + context 'when runner can not pick untagged jobs' do + before { runner.run_untagged = false } + + it 'can not handle builds without tags' do + expect(build.can_be_served?(runner)).to be_falsey + end + + it_behaves_like 'tagged build picker' end end end From 9129c37c5bd6e648e23ebe6847b909dd151c7e8a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 5 May 2016 13:28:44 +0200 Subject: [PATCH 358/449] Add CI API tests for runner config and untagged jobs --- spec/requests/ci/api/builds_spec.rb | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index cae4656010..7ebf8e41f3 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -128,6 +128,38 @@ describe Ci::API::API do end end end + + context 'when build has no tags' do + before do + commit = create(:ci_commit, project: project) + create(:ci_build, commit: commit, tags: []) + end + + context 'when runner is allowed to pick untagged builds' do + before { runner.update_column(:run_untagged, true) } + + it 'picks build' do + register_builds + + expect(response).to have_http_status 201 + end + end + + context 'when runner is not allowed to pick untagged builds' do + before { runner.update_column(:run_untagged, false) } + + it 'does not pick build' do + register_builds + + expect(response).to have_http_status 404 + end + end + + def register_builds + post ci_api("/builds/register"), token: runner.token, + info: { platform: :darwin } + end + end end describe "PUT /builds/:id" do From dd8e9e2d7acf05b837a3bacd1258e9f3df4254a6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 5 May 2016 14:27:06 +0200 Subject: [PATCH 359/449] Add custom validator to runner model --- app/controllers/projects/runners_controller.rb | 3 ++- app/models/ci/runner.rb | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 3a9d67aff6..12172b4752 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -20,7 +20,8 @@ class Projects::RunnersController < Projects::ApplicationController if @runner.update_attributes(runner_params) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else - redirect_to runner_path(@runner), alert: 'Runner was not updated.' + flash[:alert] = @runner.errors.full_messages.to_sentence + render 'edit' end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 23b1556712..b1227d7240 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -26,6 +26,13 @@ module Ci .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) end + validate do |runner| + if runner.tag_list.empty? && !runner.run_untagged? + errors.add(:tags_errors, + 'Runner without tags must be able to pick untagged jobs!') + end + end + acts_as_taggable # Searches for runners matching the given query. From 2ee24bd9ece394269c006f9762d3fa5e16876949 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 May 2016 08:41:51 +0200 Subject: [PATCH 360/449] Update specs to be valid only for tagged runner --- spec/features/runners_spec.rb | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 4f942c3ab1..2c9c12b4df 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -126,17 +126,21 @@ describe "Runners" do expect(page).to have_content 'Can run untagged jobs Yes' end - scenario 'user want to prevent runner from running untagged job' do - visit runners_path(project) - page.within('.activated-specific-runners') do - first('small > a').click + context 'when runner has tags' do + before { runner.update_attribute(:tag_list, ['tag']) } + + scenario 'user want to prevent runner from running untagged job' do + visit runners_path(project) + page.within('.activated-specific-runners') do + first('small > a').click + end + + uncheck 'runner_run_untagged' + click_button 'Save changes' + + expect(page).to have_content 'Can run untagged jobs No' + expect(runner.reload.run_untagged?).to eq false end - - uncheck 'runner_run_untagged' - click_button 'Save changes' - - expect(page).to have_content 'Can run untagged jobs No' - expect(runner.reload.run_untagged?).to eq false end end end From 9fd6f1b6009410cee0d0d7db8703ad64701db62b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 May 2016 08:47:19 +0200 Subject: [PATCH 361/449] Improve displaying validation messages for runner --- app/controllers/projects/runners_controller.rb | 1 - app/models/ci/runner.rb | 10 +++++++--- app/views/projects/runners/edit.html.haml | 6 ++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 12172b4752..0b4fa57250 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -20,7 +20,6 @@ class Projects::RunnersController < Projects::ApplicationController if @runner.update_attributes(runner_params) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else - flash[:alert] = @runner.errors.full_messages.to_sentence render 'edit' end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index b1227d7240..f581358949 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -27,9 +27,9 @@ module Ci end validate do |runner| - if runner.tag_list.empty? && !runner.run_untagged? - errors.add(:tags_errors, - 'Runner without tags must be able to pick untagged jobs!') + unless runner.has_tags? || runner.run_untagged? + errors.add(:tags_list, + 'can not be empty when runner is not allowed to pick untagged jobs') end end @@ -103,5 +103,9 @@ module Ci def short_sha token[0...8] if token end + + def has_tags? + tag_list.any? + end end end diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index 771947d790..a5cfa9a8da 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -1,5 +1,11 @@ - page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners" %h4 Runner ##{@runner.id} + +- if @runner.errors.any? + .error-message.js-errors + - @runner.errors.full_messages.each do |error| + %div= error + %hr = render 'form', runner: @runner, runner_form_url: runner_path(@runner) From 8252333183dcff74fd229ca81e7317641ad30791 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 May 2016 09:04:09 +0200 Subject: [PATCH 362/449] Extend CI runners specs --- spec/models/ci/runner_spec.rb | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index eaa9422892..7c6e39419e 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -1,6 +1,24 @@ require 'spec_helper' describe Ci::Runner, models: true do + describe 'validation' do + context 'when runner is not allowed to pick untagged jobs' do + context 'when runner does not have tags' do + it 'is not valid' do + runner = build(:ci_runner, tag_list: [], run_untagged: false) + expect(runner).to be_invalid + end + end + + context 'when runner has tags' do + it 'is valid' do + runner = build(:ci_runner, tag_list: ['tag'], run_untagged: false) + expect(runner).to be_valid + end + end + end + end + describe '#display_name' do it 'should return the description if it has a value' do runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') @@ -114,7 +132,19 @@ describe Ci::Runner, models: true do end end - describe '#search' do + describe '#has_tags?' do + context 'when runner has tags' do + subject { create(:ci_runner, tag_list: ['tag']) } + it { is_expected.to have_tags } + end + + context 'when runner does not have tags' do + subject { create(:ci_runner, tag_list: []) } + it { is_expected.to_not have_tags } + end + end + + describe '.search' do let(:runner) { create(:ci_runner, token: '123abc') } it 'returns runners with a matching token' do From 8001eed06e5871f1cb108ceb70213c8b3f9192d2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 May 2016 09:17:27 +0200 Subject: [PATCH 363/449] Add method that check if build has tags --- app/models/ci/build.rb | 8 +++++--- spec/models/build_spec.rb | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 77a2dec4f7..6c9ce0dc48 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -290,13 +290,15 @@ module Ci end def can_be_served?(runner) - if tag_list.empty? && !runner.run_untagged? - return false - end + return false unless has_tags? || runner.run_untagged? (tag_list - runner.tag_list).empty? end + def has_tags? + tag_list.any? + end + def any_runners_online? project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) } end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 5da54e07de..abae3271a5 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -309,6 +309,18 @@ describe Ci::Build, models: true do end end + describe '#has_tags?' do + context 'when build has tags' do + subject { create(:ci_build, tag_list: ['tag']) } + it { is_expected.to have_tags } + end + + context 'when build does not have tags' do + subject { create(:ci_build, tag_list: []) } + it { is_expected.to_not have_tags } + end + end + describe '#any_runners_online?' do subject { build.any_runners_online? } From 7b607cf4f0edda45e864c07b468b0b0819f974df Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 May 2016 09:58:17 +0200 Subject: [PATCH 364/449] Add Changelog entry for new runner configuration --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 422718dec9..e8987000f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 8.8.0 (unreleased) - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) - Use a case-insensitive comparison in sanitizing URI schemes - Toggle sign-up confirmation emails in application settings + - Make it possible to prevent tagged runner from picking untagged jobs - Project#open_branches has been cleaned up and no longer loads entire records into memory. - Escape HTML in commit titles in system note messages - Improve multiple branch push performance by memoizing permission checking From da8b72d45300db3339925cf34800b6d17828582f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 May 2016 12:32:17 +0200 Subject: [PATCH 365/449] Extend runner options that are configurable via API --- lib/api/entities.rb | 1 + lib/api/runners.rb | 2 +- spec/requests/api/runners_spec.rb | 15 +++++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index dbd03ea74f..8298e3ad34 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -408,6 +408,7 @@ module API class RunnerDetails < Runner expose :tag_list + expose :run_untagged expose :version, :revision, :platform, :architecture expose :contacted_at expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? } diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 8ec91485b2..4faba9dc87 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -49,7 +49,7 @@ module API runner = get_runner(params[:id]) authenticate_update_runner!(runner) - attrs = attributes_for_keys [:description, :active, :tag_list] + attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged] if runner.update(attrs) present runner, with: Entities::RunnerDetails, current_user: current_user else diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 3af61d4b33..73ae8ef631 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -184,21 +184,24 @@ describe API::Runners, api: true do description = shared_runner.description active = shared_runner.active - put api("/runners/#{shared_runner.id}", admin), description: "#{description}_updated", active: !active, - tag_list: ['ruby2.1', 'pgsql', 'mysql'] + update_runner(shared_runner.id, admin, description: "#{description}_updated", + active: !active, + tag_list: ['ruby2.1', 'pgsql', 'mysql'], + run_untagged: 'false') shared_runner.reload expect(response.status).to eq(200) expect(shared_runner.description).to eq("#{description}_updated") expect(shared_runner.active).to eq(!active) expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') + expect(shared_runner.run_untagged?).to be false end end context 'when runner is not shared' do it 'should update runner' do description = specific_runner.description - put api("/runners/#{specific_runner.id}", admin), description: 'test' + update_runner(specific_runner.id, admin, description: 'test') specific_runner.reload expect(response.status).to eq(200) @@ -208,10 +211,14 @@ describe API::Runners, api: true do end it 'should return 404 if runner does not exists' do - put api('/runners/9999', admin), description: 'test' + update_runner(9999, admin, description: 'test') expect(response.status).to eq(404) end + + def update_runner(id, user, args) + put api("/runners/#{id}", user), args + end end context 'authorized user' do From b253aa32c75cb5b0796ad6a7cd85cba78516ba26 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 May 2016 12:44:20 +0200 Subject: [PATCH 366/449] Add docs for a new configuration setting for runner --- doc/ci/runners/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index a06650b338..e449d3dc61 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -125,7 +125,13 @@ shared runners will only run the jobs they are equipped to run. For instance, at GitLab we have runners tagged with "rails" if they contain the appropriate dependencies to run Rails test suites. -### Be Careful with Sensitive Information +### Prevent runner with tags from picking jobs without tags + +You can configure runner to prevent it from picking jobs with tags when +runnner does not have tags assigned. This configuration setting is available +in GitLab interface when editting runner details. + +### Be careful with sensitive information If you can run a build on a runner, you can get access to any code it runs and get the token of the runner. With shared runners, this means that anyone From 9ba72378fc006ecd353e1447a50d2231df09c851 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 May 2016 13:56:22 +0200 Subject: [PATCH 367/449] Refactor CI API specs for creating runner --- spec/requests/ci/api/runners_spec.rb | 46 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index db8189ffb7..5d6f5f774e 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -12,44 +12,56 @@ describe Ci::API::API do end describe "POST /runners/register" do - describe "should create a runner if token provided" do + context 'when runner token is provided' do before { post ci_api("/runners/register"), token: registration_token } - it { expect(response.status).to eq(201) } + it 'creates runner' do + expect(response.status).to eq(201) + end end - describe "should create a runner with description" do + context 'when runner description is provided' do before { post ci_api("/runners/register"), token: registration_token, description: "server.hostname" } - it { expect(response.status).to eq(201) } - it { expect(Ci::Runner.first.description).to eq("server.hostname") } + it 'creates runner' do + expect(response.status).to eq(201) + expect(Ci::Runner.first.description).to eq("server.hostname") + end end - describe "should create a runner with tags" do + context 'when runner tags are provided' do before { post ci_api("/runners/register"), token: registration_token, tag_list: "tag1, tag2" } - it { expect(response.status).to eq(201) } - it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) } + it 'creates runner' do + expect(response.status).to eq(201) + expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) + end end - describe "should create a runner if project token provided" do + context 'when project token is provided' do let(:project) { FactoryGirl.create(:empty_project) } before { post ci_api("/runners/register"), token: project.runners_token } - it { expect(response.status).to eq(201) } - it { expect(project.runners.size).to eq(1) } + it 'creates runner' do + expect(response.status).to eq(201) + expect(project.runners.size).to eq(1) + end end - it "should return 403 error if token is invalid" do - post ci_api("/runners/register"), token: 'invalid' + context 'when token is invalid' do + it 'returns 403 error' do + post ci_api("/runners/register"), token: 'invalid' - expect(response.status).to eq(403) + expect(response.status).to eq(403) + end end - it "should return 400 error if no token" do - post ci_api("/runners/register") + context 'when no token provided' do + it 'returns 400 error' do + post ci_api("/runners/register") - expect(response.status).to eq(400) + expect(response.status).to eq(400) + end end %w(name version revision platform architecture).each do |param| From b8cf2a340b3c56eb7e226473034ead2c4e5d609a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 May 2016 14:06:57 +0200 Subject: [PATCH 368/449] Set run untagged option when registering a runner --- lib/ci/api/runners.rb | 15 +++++------- spec/requests/ci/api/runners_spec.rb | 35 +++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 192b1d18a5..ea35d0f6dd 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -28,20 +28,17 @@ module Ci post "register" do required_attributes! [:token] + attributes = { description: params[:description], + tag_list: params[:tag_list], + run_untagged: params[:run_untagged] || true } + runner = if runner_registration_token_valid? # Create shared runner. Requires admin access - Ci::Runner.create( - description: params[:description], - tag_list: params[:tag_list], - is_shared: true - ) + Ci::Runner.create(attributes.merge(is_shared: true)) elsif project = Project.find_by(runners_token: params[:token]) # Create a specific runner for project. - project.runners.create( - description: params[:description], - tag_list: params[:tag_list] - ) + project.runners.create(attributes) end return forbidden! unless runner diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 5d6f5f774e..eb11258cbb 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -15,13 +15,17 @@ describe Ci::API::API do context 'when runner token is provided' do before { post ci_api("/runners/register"), token: registration_token } - it 'creates runner' do + it 'creates runner with default values' do expect(response.status).to eq(201) + expect(Ci::Runner.first.run_untagged).to be true end end context 'when runner description is provided' do - before { post ci_api("/runners/register"), token: registration_token, description: "server.hostname" } + before do + post ci_api("/runners/register"), token: registration_token, + description: "server.hostname" + end it 'creates runner' do expect(response.status).to eq(201) @@ -30,7 +34,10 @@ describe Ci::API::API do end context 'when runner tags are provided' do - before { post ci_api("/runners/register"), token: registration_token, tag_list: "tag1, tag2" } + before do + post ci_api("/runners/register"), token: registration_token, + tag_list: "tag1, tag2" + end it 'creates runner' do expect(response.status).to eq(201) @@ -38,6 +45,28 @@ describe Ci::API::API do end end + context 'when option for running untagged jobs is provided' do + context 'when tags are provided' do + it 'creates runner' do + post ci_api("/runners/register"), token: registration_token, + run_untagged: false, + tag_list: ['tag'] + + expect(response.status).to eq(201) + expect(Ci::Runner.first.run_untagged).to be false + end + end + + context 'when tags are not provided' do + it 'does not create runner' do + post ci_api("/runners/register"), token: registration_token, + run_untagged: false + + expect(response.status).to eq(404) + end + end + end + context 'when project token is provided' do let(:project) { FactoryGirl.create(:empty_project) } before { post ci_api("/runners/register"), token: project.runners_token } From 3b0eeccc0324e2d6c024fd94067933314e45b862 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 7 May 2016 20:23:36 +0200 Subject: [PATCH 369/449] Add not null constraint to run untagged runner option --- db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb | 2 +- db/schema.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb index 38fee17f90..c6753f2b39 100644 --- a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb +++ b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb @@ -6,6 +6,6 @@ class AddRunUntaggedToCiRunner < ActiveRecord::Migration # caused by the default value. # def change - add_column :ci_runners, :run_untagged, :boolean, default: true + add_column :ci_runners, :run_untagged, :boolean, default: true, null: false end end diff --git a/db/schema.rb b/db/schema.rb index 5b9fbe79fa..2e154b98b3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -269,7 +269,7 @@ ActiveRecord::Schema.define(version: 20160509201028) do t.string "revision" t.string "platform" t.string "architecture" - t.boolean "run_untagged", default: true + t.boolean "run_untagged", default: true, null: false end add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} From 8ab4a67ca6a9166168b744bc940da98bf9651efd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 7 May 2016 20:35:08 +0200 Subject: [PATCH 370/449] Use form errors helper in CI runner edit form --- app/views/projects/runners/edit.html.haml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index a5cfa9a8da..e4aa98080e 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -2,10 +2,7 @@ %h4 Runner ##{@runner.id} -- if @runner.errors.any? - .error-message.js-errors - - @runner.errors.full_messages.each do |error| - %div= error += form_errors(@runner) %hr = render 'form', runner: @runner, runner_form_url: runner_path(@runner) From f6dd8a5257bf71f87ea9a2fa2f3d16012e61b6e4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 7 May 2016 20:36:03 +0200 Subject: [PATCH 371/449] Move runner validator to separate private method --- app/models/ci/runner.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index f581358949..cf4d323651 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -26,12 +26,7 @@ module Ci .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) end - validate do |runner| - unless runner.has_tags? || runner.run_untagged? - errors.add(:tags_list, - 'can not be empty when runner is not allowed to pick untagged jobs') - end - end + validate :verify_tags_constraints acts_as_taggable @@ -107,5 +102,14 @@ module Ci def has_tags? tag_list.any? end + + private + + def verify_tags_constraints + unless has_tags? || run_untagged? + errors.add(:tags_list, + 'can not be empty when runner is not allowed to pick untagged jobs') + end + end end end From 0fd100d28d3748de90aabc3dbbb789e37399f224 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 7 May 2016 20:42:36 +0200 Subject: [PATCH 372/449] Improve setting default runner attrs when using API --- lib/ci/api/runners.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index ea35d0f6dd..1dc4c8c2cd 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -29,8 +29,11 @@ module Ci required_attributes! [:token] attributes = { description: params[:description], - tag_list: params[:tag_list], - run_untagged: params[:run_untagged] || true } + tag_list: params[:tag_list] } + + unless params[:run_untagged].nil? + attributes.merge!(run_untagged: params[:run_untagged]) + end runner = if runner_registration_token_valid? From bf9cc351c28a349ca4c573978c869d2b90209d52 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 10 May 2016 13:19:25 +0200 Subject: [PATCH 373/449] Add minor corrections related to config of runner --- app/views/projects/runners/_form.html.haml | 3 ++- app/views/projects/runners/edit.html.haml | 2 -- doc/ci/runners/README.md | 6 +++--- lib/ci/api/runners.rb | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index 147f1a2ebc..6be406fa18 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -1,4 +1,5 @@ = form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f| + = form_errors(@runner) .form-group = label :active, "Active", class: 'control-label' .col-sm-10 @@ -10,7 +11,7 @@ .col-sm-10 .checkbox = f.check_box :run_untagged - %span.light This runner can pick jobs without tags + %span.light Indicates whether runner can pick jobs without tags .form-group = label_tag :token, class: 'control-label' do Token diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index e4aa98080e..9570688865 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -2,7 +2,5 @@ %h4 Runner ##{@runner.id} -= form_errors(@runner) - %hr = render 'form', runner: @runner, runner_form_url: runner_path(@runner) diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index e449d3dc61..b42d7a62eb 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -127,9 +127,9 @@ the appropriate dependencies to run Rails test suites. ### Prevent runner with tags from picking jobs without tags -You can configure runner to prevent it from picking jobs with tags when -runnner does not have tags assigned. This configuration setting is available -in GitLab interface when editting runner details. +You can configure a runner to prevent it from picking jobs with tags when +the runnner does not have tags assigned. This setting is available on each +runner in *Project Settings* > *Runners*. ### Be careful with sensitive information diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 1dc4c8c2cd..0c41f22c7c 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -32,7 +32,7 @@ module Ci tag_list: params[:tag_list] } unless params[:run_untagged].nil? - attributes.merge!(run_untagged: params[:run_untagged]) + attributes[:run_untagged] = params[:run_untagged] end runner = From 52ba3a2d05ab93caa5ddbc6207359e99301dda91 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 18 May 2016 17:23:26 +0200 Subject: [PATCH 374/449] Display validation errors when admin edits a runner --- app/controllers/admin/runners_controller.rb | 26 +++++++++++++-------- app/views/projects/runners/_form.html.haml | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 8b8a732007..a164209455 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -9,19 +9,13 @@ class Admin::RunnersController < Admin::ApplicationController end def show - @builds = @runner.builds.order('id DESC').first(30) - @projects = - if params[:search].present? - ::Project.search(params[:search]) - else - Project.all - end - @projects = @projects.where.not(id: @runner.projects.select(:id)) if @runner.projects.any? - @projects = @projects.page(params[:page]).per(30) + set_builds_and_projects end def update - @runner.update_attributes(runner_params) + unless @runner.update_attributes(runner_params) + set_builds_and_projects and return render 'show' + end respond_to do |format| format.js @@ -60,4 +54,16 @@ class Admin::RunnersController < Admin::ApplicationController def runner_params params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end + + def set_builds_and_projects + @builds = runner.builds.order('id DESC').first(30) + @projects = + if params[:search].present? + ::Project.search(params[:search]) + else + Project.all + end + @projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any? + @projects = @projects.page(params[:page]).per(30) + end end diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index 6be406fa18..b8fe0a9810 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -1,5 +1,5 @@ = form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f| - = form_errors(@runner) + = form_errors(runner) .form-group = label :active, "Active", class: 'control-label' .col-sm-10 From c3c503d259bbf4691f0fb24fcd713ec5b4474e61 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 18 May 2016 18:38:21 +0200 Subject: [PATCH 375/449] Rename method that validates runner tag constrains --- app/models/ci/runner.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index cf4d323651..6829dc91cb 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -26,7 +26,7 @@ module Ci .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) end - validate :verify_tags_constraints + validate :tag_constraints acts_as_taggable @@ -105,7 +105,7 @@ module Ci private - def verify_tags_constraints + def tag_constraints unless has_tags? || run_untagged? errors.add(:tags_list, 'can not be empty when runner is not allowed to pick untagged jobs') From 4cc77c3bf8ef72d1b08160da9f55eb1c5f95e832 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 19 May 2016 21:27:52 +0200 Subject: [PATCH 376/449] Minor runner-related code refactorings --- app/controllers/admin/runners_controller.rb | 19 ++++++++++--------- app/views/projects/runners/_form.html.haml | 2 +- spec/features/runners_spec.rb | 2 +- spec/requests/ci/api/runners_spec.rb | 20 ++++++++++---------- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index a164209455..7345c91f67 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -9,17 +9,18 @@ class Admin::RunnersController < Admin::ApplicationController end def show - set_builds_and_projects + assign_builds_and_projects end def update - unless @runner.update_attributes(runner_params) - set_builds_and_projects and return render 'show' - end - - respond_to do |format| - format.js - format.html { redirect_to admin_runner_path(@runner) } + if @runner.update_attributes(runner_params) + respond_to do |format| + format.js + format.html { redirect_to admin_runner_path(@runner) } + end + else + assign_builds_and_projects + render 'show' end end @@ -55,7 +56,7 @@ class Admin::RunnersController < Admin::ApplicationController params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end - def set_builds_and_projects + def assign_builds_and_projects @builds = runner.builds.order('id DESC').first(30) @projects = if params[:search].present? diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index b8fe0a9810..d62f5c8f13 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -11,7 +11,7 @@ .col-sm-10 .checkbox = f.check_box :run_untagged - %span.light Indicates whether runner can pick jobs without tags + %span.light Indicates whether this runner can pick jobs without tags .form-group = label_tag :token, class: 'control-label' do Token diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 2c9c12b4df..49156130ad 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -129,7 +129,7 @@ describe "Runners" do context 'when runner has tags' do before { runner.update_attribute(:tag_list, ['tag']) } - scenario 'user want to prevent runner from running untagged job' do + scenario 'user wants to prevent runner from running untagged job' do visit runners_path(project) page.within('.activated-specific-runners') do first('small > a').click diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index eb11258cbb..43596f07cb 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -16,7 +16,7 @@ describe Ci::API::API do before { post ci_api("/runners/register"), token: registration_token } it 'creates runner with default values' do - expect(response.status).to eq(201) + expect(response).to have_http_status 201 expect(Ci::Runner.first.run_untagged).to be true end end @@ -28,7 +28,7 @@ describe Ci::API::API do end it 'creates runner' do - expect(response.status).to eq(201) + expect(response).to have_http_status 201 expect(Ci::Runner.first.description).to eq("server.hostname") end end @@ -40,7 +40,7 @@ describe Ci::API::API do end it 'creates runner' do - expect(response.status).to eq(201) + expect(response).to have_http_status 201 expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) end end @@ -52,7 +52,7 @@ describe Ci::API::API do run_untagged: false, tag_list: ['tag'] - expect(response.status).to eq(201) + expect(response).to have_http_status 201 expect(Ci::Runner.first.run_untagged).to be false end end @@ -62,7 +62,7 @@ describe Ci::API::API do post ci_api("/runners/register"), token: registration_token, run_untagged: false - expect(response.status).to eq(404) + expect(response).to have_http_status 404 end end end @@ -72,7 +72,7 @@ describe Ci::API::API do before { post ci_api("/runners/register"), token: project.runners_token } it 'creates runner' do - expect(response.status).to eq(201) + expect(response).to have_http_status 201 expect(project.runners.size).to eq(1) end end @@ -81,7 +81,7 @@ describe Ci::API::API do it 'returns 403 error' do post ci_api("/runners/register"), token: 'invalid' - expect(response.status).to eq(403) + expect(response).to have_http_status 403 end end @@ -89,7 +89,7 @@ describe Ci::API::API do it 'returns 400 error' do post ci_api("/runners/register") - expect(response.status).to eq(400) + expect(response).to have_http_status 400 end end @@ -101,7 +101,7 @@ describe Ci::API::API do it do post ci_api("/runners/register"), token: registration_token, info: { param => value } - expect(response.status).to eq(201) + expect(response).to have_http_status 201 is_expected.to eq(value) end end @@ -112,7 +112,7 @@ describe Ci::API::API do let!(:runner) { FactoryGirl.create(:ci_runner) } before { delete ci_api("/runners/delete"), token: runner.token } - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status 200 } it { expect(Ci::Runner.count).to eq(0) } end end From 52c8b9da37451943fe97f3a687d43ae39105aaa0 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 19 May 2016 22:11:40 +0200 Subject: [PATCH 377/449] Use migration helper to prevent downtime migration --- ...0504112519_add_run_untagged_to_ci_runner.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb index c6753f2b39..84e5e4eabe 100644 --- a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb +++ b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb @@ -1,11 +1,13 @@ class AddRunUntaggedToCiRunner < ActiveRecord::Migration - ## - # Downtime expected! - # - # This migration will cause downtime due to exclusive lock - # caused by the default value. - # - def change - add_column :ci_runners, :run_untagged, :boolean, default: true, null: false + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_column_with_default(:ci_runners, :run_untagged, :boolean, + default: true, allow_null: false) + end + + def down + remove_column(:ci_runners, :run_untagged) end end From e3aaab2d266610e85a452df74bd41e75e86b8df3 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 19 May 2016 15:32:07 -0500 Subject: [PATCH 378/449] Updated Rubocop for generator_templates/ --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 2d2055d76b..0946ef5d84 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -21,7 +21,7 @@ AllCops: - 'lib/email_validator.rb' - 'lib/gitlab/upgrader.rb' - 'lib/gitlab/seeder.rb' - - 'lib/templates/**/*' + - 'generator_templates/**/*' ##################### Style ################################## From b876993677edde4fee3a8ed349c522f1542190f7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 19 May 2016 13:48:57 -0500 Subject: [PATCH 379/449] Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios --- CHANGELOG | 1 + app/services/create_commit_builds_service.rb | 17 +++++++---------- spec/workers/post_receive_spec.rb | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9613332774..d0f3d03732 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.8.0 (unreleased) - Toggle sign-up confirmation emails in application settings - Project#open_branches has been cleaned up and no longer loads entire records into memory. - Escape HTML in commit titles in system note messages + - Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios - Improve multiple branch push performance by memoizing permission checking - Log to application.log when an admin starts and stops impersonating a user - Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi) diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 0d2aa1ff03..5b6fefe669 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -18,19 +18,16 @@ class CreateCommitBuildsService return false end - commit = project.ci_commit(sha, ref) - unless commit - commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag) + commit = Ci::Commit.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) - # Skip creating ci_commit when no gitlab-ci.yml is found - unless commit.ci_yaml_file - return false - end - - # Create a new ci_commit - commit.save! + # Skip creating ci_commit when no gitlab-ci.yml is found + unless commit.ci_yaml_file + return false end + # Create a new ci_commit + commit.save! + # Skip creating builds for commits that have [ci skip] unless commit.skip_ci? # Create builds for commit diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 94ff345790..2f465bcf1e 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -48,6 +48,22 @@ describe PostReceive do PostReceive.new.perform(pwd(project), key_id, base64_changes) end end + + context "gitlab-ci.yml" do + subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) } + + context "creates a Ci::Commit for every change" do + before { stub_ci_commit_to_return_yaml_file } + + it { expect{ subject }.to change{ Ci::Commit.count }.by(2) } + end + + context "does not create a Ci::Commit" do + before { stub_ci_commit_yaml_file(nil) } + + it { expect{ subject }.to_not change{ Ci::Commit.count } } + end + end end context "webhook" do From 6fb9bf87c4f202dee9815c10cad9486a1077cc99 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 19 May 2016 19:20:06 -0400 Subject: [PATCH 380/449] Use the right default notification settings for non members --- app/models/user.rb | 8 +------- spec/controllers/projects_controller_spec.rb | 7 ++++--- .../merge_requests/update_service_spec.rb | 1 - spec/services/notification_service_spec.rb | 19 +++++++------------ 4 files changed, 12 insertions(+), 23 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 831fbcbb3f..368a3f3cfb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -777,13 +777,7 @@ class User < ActiveRecord::Base end def notification_settings_for(source) - notification_setting = notification_settings.find_or_initialize_by(source: source) - - if source.is_a?(Project) && !source.team.member?(id) && !notification_setting.persisted? - notification_setting.level = :disabled - end - - notification_setting + notification_settings.find_or_initialize_by(source: source) end private diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 685fe9ffe0..91b46c4d65 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -24,20 +24,20 @@ describe ProjectsController do context "and does not have notification setting" do it "initializes notification as disabled" do get :show, namespace_id: public_project.namespace.path, id: public_project.path - expect(assigns(:notification_setting).level).to eq("disabled") + expect(assigns(:notification_setting).level).to eq("global") end end context "and has notification setting" do before do setting = user.notification_settings_for(public_project) - setting.level = :global + setting.level = :watch setting.save end it "shows current notification setting" do get :show, namespace_id: public_project.namespace.path, id: public_project.path - expect(assigns(:notification_setting).level).to eq("global") + expect(assigns(:notification_setting).level).to eq("watch") end end end @@ -45,6 +45,7 @@ describe ProjectsController do context "rendering default project view" do render_views + it "renders the activity view" do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:project_view).and_return('activity') diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index e872fc0eed..213e8c2eb3 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -17,7 +17,6 @@ describe MergeRequests::UpdateService, services: true do before do project.team << [user, :master] project.team << [user2, :developer] - project.team << [user3, :developer] end describe 'execute' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 4bf8fcde4d..cef5e0d865 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -35,7 +35,7 @@ describe NotificationService, services: true do describe 'Notes' do context 'issue note' do - let(:project) { create(:empty_project, :internal) } + let(:project) { create(:empty_project, :private) } let(:issue) { create(:issue, project: project, assignee: create(:user)) } let(:mentioned_issue) { create(:issue, assignee: issue.assignee) } let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @outsider also') } @@ -52,8 +52,8 @@ describe NotificationService, services: true do it do add_users_with_subscription(note.project, issue) - # Ensure create SentNotification by noteable = issue 7 times, not noteable = note - expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times + # Ensure create SentNotification by noteable = issue 6 times, not noteable = note + expect(SentNotification).to receive(:record).with(issue, any_args).exactly(7).times ActionMailer::Base.deliveries.clear @@ -66,7 +66,7 @@ describe NotificationService, services: true do should_email(@subscriber) should_email(@watcher_and_subscriber) should_email(@subscribed_participant) - should_email(@u_guest_watcher) + should_not_email(@u_guest_watcher) should_not_email(note.author) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -101,7 +101,7 @@ describe NotificationService, services: true do should_email(note.noteable.author) should_email(note.noteable.assignee) should_email(@u_mentioned) - should_email(@u_guest_watcher) + should_not_email(@u_guest_watcher) should_not_email(@u_watcher) should_not_email(note.author) should_not_email(@u_participating) @@ -121,10 +121,7 @@ describe NotificationService, services: true do let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") } it 'filters out users that can not read the issue' do - project.team << [admin, :master] - project.team << [author, :developer] project.team << [member, :developer] - project.team << [assignee, :developer] expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times @@ -148,7 +145,7 @@ describe NotificationService, services: true do before do build_team(note.project) - note.project.team << [[note.author, note.noteable.author, note.noteable.assignee], :master] + note.project.team << [note.author, :master] ActionMailer::Base.deliveries.clear end @@ -190,7 +187,7 @@ describe NotificationService, services: true do before do build_team(note.project) - note.project.team << [[note.author, note.noteable.author], :master] + note.project.team << [note.author, :master] ActionMailer::Base.deliveries.clear end @@ -269,7 +266,6 @@ describe NotificationService, services: true do before do build_team(issue.project) add_users_with_subscription(issue.project, issue) - project.team << [[issue.assignee, issue.author], :developer] ActionMailer::Base.deliveries.clear end @@ -510,7 +506,6 @@ describe NotificationService, services: true do before do build_team(merge_request.target_project) add_users_with_subscription(merge_request.target_project, merge_request) - project.team << [merge_request.assignee, :developer] ActionMailer::Base.deliveries.clear end From 260a915cb0aa163bd282557b5584ed368cd9983a Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 10 May 2016 12:26:24 -0300 Subject: [PATCH 381/449] Fix group visibility level migration in case all visibility levels are restricted --- ...03_add_default_group_visibility_to_application_settings.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb index 75de5f70fa..72b862d67d 100644 --- a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb +++ b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb @@ -7,7 +7,9 @@ class AddDefaultGroupVisibilityToApplicationSettings < ActiveRecord::Migration add_column :application_settings, :default_group_visibility, :integer # Unfortunately, this can't be a `default`, since we don't want the configuration specific # `allowed_visibility_level` to end up in schema.rb - execute("UPDATE application_settings SET default_group_visibility = #{allowed_visibility_level}") + + visibility_level = allowed_visibility_level || Gitlab::VisibilityLevel::PRIVATE + execute("UPDATE application_settings SET default_group_visibility = #{visibility_level}") end def down From a5954080e95318f29790ac1baf6588c6c95184eb Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 19 May 2016 21:08:07 -0500 Subject: [PATCH 382/449] Upgrade activerecord-sessionstore from 0.1.2 to 1.0.0. Changelog: https://github.com/rails/activerecord-session_store/releases/tag/v1.0.0 Includes Rails 5 support. --- Gemfile | 2 +- Gemfile.lock | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 91ad1706a0..832c5a3ca8 100644 --- a/Gemfile +++ b/Gemfile @@ -325,7 +325,7 @@ gem "mail_room", "~> 0.7" gem 'email_reply_parser', '~> 0.5.8' ## CI -gem 'activerecord-session_store', '~> 0.1.0' +gem 'activerecord-session_store', '~> 1.0.0' gem "nested_form", '~> 0.3.2' # OAuth diff --git a/Gemfile.lock b/Gemfile.lock index b55764504c..e1c5b9630d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,10 +33,12 @@ GEM activemodel (= 4.2.6) activesupport (= 4.2.6) arel (~> 6.0) - activerecord-session_store (0.1.2) - actionpack (>= 4.0.0, < 5) - activerecord (>= 4.0.0, < 5) - railties (>= 4.0.0, < 5) + activerecord-session_store (1.0.0) + actionpack (>= 4.0, < 5.1) + activerecord (>= 4.0, < 5.1) + multi_json (~> 1.11, >= 1.11.2) + rack (>= 1.5.2, < 3) + railties (>= 4.0, < 5.1) activesupport (4.2.6) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) @@ -883,7 +885,7 @@ PLATFORMS DEPENDENCIES RedCloth (~> 4.2.9) ace-rails-ap (~> 4.0.2) - activerecord-session_store (~> 0.1.0) + activerecord-session_store (~> 1.0.0) acts-as-taggable-on (~> 3.4) addressable (~> 2.3.8) after_commit_queue From a768c04ba511b5f98071d45c31d5c017dd7e4282 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 May 2016 22:09:51 -0500 Subject: [PATCH 383/449] Fix fork counter ui bug for safari Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/projects.scss | 6 +++++- app/views/projects/buttons/_fork.html.haml | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a3690e40e2..b4702b3118 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -149,6 +149,10 @@ white-space: nowrap; margin: 0 11px 0 4px; + a { + color: inherit; + } + &:hover { background: #fff; } @@ -161,7 +165,7 @@ display: inline-table; margin-right: 12px; - a { + > a { margin: -1px; } } diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 5fb5fe5af2..34ad9fe2c4 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -12,7 +12,8 @@ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do = icon('code-fork fw') Fork - = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do + %div.count-with-arrow %span.arrow %span.count - = @project.forks_count + = link_to namespace_project_forks_path(@project.namespace, @project) do + = @project.forks_count From 1472bb92471113790ba3564d16af11d5b96c28aa Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 19 May 2016 23:13:51 -0500 Subject: [PATCH 384/449] Updated the DB schema --- db/schema.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 2e154b98b3..aee5fb9093 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: 20160509201028) do +ActiveRecord::Schema.define(version: 20160516174813) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -82,6 +82,7 @@ ActiveRecord::Schema.define(version: 20160509201028) do t.integer "metrics_packet_size", default: 1 t.text "disabled_oauth_sign_in_sources" t.string "health_check_access_token" + t.boolean "send_user_confirmation_email", default: false end create_table "audit_events", force: :cascade do |t| @@ -633,10 +634,10 @@ ActiveRecord::Schema.define(version: 20160509201028) do t.string "line_code" t.string "commit_id" t.integer "noteable_id" - t.boolean "system", default: false, null: false + t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" - t.boolean "is_award", default: false, null: false + t.boolean "is_award", default: false, null: false t.string "type" end From ce8dd3bd660e18c1a143c24539418c8697fe29b9 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 19 May 2016 12:51:15 +0100 Subject: [PATCH 385/449] Fixed issue with merge button color Closes #17644 --- app/assets/javascripts/merge_request_widget.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index f58647988a..b6d590f681 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -113,7 +113,7 @@ class @MergeRequestWidget switch state when "failed", "canceled", "not_found" @setMergeButtonClass('btn-danger') - when "running", "pending" + when "running" @setMergeButtonClass('btn-warning') when "success" @setMergeButtonClass('btn-create') @@ -126,6 +126,6 @@ class @MergeRequestWidget $('.ci_widget:visible .ci-coverage').text(text) setMergeButtonClass: (css_class) -> - $('.accept_merge_request') + $('.js-merge-button') .removeClass('btn-danger btn-warning btn-create') .addClass(css_class) From 9f60a2eda24c9017c8b23ae8885ca31d59fbc659 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 19 May 2016 12:33:08 +0100 Subject: [PATCH 386/449] Fixed issue with enter key selecting wrong option in dropdown If you search for a label and press enter the dropdown will select the last element rather than the first. This is because the currentIndex is -1 rather than 0 Closes #17630 --- app/assets/javascripts/gl_dropdown.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 1d1bfeb2e7..fd97471cdc 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -460,7 +460,7 @@ class GitLabDropdown return false if currentKeyCode is 13 - @selectRowAtIndex currentIndex + @selectRowAtIndex if currentIndex < 0 then 0 else currentIndex removeArrayKeyEvent: -> $('body').off 'keydown' From f307ff34134caa376b50e00f8e285c355b4af701 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 20 May 2016 09:11:28 +0100 Subject: [PATCH 387/449] Replaced line breaks --- app/views/shared/issuable/_form.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index d06a287281..6990863bd3 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -56,8 +56,8 @@ placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, selected: issuable.assignee_id, project: @target_project || @project, first_user: true, current_user: true, include_blank: true) - %br - = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline' + %div + = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline' .form-group.issue-milestone = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } @@ -69,8 +69,8 @@ .prepend-top-10 %span.light No open milestones available. - if can? current_user, :admin_milestone, issuable.project - %br - = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + %div + = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" .form-group - has_labels = issuable.project.labels.any? = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" @@ -82,8 +82,8 @@ - else %span.light No labels yet. - if can? current_user, :admin_label, issuable.project - %br - = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + %div + = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" - if has_due_date .col-lg-6 .form-group From 7880a300dc9ef3fcceb7f1a6af6a6500b0b46e5c Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Fri, 12 Feb 2016 21:41:31 +0100 Subject: [PATCH 388/449] Allows MR authors to have the source branch removed when merging the MR --- CHANGELOG | 1 + .../projects/merge_requests_controller.rb | 3 ++- app/models/merge_request.rb | 17 ++++++++++++++++- app/services/merge_requests/create_service.rb | 7 +++++-- app/services/merge_requests/merge_service.rb | 8 ++++++-- app/services/merge_requests/update_service.rb | 2 ++ .../widget/open/_accept.html.haml | 5 ++++- .../open/_merge_when_build_succeeds.html.haml | 5 ++--- .../widget/open/_not_allowed.html.haml | 4 +++- app/views/shared/issuable/_form.html.haml | 7 +++++++ spec/models/merge_request_spec.rb | 7 ++++++- .../merge_requests/create_service_spec.rb | 4 +++- .../merge_requests/merge_service_spec.rb | 15 +++++++++++++++ .../merge_requests/update_service_spec.rb | 4 +++- 14 files changed, 75 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4c810ab21f..ca33eeef8d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -65,6 +65,7 @@ v 8.8.0 (unreleased) - All Grape API helpers are now instrumented - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) - Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine) + - Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs) v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index c5757a2462..f137c12d21 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -334,7 +334,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController params.require(:merge_request).permit( :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, - :state_event, :description, :task_num, label_ids: [] + :state_event, :description, :task_num, :force_remove_source_branch, + label_ids: [] ) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 45ddcf6812..722c258244 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -286,6 +286,18 @@ class MergeRequest < ActiveRecord::Base last_commit == source_project.commit(source_branch) end + def should_remove_source_branch? + merge_params['should_remove_source_branch'].present? + end + + def force_remove_source_branch? + merge_params['force_remove_source_branch'].present? + end + + def remove_source_branch? + should_remove_source_branch? || force_remove_source_branch? + end + def mr_and_commit_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 @@ -426,7 +438,10 @@ class MergeRequest < ActiveRecord::Base self.merge_when_build_succeeds = false self.merge_user = nil - self.merge_params = nil + if merge_params + merge_params.delete('should_remove_source_branch') + merge_params.delete('commit_message') + end self.save end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 33609d01f2..96a25330af 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -8,11 +8,14 @@ module MergeRequests @project = Project.find(params[:target_project_id]) if params[:target_project_id] filter_params - label_params = params[:label_ids] - merge_request = MergeRequest.new(params.except(:label_ids)) + label_params = params.delete(:label_ids) + force_remove_source_branch = params.delete(:force_remove_source_branch) + + merge_request = MergeRequest.new(params) merge_request.source_project = source_project merge_request.target_project ||= source_project merge_request.author = current_user + merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch if merge_request.save merge_request.update_attributes(label_ids: label_params) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 9a58383b39..9aaf5a5e56 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -45,10 +45,14 @@ module MergeRequests def after_merge MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) - if params[:should_remove_source_branch].present? - DeleteBranchService.new(@merge_request.source_project, current_user). + if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? + DeleteBranchService.new(@merge_request.source_project, branch_deletion_user). execute(merge_request.source_branch) end end + + def branch_deletion_user + @merge_request.force_remove_source_branch? ? @merge_request.author : current_user + end end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 477c64e737..026a37997d 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,6 +11,8 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) + update(merge_request) end diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 807833741a..cfdf4edac3 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -25,7 +25,10 @@ - else = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do Accept Merge Request - - if @merge_request.can_remove_source_branch?(current_user) + - if @merge_request.force_remove_source_branch? + .accept-control + The source branch will be removed. + - elsif @merge_request.can_remove_source_branch?(current_user) .accept-control.checkbox = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do = check_box_tag :should_remove_source_branch diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 2168294c68..b83ddcab3a 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -2,17 +2,16 @@ Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} to be merged automatically when the build succeeds. %div - - should_remove_source_branch = @merge_request.merge_params["should_remove_source_branch"].present? %p = succeed '.' do The changes will be merged into %span.label-branch= @merge_request.target_branch - - if should_remove_source_branch + - if @merge_request.remove_source_branch? The source branch will be removed. - else The source branch will not be removed. - - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch && @merge_request.merge_user == current_user + - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user) - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 diff --git a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml index a8145558ca..57ce195902 100644 --- a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml +++ b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml @@ -1,4 +1,6 @@ -%h4 +%h4 Ready to be merged automatically %p Ask someone with write access to this repository to merge this request. + - if @merge_request.force_remove_source_branch? + The source branch will be removed. diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 5c52cc6d1d..1181edec3f 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -114,6 +114,13 @@ - if @merge_request.new_record?   = link_to 'Change branches', mr_change_branches_path(@merge_request) + - if @merge_request.can_remove_source_branch?(current_user) + .form-group + .col-sm-10.col-sm-offset-2 + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? + Remove source branch when merge request is accepted. - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 9eef08c6d0..e269ff26a0 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -260,13 +260,18 @@ describe MergeRequest, models: true do end describe "#reset_merge_when_build_succeeds" do - let(:merge_if_green) { create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user) } + let(:merge_if_green) do + create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user), + merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" } + end it "sets the item to false" do merge_if_green.reset_merge_when_build_succeeds merge_if_green.reload expect(merge_if_green.merge_when_build_succeeds).to be_falsey + expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil + expect(merge_if_green.merge_params["commit_message"]).to be_nil end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 120f4d6a66..e433f49872 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -12,7 +12,8 @@ describe MergeRequests::CreateService, services: true do title: 'Awesome merge_request', description: 'please fix', source_branch: 'feature', - target_branch: 'master' + target_branch: 'master', + force_remove_source_branch: '1' } end @@ -29,6 +30,7 @@ describe MergeRequests::CreateService, services: true do it { expect(@merge_request).to be_valid } it { expect(@merge_request.title).to eq('Awesome merge_request') } it { expect(@merge_request.assignee).to be_nil } + it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } it 'should execute hooks with default action' do expect(service).to have_received(:execute_hooks).with(@merge_request) diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index ceb3f97280..1b0396eb68 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -38,6 +38,21 @@ describe MergeRequests::MergeService, services: true do end end + context 'remove source branch by author' do + let(:service) do + merge_request.merge_params['force_remove_source_branch'] = '1' + merge_request.save! + MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') + end + + it 'removes the source branch' do + expect(DeleteBranchService).to receive(:new). + with(merge_request.source_project, merge_request.author). + and_call_original + service.execute(merge_request) + end + end + context "error handling" do let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 213e8c2eb3..cc7dc216a7 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -39,7 +39,8 @@ describe MergeRequests::UpdateService, services: true do assignee_id: user2.id, state_event: 'close', label_ids: [label.id], - target_branch: 'target' + target_branch: 'target', + force_remove_source_branch: '1' } end @@ -61,6 +62,7 @@ describe MergeRequests::UpdateService, services: true do it { expect(@merge_request.labels.count).to eq(1) } it { expect(@merge_request.labels.first.title).to eq(label.name) } it { expect(@merge_request.target_branch).to eq('target') } + it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } it 'should execute hooks with update action' do expect(service).to have_received(:execute_hooks). From 341c1181d9bb2079c27a6da5b13f9299c7be0952 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 20 May 2016 14:55:27 +0200 Subject: [PATCH 389/449] refactor project to include some nicer EE stuff --- app/models/project.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 9a34198e09..37de1dfe4d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -204,7 +204,7 @@ class Project < ActiveRecord::Base state :finished state :failed - after_transition any => :finished, do: :clear_import_data + after_transition any => :finished, do: :reset_cache_and_import_attrs end class << self @@ -360,7 +360,7 @@ class Project < ActiveRecord::Base end end - def clear_import_data + def reset_cache_and_import_attrs update(import_error: nil) ProjectCacheWorker.perform_async(self.id) @@ -426,12 +426,7 @@ class Project < ActiveRecord::Base end def safe_import_url - result = URI.parse(self.import_url) - result.password = '*****' unless result.password.nil? - result.user = '*****' unless result.user.nil? || result.user == "git" #tokens or other data may be saved as user - result.to_s - rescue - self.import_url + Gitlab::UrlSanitizer.new(import_url).masked_url end def check_limit From 1a98dcacc527f684bc68eb6c7d7f94371e329fee Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Fri, 20 May 2016 17:24:06 +0000 Subject: [PATCH 390/449] Fix grammar in health_check.md A access token -> An access token --- doc/monitoring/health_check.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md index 22c53a9aec..5157cf686a 100644 --- a/doc/monitoring/health_check.md +++ b/doc/monitoring/health_check.md @@ -9,7 +9,7 @@ and access the cache. This endpoint can be provided to uptime monitoring service ## Access Token -A access token needs to be provided while accessing the health check endpoint. The current +An access token needs to be provided while accessing the health check endpoint. The current accepted token can be found on the `admin/heath_check` page of your GitLab instance. ![access token](img/health_check_token.png) From f04c5b27f0c19dfab00a6ac2de8b4168b67fe7a0 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 20 May 2016 13:52:42 -0400 Subject: [PATCH 391/449] current_user.confirmed_at.present? => current_user.confirmed? --- app/controllers/registrations_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 26eb15f49e..75b78a49ea 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -38,7 +38,7 @@ class RegistrationsController < Devise::RegistrationsController end def after_sign_up_path_for(user) - user.confirmed_at.present? ? dashboard_projects_path : users_almost_there_path + user.confirmed? ? dashboard_projects_path : users_almost_there_path end def after_inactive_sign_up_path_for(_resource) From e166a8022a3f239938a1449a0a8ce3485f309766 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 29 Apr 2016 16:25:03 +0200 Subject: [PATCH 392/449] Backend for a gitignores dropdown --- app/helpers/blob_helper.rb | 4 ++ app/models/repository.rb | 20 +++++-- app/views/projects/blob/_editor.html.haml | 3 + app/views/projects/empty.html.haml | 10 +++- lib/api/api.rb | 1 + lib/api/entities.rb | 8 +++ lib/api/gitignores.rb | 29 ++++++++++ lib/gitlab/gitignore.rb | 68 +++++++++++++++++++++++ lib/tasks/gitlab/update_gitignore.rake | 26 +++++++++ spec/lib/gitlab/gitignore_spec.rb | 38 +++++++++++++ spec/requests/api/gitignores_spec.rb | 29 ++++++++++ 11 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 lib/api/gitignores.rb create mode 100644 lib/gitlab/gitignore.rb create mode 100644 lib/tasks/gitlab/update_gitignore.rake create mode 100644 spec/lib/gitlab/gitignore_spec.rb create mode 100644 spec/requests/api/gitignores_spec.rb diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 93241b3afb..fb1b764946 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -184,4 +184,8 @@ module BlobHelper Other: licenses.reject(&:featured).map { |license| [license.name, license.key] } } end + + def gitignores_for_select + @gitignores_for_select ||= Gitlab::Gitignore.all + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 47a7223c72..f26278cc3a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -472,9 +472,7 @@ class Repository def changelog cache.fetch(:changelog) do - tree(:head).blobs.find do |file| - file.name =~ /\A(changelog|history|changes|news)/i - end + file_on_head(/\A(changelog|history|changes|news)/i) end end @@ -482,9 +480,7 @@ class Repository return nil unless head_exists? cache.fetch(:license_blob) do - tree(:head).blobs.find do |file| - file.name =~ /\A(licen[sc]e|copying)(\..+|\z)/i - end + file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i) end end @@ -496,6 +492,14 @@ class Repository end end + def gitignore + return nil if !exists? || empty? + + cache.fetch(:gitignore) do + file_on_head(/\A\.gitignore\z/) + end + end + def gitlab_ci_yml return nil unless head_exists? @@ -989,4 +993,8 @@ class Repository def head_exists? exists? && !empty? && !rugged.head_unborn? end + + def file_on_head(regex) + tree(:head).blobs.find { |file| file.name =~ regex } + end end diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index fefa652a3d..eac5014b7c 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -16,6 +16,9 @@ .license-selector.js-license-selector.hide = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name} + .gitignore-selector.js-gitignore-selector.hide + = select_tag :gitignore_template, options_for_select(Gitlab::Gitignore.all), include_blank: true, class: 'select2 gitignore-select', data: {placeholder: 'Choose a .gitignore template'} + .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 1a2e59752f..636beb73ec 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -15,10 +15,14 @@ If you already have files you can push them using command line instructions below. %p Otherwise you can start with adding a - = link_to "README", new_readme_path, class: 'underlined-link' + = succeed ',' do + = link_to "README", new_readme_path, class: 'underlined-link' + a + = succeed ',' do + = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' or a - = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' - file to this project. + = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link' + to this project. - if can?(current_user, :push_code, @project) %div{ class: container_class } diff --git a/lib/api/api.rb b/lib/api/api.rb index 360fb41a72..6cd909f611 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -58,5 +58,6 @@ module API mount ::API::Runners mount ::API::Licenses mount ::API::Subscriptions + mount ::API::Gitignores end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 8298e3ad34..31491cf31d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -457,5 +457,13 @@ module API expose(:limitations) { |license| license.meta['limitations'] } expose :content end + + class GitignoresList < Grape::Entity + expose :name + end + + class Gitignore < Grape::Entity + expose :name, :content + end end end diff --git a/lib/api/gitignores.rb b/lib/api/gitignores.rb new file mode 100644 index 0000000000..1af9ba6b31 --- /dev/null +++ b/lib/api/gitignores.rb @@ -0,0 +1,29 @@ +module API + class Gitignores < Grape::API + + # Get the list of the available gitignore templates + # + # Example Request: + # GET /gitignores + get 'gitignores' do + present Gitlab::Gitignore.all, with: Entities::GitignoresList + end + + # Get the text for a specific gitignore + # + # Parameters: + # key (required) - The key of a license + # + # Example Request: + # GET /gitignores/elixir + # + get 'gitignores/:key' do + required_attributes! [:key] + + gitignore = Gitlab::Gitignore.find(params[:key]) + not_found!('.gitignore') unless gitignore + + present gitignore, with: Entities::Gitignore + end + end +end diff --git a/lib/gitlab/gitignore.rb b/lib/gitlab/gitignore.rb new file mode 100644 index 0000000000..a2de2831e3 --- /dev/null +++ b/lib/gitlab/gitignore.rb @@ -0,0 +1,68 @@ +module Gitlab + class Gitignore + FILTER_REGEX = /\.gitignore\z/.freeze + + attr_accessor :name, :directory + + def initialize(name, directory) + @name = name + @directory = directory + end + + def content + File.read(path) + end + + class << self + def all + languages_frameworks + global + end + + def find(key) + file_name = "#{key}.gitignore" + + directory = select_directory(file_name) + directory ? new(key, directory) : nil + end + + def global + files_for_folder(global_dir).map { |f| new(f, global_dir) } + end + + def languages_frameworks + files_for_folder(gitignore_dir).map { |f| new(f, gitignore_dir) } + end + end + + private + + def path + File.expand_path("#{name}.gitignore", directory) + end + + class << self + def select_directory(file_name) + [self.gitignore_dir, self.global_dir].find { |dir| File.exist?(File.expand_path(file_name, dir)) } + end + + def global_dir + File.expand_path('Global', gitignore_dir) + end + + def gitignore_dir + File.expand_path('vendor/gitignore', Rails.root) + end + + def files_for_folder(dir) + gitignores = [] + Dir.entries(dir).each do |e| + next unless e.end_with?('.gitignore') + + gitignores << e.gsub(FILTER_REGEX, '') + end + + gitignores + end + end + end +end diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake new file mode 100644 index 0000000000..61cbfd6737 --- /dev/null +++ b/lib/tasks/gitlab/update_gitignore.rake @@ -0,0 +1,26 @@ +namespace :gitlab do + desc "GitLab | Update gitignore" + task :update_gitignore do + dir = File.expand_path('vendor', Rails.root) + FileUtils.cd(dir) + + dir = File.expand_path('gitignore', dir) + clone_gitignores(dir) + remove_unneeded_files(dir) + + puts "Done".green + end + + def clone_gitignores(dir) + FileUtils.rm_rf(dir) if Dir.exist?(dir) + system('git clone --depth=1 --branch=master https://github.com/github/gitignore.git') + end + + def remove_unneeded_files(dir) + [File.expand_path('Global', dir), dir].each do |path| + Dir.entries(path).reject { |e| e =~ /(\.{1,2}|Global|\.gitignore)\z/ }.each do |file| + FileUtils.rm_rf File.expand_path(file, path) + end + end + end +end diff --git a/spec/lib/gitlab/gitignore_spec.rb b/spec/lib/gitlab/gitignore_spec.rb new file mode 100644 index 0000000000..5dab821a8e --- /dev/null +++ b/spec/lib/gitlab/gitignore_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::Gitignore do + subject { Gitlab::Gitignore } + describe '.all' do + it 'strips the gitignore suffix' do + expect(subject.all.first.name).not_to end_with('.gitignore') + end + + it 'combines the globals and rest' do + all = subject.all.map(&:name) + + expect(all).to include('Vim') + expect(all).to include('Ruby') + end + end + + describe '.find' do + it 'returns nil if the file does not exist' do + expect(subject.find('mepmep-yadida')).to be nil + end + + it 'returns the Gitignore object of a valid file' do + ruby = subject.find('Ruby') + + expect(ruby).to be_a Gitlab::Gitignore + end + end + + describe '#content' do + it 'loads the full file' do + gitignore = subject.new('Ruby', File.expand_path('vendor/gitignore', Rails.root)) + + expect(gitignore.name).to eq 'Ruby' + expect(gitignore.content).to start_with('*.gem') + end + end +end diff --git a/spec/requests/api/gitignores_spec.rb b/spec/requests/api/gitignores_spec.rb new file mode 100644 index 0000000000..d0e576e637 --- /dev/null +++ b/spec/requests/api/gitignores_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe API::Gitignores, api: true do + include ApiHelpers + + describe 'Entity Gitignore' do + before { get api('/gitignores/Ruby') } + + it { expect(json_response['name']).to eq('Ruby') } + it { expect(json_response['content']).to include('*.gem') } + end + + describe 'Entity GitignoresList' do + before { get api('/gitignores') } + + it { expect(json_response.first['name']).to be_truthy } + it { expect(json_response.first['content']).to be_falsey } + end + + describe 'GET /gitignores' do + it 'returns a list of available license templates' do + get api('/gitignores') + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to be > 15 + end + end +end From fab9dbb3eba55151c34fc8bc171e3670e6c7c422 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 6 May 2016 17:13:16 +0200 Subject: [PATCH 393/449] First import of gitignores Add .gitignore dropdown when creating a new .gitignore file Signed-off-by: Alfredo Sumaran --- app/assets/javascripts/api.js.coffee | 7 + .../blob/blob_gitignore_selector.js.coffee | 62 ++ .../javascripts/blob/edit_blob.js.coffee | 1 + app/assets/stylesheets/pages/editor.scss | 21 +- app/views/projects/blob/_editor.html.haml | 6 +- vendor/gitignore/Actionscript.gitignore | 19 + vendor/gitignore/Ada.gitignore | 5 + vendor/gitignore/Agda.gitignore | 1 + vendor/gitignore/Android.gitignore | 39 ++ vendor/gitignore/AppEngine.gitignore | 2 + .../gitignore/AppceleratorTitanium.gitignore | 3 + vendor/gitignore/ArchLinuxPackages.gitignore | 13 + vendor/gitignore/Autotools.gitignore | 18 + vendor/gitignore/C++.gitignore | 28 + vendor/gitignore/C.gitignore | 33 ++ vendor/gitignore/CFWheels.gitignore | 12 + vendor/gitignore/CMake.gitignore | 6 + vendor/gitignore/CUDA.gitignore | 6 + vendor/gitignore/CakePHP.gitignore | 25 + vendor/gitignore/ChefCookbook.gitignore | 9 + vendor/gitignore/Clojure.gitignore | 1 + vendor/gitignore/CodeIgniter.gitignore | 6 + vendor/gitignore/CommonLisp.gitignore | 3 + vendor/gitignore/Composer.gitignore | 6 + vendor/gitignore/Concrete5.gitignore | 4 + vendor/gitignore/Coq.gitignore | 3 + vendor/gitignore/CraftCMS.gitignore | 3 + vendor/gitignore/D.gitignore | 20 + vendor/gitignore/DM.gitignore | 5 + vendor/gitignore/Dart.gitignore | 27 + vendor/gitignore/Delphi.gitignore | 66 +++ vendor/gitignore/Drupal.gitignore | 36 ++ vendor/gitignore/EPiServer.gitignore | 4 + vendor/gitignore/Eagle.gitignore | 44 ++ vendor/gitignore/Elisp.gitignore | 5 + vendor/gitignore/Elixir.gitignore | 5 + vendor/gitignore/Elm.gitignore | 4 + vendor/gitignore/Erlang.gitignore | 10 + vendor/gitignore/ExpressionEngine.gitignore | 19 + vendor/gitignore/ExtJs.gitignore | 4 + vendor/gitignore/Fancy.gitignore | 2 + vendor/gitignore/Finale.gitignore | 13 + vendor/gitignore/ForceDotCom.gitignore | 4 + vendor/gitignore/Fortran.gitignore | 1 + vendor/gitignore/FuelPHP.gitignore | 21 + vendor/gitignore/GWT.gitignore | 28 + vendor/gitignore/Gcov.gitignore | 5 + vendor/gitignore/GitBook.gitignore | 16 + vendor/gitignore/Global/Anjuta.gitignore | 3 + vendor/gitignore/Global/Archives.gitignore | 27 + vendor/gitignore/Global/BricxCC.gitignore | 4 + vendor/gitignore/Global/CVS.gitignore | 4 + vendor/gitignore/Global/Calabash.gitignore | 10 + vendor/gitignore/Global/Cloud9.gitignore | 3 + vendor/gitignore/Global/CodeKit.gitignore | 3 + vendor/gitignore/Global/DartEditor.gitignore | 2 + vendor/gitignore/Global/Dreamweaver.gitignore | 7 + vendor/gitignore/Global/Dropbox.gitignore | 4 + vendor/gitignore/Global/Eclipse.gitignore | 51 ++ .../gitignore/Global/EiffelStudio.gitignore | 2 + vendor/gitignore/Global/Emacs.gitignore | 42 ++ vendor/gitignore/Global/Ensime.gitignore | 4 + vendor/gitignore/Global/Espresso.gitignore | 1 + vendor/gitignore/Global/FlexBuilder.gitignore | 3 + vendor/gitignore/Global/GPG.gitignore | 2 + .../Global/IPythonNotebook.gitignore | 2 + vendor/gitignore/Global/JDeveloper.gitignore | 13 + vendor/gitignore/Global/JetBrains.gitignore | 44 ++ vendor/gitignore/Global/KDevelop4.gitignore | 2 + vendor/gitignore/Global/Kate.gitignore | 3 + vendor/gitignore/Global/Lazarus.gitignore | 30 + vendor/gitignore/Global/LibreOffice.gitignore | 2 + vendor/gitignore/Global/Linux.gitignore | 10 + vendor/gitignore/Global/LyX.gitignore | 4 + vendor/gitignore/Global/Matlab.gitignore | 19 + vendor/gitignore/Global/Mercurial.gitignore | 6 + .../Global/MicrosoftOffice.gitignore | 16 + vendor/gitignore/Global/ModelSim.gitignore | 23 + vendor/gitignore/Global/Momentics.gitignore | 8 + vendor/gitignore/Global/MonoDevelop.gitignore | 8 + vendor/gitignore/Global/NetBeans.gitignore | 7 + vendor/gitignore/Global/Ninja.gitignore | 2 + vendor/gitignore/Global/NotepadPP.gitignore | 2 + vendor/gitignore/Global/OSX.gitignore | 24 + vendor/gitignore/Global/Otto.gitignore | 1 + vendor/gitignore/Global/Redcar.gitignore | 1 + vendor/gitignore/Global/Redis.gitignore | 3 + vendor/gitignore/Global/SBT.gitignore | 9 + vendor/gitignore/Global/SVN.gitignore | 1 + vendor/gitignore/Global/SlickEdit.gitignore | 11 + vendor/gitignore/Global/SublimeText.gitignore | 14 + vendor/gitignore/Global/SynopsysVCS.gitignore | 36 ++ vendor/gitignore/Global/Tags.gitignore | 16 + vendor/gitignore/Global/TextMate.gitignore | 3 + vendor/gitignore/Global/TortoiseGit.gitignore | 2 + vendor/gitignore/Global/Vagrant.gitignore | 1 + vendor/gitignore/Global/Vim.gitignore | 10 + vendor/gitignore/Global/VirtualEnv.gitignore | 12 + .../Global/VisualStudioCode.gitignore | 2 + vendor/gitignore/Global/WebMethods.gitignore | 14 + vendor/gitignore/Global/Windows.gitignore | 18 + vendor/gitignore/Global/Xcode.gitignore | 23 + vendor/gitignore/Global/XilinxISE.gitignore | 67 +++ vendor/gitignore/Go.gitignore | 24 + vendor/gitignore/Gradle.gitignore | 14 + vendor/gitignore/Grails.gitignore | 33 ++ vendor/gitignore/Haskell.gitignore | 18 + vendor/gitignore/IGORPro.gitignore | 5 + vendor/gitignore/Idris.gitignore | 2 + vendor/gitignore/Java.gitignore | 12 + vendor/gitignore/Jboss.gitignore | 19 + vendor/gitignore/Jekyll.gitignore | 3 + vendor/gitignore/Joomla.gitignore | 546 ++++++++++++++++++ vendor/gitignore/KiCad.gitignore | 20 + vendor/gitignore/Kohana.gitignore | 2 + vendor/gitignore/LabVIEW.gitignore | 16 + vendor/gitignore/Laravel.gitignore | 16 + vendor/gitignore/Leiningen.gitignore | 12 + vendor/gitignore/LemonStand.gitignore | 21 + vendor/gitignore/Lilypond.gitignore | 6 + vendor/gitignore/Lithium.gitignore | 2 + vendor/gitignore/Lua.gitignore | 41 ++ vendor/gitignore/Magento.gitignore | 104 ++++ vendor/gitignore/Maven.gitignore | 9 + vendor/gitignore/Mercury.gitignore | 13 + .../gitignore/MetaProgrammingSystem.gitignore | 16 + vendor/gitignore/Nanoc.gitignore | 10 + vendor/gitignore/Nim.gitignore | 1 + vendor/gitignore/Node.gitignore | 37 ++ vendor/gitignore/OCaml.gitignore | 20 + vendor/gitignore/Objective-C.gitignore | 51 ++ vendor/gitignore/Opa.gitignore | 13 + vendor/gitignore/OpenCart.gitignore | 13 + vendor/gitignore/OracleForms.gitignore | 8 + vendor/gitignore/Packer.gitignore | 5 + vendor/gitignore/Perl.gitignore | 20 + vendor/gitignore/Phalcon.gitignore | 2 + vendor/gitignore/PlayFramework.gitignore | 15 + vendor/gitignore/Plone.gitignore | 18 + vendor/gitignore/Prestashop.gitignore | 32 + vendor/gitignore/Processing.gitignore | 7 + vendor/gitignore/Python.gitignore | 89 +++ vendor/gitignore/Qooxdoo.gitignore | 5 + vendor/gitignore/Qt.gitignore | 38 ++ vendor/gitignore/R.gitignore | 33 ++ vendor/gitignore/ROS.gitignore | 47 ++ vendor/gitignore/Rails.gitignore | 38 ++ vendor/gitignore/RhodesRhomobile.gitignore | 9 + vendor/gitignore/Ruby.gitignore | 50 ++ vendor/gitignore/Rust.gitignore | 7 + vendor/gitignore/SCons.gitignore | 2 + vendor/gitignore/Sass.gitignore | 2 + vendor/gitignore/Scala.gitignore | 17 + vendor/gitignore/Scheme.gitignore | 7 + vendor/gitignore/Scrivener.gitignore | 7 + vendor/gitignore/Sdcc.gitignore | 8 + vendor/gitignore/SeamGen.gitignore | 26 + vendor/gitignore/SketchUp.gitignore | 1 + vendor/gitignore/Smalltalk.gitignore | 18 + vendor/gitignore/Stella.gitignore | 12 + vendor/gitignore/SugarCRM.gitignore | 25 + vendor/gitignore/Swift.gitignore | 63 ++ vendor/gitignore/Symfony.gitignore | 48 ++ vendor/gitignore/SymphonyCMS.gitignore | 6 + vendor/gitignore/TeX.gitignore | 180 ++++++ vendor/gitignore/Terraform.gitignore | 3 + vendor/gitignore/Textpattern.gitignore | 11 + vendor/gitignore/TurboGears2.gitignore | 20 + vendor/gitignore/Typo3.gitignore | 20 + vendor/gitignore/Umbraco.gitignore | 19 + vendor/gitignore/Unity.gitignore | 30 + vendor/gitignore/UnrealEngine.gitignore | 62 ++ vendor/gitignore/VVVV.gitignore | 6 + vendor/gitignore/VisualStudio.gitignore | 252 ++++++++ vendor/gitignore/Waf.gitignore | 4 + vendor/gitignore/WordPress.gitignore | 18 + vendor/gitignore/Xojo.gitignore | 11 + vendor/gitignore/Yeoman.gitignore | 6 + vendor/gitignore/Yii.gitignore | 6 + vendor/gitignore/ZendFramework.gitignore | 25 + vendor/gitignore/Zephir.gitignore | 26 + 181 files changed, 3780 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/blob/blob_gitignore_selector.js.coffee create mode 100644 vendor/gitignore/Actionscript.gitignore create mode 100644 vendor/gitignore/Ada.gitignore create mode 100644 vendor/gitignore/Agda.gitignore create mode 100644 vendor/gitignore/Android.gitignore create mode 100644 vendor/gitignore/AppEngine.gitignore create mode 100644 vendor/gitignore/AppceleratorTitanium.gitignore create mode 100644 vendor/gitignore/ArchLinuxPackages.gitignore create mode 100644 vendor/gitignore/Autotools.gitignore create mode 100644 vendor/gitignore/C++.gitignore create mode 100644 vendor/gitignore/C.gitignore create mode 100644 vendor/gitignore/CFWheels.gitignore create mode 100644 vendor/gitignore/CMake.gitignore create mode 100644 vendor/gitignore/CUDA.gitignore create mode 100644 vendor/gitignore/CakePHP.gitignore create mode 100644 vendor/gitignore/ChefCookbook.gitignore create mode 120000 vendor/gitignore/Clojure.gitignore create mode 100644 vendor/gitignore/CodeIgniter.gitignore create mode 100644 vendor/gitignore/CommonLisp.gitignore create mode 100644 vendor/gitignore/Composer.gitignore create mode 100644 vendor/gitignore/Concrete5.gitignore create mode 100644 vendor/gitignore/Coq.gitignore create mode 100644 vendor/gitignore/CraftCMS.gitignore create mode 100644 vendor/gitignore/D.gitignore create mode 100644 vendor/gitignore/DM.gitignore create mode 100644 vendor/gitignore/Dart.gitignore create mode 100644 vendor/gitignore/Delphi.gitignore create mode 100644 vendor/gitignore/Drupal.gitignore create mode 100644 vendor/gitignore/EPiServer.gitignore create mode 100644 vendor/gitignore/Eagle.gitignore create mode 100644 vendor/gitignore/Elisp.gitignore create mode 100644 vendor/gitignore/Elixir.gitignore create mode 100644 vendor/gitignore/Elm.gitignore create mode 100644 vendor/gitignore/Erlang.gitignore create mode 100644 vendor/gitignore/ExpressionEngine.gitignore create mode 100644 vendor/gitignore/ExtJs.gitignore create mode 100644 vendor/gitignore/Fancy.gitignore create mode 100644 vendor/gitignore/Finale.gitignore create mode 100644 vendor/gitignore/ForceDotCom.gitignore create mode 120000 vendor/gitignore/Fortran.gitignore create mode 100644 vendor/gitignore/FuelPHP.gitignore create mode 100644 vendor/gitignore/GWT.gitignore create mode 100644 vendor/gitignore/Gcov.gitignore create mode 100644 vendor/gitignore/GitBook.gitignore create mode 100644 vendor/gitignore/Global/Anjuta.gitignore create mode 100644 vendor/gitignore/Global/Archives.gitignore create mode 100644 vendor/gitignore/Global/BricxCC.gitignore create mode 100644 vendor/gitignore/Global/CVS.gitignore create mode 100644 vendor/gitignore/Global/Calabash.gitignore create mode 100644 vendor/gitignore/Global/Cloud9.gitignore create mode 100644 vendor/gitignore/Global/CodeKit.gitignore create mode 100644 vendor/gitignore/Global/DartEditor.gitignore create mode 100644 vendor/gitignore/Global/Dreamweaver.gitignore create mode 100644 vendor/gitignore/Global/Dropbox.gitignore create mode 100644 vendor/gitignore/Global/Eclipse.gitignore create mode 100644 vendor/gitignore/Global/EiffelStudio.gitignore create mode 100644 vendor/gitignore/Global/Emacs.gitignore create mode 100644 vendor/gitignore/Global/Ensime.gitignore create mode 100644 vendor/gitignore/Global/Espresso.gitignore create mode 100644 vendor/gitignore/Global/FlexBuilder.gitignore create mode 100644 vendor/gitignore/Global/GPG.gitignore create mode 100644 vendor/gitignore/Global/IPythonNotebook.gitignore create mode 100644 vendor/gitignore/Global/JDeveloper.gitignore create mode 100644 vendor/gitignore/Global/JetBrains.gitignore create mode 100644 vendor/gitignore/Global/KDevelop4.gitignore create mode 100644 vendor/gitignore/Global/Kate.gitignore create mode 100644 vendor/gitignore/Global/Lazarus.gitignore create mode 100644 vendor/gitignore/Global/LibreOffice.gitignore create mode 100644 vendor/gitignore/Global/Linux.gitignore create mode 100644 vendor/gitignore/Global/LyX.gitignore create mode 100644 vendor/gitignore/Global/Matlab.gitignore create mode 100644 vendor/gitignore/Global/Mercurial.gitignore create mode 100644 vendor/gitignore/Global/MicrosoftOffice.gitignore create mode 100644 vendor/gitignore/Global/ModelSim.gitignore create mode 100644 vendor/gitignore/Global/Momentics.gitignore create mode 100644 vendor/gitignore/Global/MonoDevelop.gitignore create mode 100644 vendor/gitignore/Global/NetBeans.gitignore create mode 100644 vendor/gitignore/Global/Ninja.gitignore create mode 100644 vendor/gitignore/Global/NotepadPP.gitignore create mode 100644 vendor/gitignore/Global/OSX.gitignore create mode 100644 vendor/gitignore/Global/Otto.gitignore create mode 100644 vendor/gitignore/Global/Redcar.gitignore create mode 100644 vendor/gitignore/Global/Redis.gitignore create mode 100644 vendor/gitignore/Global/SBT.gitignore create mode 100644 vendor/gitignore/Global/SVN.gitignore create mode 100644 vendor/gitignore/Global/SlickEdit.gitignore create mode 100644 vendor/gitignore/Global/SublimeText.gitignore create mode 100644 vendor/gitignore/Global/SynopsysVCS.gitignore create mode 100644 vendor/gitignore/Global/Tags.gitignore create mode 100644 vendor/gitignore/Global/TextMate.gitignore create mode 100644 vendor/gitignore/Global/TortoiseGit.gitignore create mode 100644 vendor/gitignore/Global/Vagrant.gitignore create mode 100644 vendor/gitignore/Global/Vim.gitignore create mode 100644 vendor/gitignore/Global/VirtualEnv.gitignore create mode 100644 vendor/gitignore/Global/VisualStudioCode.gitignore create mode 100644 vendor/gitignore/Global/WebMethods.gitignore create mode 100644 vendor/gitignore/Global/Windows.gitignore create mode 100644 vendor/gitignore/Global/Xcode.gitignore create mode 100644 vendor/gitignore/Global/XilinxISE.gitignore create mode 100644 vendor/gitignore/Go.gitignore create mode 100644 vendor/gitignore/Gradle.gitignore create mode 100644 vendor/gitignore/Grails.gitignore create mode 100644 vendor/gitignore/Haskell.gitignore create mode 100644 vendor/gitignore/IGORPro.gitignore create mode 100644 vendor/gitignore/Idris.gitignore create mode 100644 vendor/gitignore/Java.gitignore create mode 100644 vendor/gitignore/Jboss.gitignore create mode 100644 vendor/gitignore/Jekyll.gitignore create mode 100644 vendor/gitignore/Joomla.gitignore create mode 100644 vendor/gitignore/KiCad.gitignore create mode 100644 vendor/gitignore/Kohana.gitignore create mode 100644 vendor/gitignore/LabVIEW.gitignore create mode 100644 vendor/gitignore/Laravel.gitignore create mode 100644 vendor/gitignore/Leiningen.gitignore create mode 100644 vendor/gitignore/LemonStand.gitignore create mode 100644 vendor/gitignore/Lilypond.gitignore create mode 100644 vendor/gitignore/Lithium.gitignore create mode 100644 vendor/gitignore/Lua.gitignore create mode 100644 vendor/gitignore/Magento.gitignore create mode 100644 vendor/gitignore/Maven.gitignore create mode 100644 vendor/gitignore/Mercury.gitignore create mode 100644 vendor/gitignore/MetaProgrammingSystem.gitignore create mode 100644 vendor/gitignore/Nanoc.gitignore create mode 100644 vendor/gitignore/Nim.gitignore create mode 100644 vendor/gitignore/Node.gitignore create mode 100644 vendor/gitignore/OCaml.gitignore create mode 100644 vendor/gitignore/Objective-C.gitignore create mode 100644 vendor/gitignore/Opa.gitignore create mode 100644 vendor/gitignore/OpenCart.gitignore create mode 100644 vendor/gitignore/OracleForms.gitignore create mode 100644 vendor/gitignore/Packer.gitignore create mode 100644 vendor/gitignore/Perl.gitignore create mode 100644 vendor/gitignore/Phalcon.gitignore create mode 100644 vendor/gitignore/PlayFramework.gitignore create mode 100644 vendor/gitignore/Plone.gitignore create mode 100644 vendor/gitignore/Prestashop.gitignore create mode 100644 vendor/gitignore/Processing.gitignore create mode 100644 vendor/gitignore/Python.gitignore create mode 100644 vendor/gitignore/Qooxdoo.gitignore create mode 100644 vendor/gitignore/Qt.gitignore create mode 100644 vendor/gitignore/R.gitignore create mode 100644 vendor/gitignore/ROS.gitignore create mode 100644 vendor/gitignore/Rails.gitignore create mode 100644 vendor/gitignore/RhodesRhomobile.gitignore create mode 100644 vendor/gitignore/Ruby.gitignore create mode 100644 vendor/gitignore/Rust.gitignore create mode 100644 vendor/gitignore/SCons.gitignore create mode 100644 vendor/gitignore/Sass.gitignore create mode 100644 vendor/gitignore/Scala.gitignore create mode 100644 vendor/gitignore/Scheme.gitignore create mode 100644 vendor/gitignore/Scrivener.gitignore create mode 100644 vendor/gitignore/Sdcc.gitignore create mode 100644 vendor/gitignore/SeamGen.gitignore create mode 100644 vendor/gitignore/SketchUp.gitignore create mode 100644 vendor/gitignore/Smalltalk.gitignore create mode 100644 vendor/gitignore/Stella.gitignore create mode 100644 vendor/gitignore/SugarCRM.gitignore create mode 100644 vendor/gitignore/Swift.gitignore create mode 100644 vendor/gitignore/Symfony.gitignore create mode 100644 vendor/gitignore/SymphonyCMS.gitignore create mode 100644 vendor/gitignore/TeX.gitignore create mode 100644 vendor/gitignore/Terraform.gitignore create mode 100644 vendor/gitignore/Textpattern.gitignore create mode 100644 vendor/gitignore/TurboGears2.gitignore create mode 100644 vendor/gitignore/Typo3.gitignore create mode 100644 vendor/gitignore/Umbraco.gitignore create mode 100644 vendor/gitignore/Unity.gitignore create mode 100644 vendor/gitignore/UnrealEngine.gitignore create mode 100644 vendor/gitignore/VVVV.gitignore create mode 100644 vendor/gitignore/VisualStudio.gitignore create mode 100644 vendor/gitignore/Waf.gitignore create mode 100644 vendor/gitignore/WordPress.gitignore create mode 100644 vendor/gitignore/Xojo.gitignore create mode 100644 vendor/gitignore/Yeoman.gitignore create mode 100644 vendor/gitignore/Yii.gitignore create mode 100644 vendor/gitignore/ZendFramework.gitignore create mode 100644 vendor/gitignore/Zephir.gitignore diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index dd1bbb3755..c654f8de20 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -6,6 +6,7 @@ projects_path: "/api/:version/projects.json" labels_path: "/api/:version/projects/:id/labels" license_path: "/api/:version/licenses/:key" + gitignoretext_path: "/api/:version/gitignores/:key" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -103,6 +104,12 @@ ).done (license) -> callback(license) + gitIgnoreText: (key, callback) -> + url = Api.buildUrl(Api.gitignoretext_path).replace(':key', key) + + $.get url, (gitignore) -> + callback(gitignore) + buildUrl: (url) -> url = gon.relative_url_root + url if gon.relative_url_root? return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee new file mode 100644 index 0000000000..b413f31da0 --- /dev/null +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee @@ -0,0 +1,62 @@ +class @BlobGitIgnoreSelector + constructor: (opts) -> + { + @dropdown + @editor + @wrapper = @dropdown.parents('.gitignore-selector') + @fileNameInput = $('#file_name') + @data = @dropdown.data('filenames') + } = opts + + @dropdown.glDropdown( + data: @data, + filterable: true, + selectable: true, + search: + fields: ['text'] + clicked: @onClick.bind(@) + ) + + @toggleGitIgnoreSelector() + @bindEvents() + + bindEvents: -> + @fileNameInput + .on 'keyup blur', (e) => + @toggleGitIgnoreSelector() + + toggleGitIgnoreSelector: -> + filename = @fileNameInput.val() or $('.editor-file-name').text().trim() + @wrapper.toggleClass 'hidden', filename isnt '.gitignore' + + onClick: (item) -> + @requestIgnoreFile(item.text) + + requestIgnoreFile: (name) -> + Api.gitIgnoreText name, @requestIgnoreFileSuccess.bind(@) + + requestIgnoreFileSuccess: (gitignore) -> + @editor.setValue(gitignore.content, -1) + + # Move cursor position to end of file + row = @editor.session.getLength() - 1 + column = @editor.session.getLine(row).length + @editor.gotoLine(row + 1, column) + @editor.focus() + +class @BlobGitIgnoreSelectors + constructor: (opts) -> + _this = @ + + { + @dropdowns = $('.js-gitignore-selector') + @editor + } = opts + + @dropdowns.each -> + $dropdown = $(@) + + new BlobGitIgnoreSelector( + dropdown: $dropdown, + editor: _this.editor + ) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index eea9aa972e..f2efeb3960 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -13,6 +13,7 @@ class @EditBlob @initModePanesAndLinks() new BlobLicenseSelector(@editor) + new BlobGitIgnoreSelectors(editor: @editor) initModePanesAndLinks: -> @$editModePanes = $(".js-edit-mode-pane") diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 8981f070a2..3030da4e6b 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -23,7 +23,7 @@ .file-title { @extend .monospace; - line-height: 42px; + line-height: 35px; padding-top: 7px; padding-bottom: 7px; @@ -43,7 +43,7 @@ .editor-file-name { @extend .monospace; - + float: left; margin-right: 10px; } @@ -59,7 +59,22 @@ } .encoding-selector, - .license-selector { + .license-selector, + .gitignore-selector { display: inline-block; + vertical-align: top; + font-family: $regular_font; + } + + .gitignore-selector { + + .dropdown { + line-height: 21px; + } + + .dropdown-menu-toggle{ + vertical-align: top; + width: 220px; + } } } diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index eac5014b7c..5efec92cf6 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -16,8 +16,10 @@ .license-selector.js-license-selector.hide = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name} - .gitignore-selector.js-gitignore-selector.hide - = select_tag :gitignore_template, options_for_select(Gitlab::Gitignore.all), include_blank: true, class: 'select2 gitignore-select', data: {placeholder: 'Choose a .gitignore template'} + .gitignore-selector.hidden + = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter projects", data: { filenames: Gitlab::Gitignore.all.map.with_index{ |v| {text: v.name} } } } ) + + -# = select_tag :gitignore_template, options_for_select(Gitlab::Gitignore.all.map(&:name)), include_blank: true, class: 'select2 gitignore-select', data: {placeholder: 'Choose a .gitignore template'} .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' diff --git a/vendor/gitignore/Actionscript.gitignore b/vendor/gitignore/Actionscript.gitignore new file mode 100644 index 0000000000..11e612e985 --- /dev/null +++ b/vendor/gitignore/Actionscript.gitignore @@ -0,0 +1,19 @@ +# Build and Release Folders +bin/ +bin-debug/ +bin-release/ +[Oo]bj/ # FlashDevelop obj +[Bb]in/ # FlashDevelop bin + +# Other files and folders +.settings/ + +# Executables +*.swf +*.air +*.ipa +*.apk + +# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` +# should NOT be excluded as they contain compiler settings and other important +# information for Eclipse / Flash Builder. diff --git a/vendor/gitignore/Ada.gitignore b/vendor/gitignore/Ada.gitignore new file mode 100644 index 0000000000..b4d703968a --- /dev/null +++ b/vendor/gitignore/Ada.gitignore @@ -0,0 +1,5 @@ +# Object file +*.o + +# Ada Library Information +*.ali diff --git a/vendor/gitignore/Agda.gitignore b/vendor/gitignore/Agda.gitignore new file mode 100644 index 0000000000..171a38976c --- /dev/null +++ b/vendor/gitignore/Agda.gitignore @@ -0,0 +1 @@ +*.agdai diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore new file mode 100644 index 0000000000..a836875126 --- /dev/null +++ b/vendor/gitignore/Android.gitignore @@ -0,0 +1,39 @@ +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Intellij +*.iml + +# Keystore files +*.jks diff --git a/vendor/gitignore/AppEngine.gitignore b/vendor/gitignore/AppEngine.gitignore new file mode 100644 index 0000000000..6227345453 --- /dev/null +++ b/vendor/gitignore/AppEngine.gitignore @@ -0,0 +1,2 @@ +# Google App Engine generated folder +appengine-generated/ diff --git a/vendor/gitignore/AppceleratorTitanium.gitignore b/vendor/gitignore/AppceleratorTitanium.gitignore new file mode 100644 index 0000000000..3abea55976 --- /dev/null +++ b/vendor/gitignore/AppceleratorTitanium.gitignore @@ -0,0 +1,3 @@ +# Build folder and log file +build/ +build.log diff --git a/vendor/gitignore/ArchLinuxPackages.gitignore b/vendor/gitignore/ArchLinuxPackages.gitignore new file mode 100644 index 0000000000..b73905529f --- /dev/null +++ b/vendor/gitignore/ArchLinuxPackages.gitignore @@ -0,0 +1,13 @@ +*.tar +*.tar.* +*.jar +*.exe +*.msi +*.zip +*.tgz +*.log +*.log.* +*.sig + +pkg/ +src/ diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore new file mode 100644 index 0000000000..1e9158e2a8 --- /dev/null +++ b/vendor/gitignore/Autotools.gitignore @@ -0,0 +1,18 @@ +# http://www.gnu.org/software/automake + +Makefile.in + +# http://www.gnu.org/software/autoconf + +/autom4te.cache +/autoscan.log +/autoscan-*.log +/aclocal.m4 +/compile +/config.h.in +/configure +/configure.scan +/depcomp +/install-sh +/missing +/stamp-h1 diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore new file mode 100644 index 0000000000..b8bd0267bd --- /dev/null +++ b/vendor/gitignore/C++.gitignore @@ -0,0 +1,28 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app diff --git a/vendor/gitignore/C.gitignore b/vendor/gitignore/C.gitignore new file mode 100644 index 0000000000..f805e810e5 --- /dev/null +++ b/vendor/gitignore/C.gitignore @@ -0,0 +1,33 @@ +# Object files +*.o +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su diff --git a/vendor/gitignore/CFWheels.gitignore b/vendor/gitignore/CFWheels.gitignore new file mode 100644 index 0000000000..f2fec34ff8 --- /dev/null +++ b/vendor/gitignore/CFWheels.gitignore @@ -0,0 +1,12 @@ +# unpacked plugin folders +plugins/**/* + +# files directory where uploads go +files + +# DBMigrate plugin: generated SQL +db/sql + +# AssetBundler plugin: generated bundles +javascripts/bundles +stylesheets/bundles diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore new file mode 100644 index 0000000000..b558e9afa6 --- /dev/null +++ b/vendor/gitignore/CMake.gitignore @@ -0,0 +1,6 @@ +CMakeCache.txt +CMakeFiles +CMakeScripts +Makefile +cmake_install.cmake +install_manifest.txt diff --git a/vendor/gitignore/CUDA.gitignore b/vendor/gitignore/CUDA.gitignore new file mode 100644 index 0000000000..cb385db83f --- /dev/null +++ b/vendor/gitignore/CUDA.gitignore @@ -0,0 +1,6 @@ +*.i +*.ii +*.gpu +*.ptx +*.cubin +*.fatbin diff --git a/vendor/gitignore/CakePHP.gitignore b/vendor/gitignore/CakePHP.gitignore new file mode 100644 index 0000000000..c6597e4eab --- /dev/null +++ b/vendor/gitignore/CakePHP.gitignore @@ -0,0 +1,25 @@ +# CakePHP 3 + +/vendor/* +/config/app.php + +/tmp/cache/models/* +!/tmp/cache/models/empty +/tmp/cache/persistent/* +!/tmp/cache/persistent/empty +/tmp/cache/views/* +!/tmp/cache/views/empty +/tmp/sessions/* +!/tmp/sessions/empty +/tmp/tests/* +!/tmp/tests/empty + +/logs/* +!/logs/empty + +# CakePHP 2 + +/app/tmp/* +/app/Config/core.php +/app/Config/database.php +/vendors/* diff --git a/vendor/gitignore/ChefCookbook.gitignore b/vendor/gitignore/ChefCookbook.gitignore new file mode 100644 index 0000000000..5ee7b7a9a1 --- /dev/null +++ b/vendor/gitignore/ChefCookbook.gitignore @@ -0,0 +1,9 @@ +.vagrant +/cookbooks + +# Bundler +bin/* +.bundle/* + +.kitchen/ +.kitchen.local.yml diff --git a/vendor/gitignore/Clojure.gitignore b/vendor/gitignore/Clojure.gitignore new file mode 120000 index 0000000000..7657a270c4 --- /dev/null +++ b/vendor/gitignore/Clojure.gitignore @@ -0,0 +1 @@ +Leiningen.gitignore \ No newline at end of file diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore new file mode 100644 index 0000000000..0f77d9e1d1 --- /dev/null +++ b/vendor/gitignore/CodeIgniter.gitignore @@ -0,0 +1,6 @@ +*/config/development +*/logs/log-*.php +!*/logs/index.html +*/cache/* +!*/cache/index.html +!*/cache/.htaccess diff --git a/vendor/gitignore/CommonLisp.gitignore b/vendor/gitignore/CommonLisp.gitignore new file mode 100644 index 0000000000..4806e580b6 --- /dev/null +++ b/vendor/gitignore/CommonLisp.gitignore @@ -0,0 +1,3 @@ +*.FASL +*.fasl +*.lisp-temp diff --git a/vendor/gitignore/Composer.gitignore b/vendor/gitignore/Composer.gitignore new file mode 100644 index 0000000000..c422267842 --- /dev/null +++ b/vendor/gitignore/Composer.gitignore @@ -0,0 +1,6 @@ +composer.phar +/vendor/ + +# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +# composer.lock diff --git a/vendor/gitignore/Concrete5.gitignore b/vendor/gitignore/Concrete5.gitignore new file mode 100644 index 0000000000..1fe53611e5 --- /dev/null +++ b/vendor/gitignore/Concrete5.gitignore @@ -0,0 +1,4 @@ +config/site.php +files/cache/* +files/tmp/* +.htaccess diff --git a/vendor/gitignore/Coq.gitignore b/vendor/gitignore/Coq.gitignore new file mode 100644 index 0000000000..d3083b3a60 --- /dev/null +++ b/vendor/gitignore/Coq.gitignore @@ -0,0 +1,3 @@ +*.vo +*.glob +*.v.d diff --git a/vendor/gitignore/CraftCMS.gitignore b/vendor/gitignore/CraftCMS.gitignore new file mode 100644 index 0000000000..a70d4772c4 --- /dev/null +++ b/vendor/gitignore/CraftCMS.gitignore @@ -0,0 +1,3 @@ +# Craft Storage (cache) [http://buildwithcraft.com/help/craft-storage-gitignore] +/craft/storage/* +!/craft/storage/logo/* \ No newline at end of file diff --git a/vendor/gitignore/D.gitignore b/vendor/gitignore/D.gitignore new file mode 100644 index 0000000000..b4433f8a51 --- /dev/null +++ b/vendor/gitignore/D.gitignore @@ -0,0 +1,20 @@ +# Compiled Object files +*.o +*.obj + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Compiled Static libraries +*.a +*.lib + +# Executables +*.exe + +# DUB +.dub +docs.json +__dummy.html diff --git a/vendor/gitignore/DM.gitignore b/vendor/gitignore/DM.gitignore new file mode 100644 index 0000000000..ba5abdab83 --- /dev/null +++ b/vendor/gitignore/DM.gitignore @@ -0,0 +1,5 @@ +*.dmb +*.rsc +*.int +*.lk +*.zip diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore new file mode 100644 index 0000000000..7c28044164 --- /dev/null +++ b/vendor/gitignore/Dart.gitignore @@ -0,0 +1,27 @@ +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.buildlog +.packages +.project +.pub/ +build/ +**/packages/ + +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) +*.dart.js +*.part.js +*.js.deps +*.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock diff --git a/vendor/gitignore/Delphi.gitignore b/vendor/gitignore/Delphi.gitignore new file mode 100644 index 0000000000..19864c6bbe --- /dev/null +++ b/vendor/gitignore/Delphi.gitignore @@ -0,0 +1,66 @@ +# Uncomment these types if you want even more clean repository. But be careful. +# It can make harm to an existing project source. Read explanations below. +# +# Resource files are binaries containing manifest, project icon and version info. +# They can not be viewed as text or compared by diff-tools. Consider replacing them with .rc files. +#*.res +# +# Type library file (binary). In old Delphi versions it should be stored. +# Since Delphi 2009 it is produced from .ridl file and can safely be ignored. +#*.tlb +# +# Diagram Portfolio file. Used by the diagram editor up to Delphi 7. +# Uncomment this if you are not using diagrams or use newer Delphi version. +#*.ddp +# +# Visual LiveBindings file. Added in Delphi XE2. +# Uncomment this if you are not using LiveBindings Designer. +#*.vlb +# +# Deployment Manager configuration file for your project. Added in Delphi XE2. +# Uncomment this if it is not mobile development and you do not use remote debug feature. +#*.deployproj +# +# C++ object files produced when C/C++ Output file generation is configured. +# Uncomment this if you are not using external objects (zlib library for example). +#*.obj +# + +# Delphi compiler-generated binaries (safe to delete) +*.exe +*.dll +*.bpl +*.bpi +*.dcp +*.so +*.apk +*.drc +*.map +*.dres +*.rsm +*.tds +*.dcu +*.lib +*.a +*.o +*.ocx + +# Delphi autogenerated files (duplicated info) +*.cfg +*.hpp +*Resource.rc + +# Delphi local files (user-specific info) +*.local +*.identcache +*.projdata +*.tvsconfig +*.dsk + +# Delphi history and backups +__history/ +__recovery/ +*.~* + +# Castalia statistics file (since XE7 Castalia is distributed with Delphi) +*.stat diff --git a/vendor/gitignore/Drupal.gitignore b/vendor/gitignore/Drupal.gitignore new file mode 100644 index 0000000000..0d2fe537f4 --- /dev/null +++ b/vendor/gitignore/Drupal.gitignore @@ -0,0 +1,36 @@ +# Ignore configuration files that may contain sensitive information. +sites/*/*settings*.php + +# Ignore paths that contain generated content. +files/ +sites/*/files +sites/*/private + +# Ignore default text files +robots.txt +/CHANGELOG.txt +/COPYRIGHT.txt +/INSTALL*.txt +/LICENSE.txt +/MAINTAINERS.txt +/UPGRADE.txt +/README.txt +sites/README.txt +sites/all/modules/README.txt +sites/all/themes/README.txt + +# Ignore everything but the "sites" folder ( for non core developer ) +.htaccess +web.config +authorize.php +cron.php +index.php +install.php +update.php +xmlrpc.php +/includes +/misc +/modules +/profiles +/scripts +/themes diff --git a/vendor/gitignore/EPiServer.gitignore b/vendor/gitignore/EPiServer.gitignore new file mode 100644 index 0000000000..97037de743 --- /dev/null +++ b/vendor/gitignore/EPiServer.gitignore @@ -0,0 +1,4 @@ +###################### +## EPiServer Files +###################### +*License.config diff --git a/vendor/gitignore/Eagle.gitignore b/vendor/gitignore/Eagle.gitignore new file mode 100644 index 0000000000..9ced126026 --- /dev/null +++ b/vendor/gitignore/Eagle.gitignore @@ -0,0 +1,44 @@ +# Ignore list for Eagle, a PCB layout tool + +# Backup files +*.s#? +*.b#? +*.l#? + +# Eagle project file +# It contains a serial number and references to the file structure +# on your computer. +# comment the following line if you want to have your project file included. +eagle.epf + +# Autorouter files +*.pro +*.job + +# CAM files +*.$$$ +*.cmp +*.ly2 +*.l15 +*.sol +*.plc +*.stc +*.sts +*.crc +*.crs + +*.dri +*.drl +*.gpi +*.pls + +*.drd +*.drd.* + +*.info + +*.eps + +# file locks introduced since 7.x +*.lck + diff --git a/vendor/gitignore/Elisp.gitignore b/vendor/gitignore/Elisp.gitignore new file mode 100644 index 0000000000..9b4291b7fe --- /dev/null +++ b/vendor/gitignore/Elisp.gitignore @@ -0,0 +1,5 @@ +# Compiled +*.elc + +# Packaging +.cask diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore new file mode 100644 index 0000000000..755b605549 --- /dev/null +++ b/vendor/gitignore/Elixir.gitignore @@ -0,0 +1,5 @@ +/_build +/cover +/deps +erl_crash.dump +*.ez diff --git a/vendor/gitignore/Elm.gitignore b/vendor/gitignore/Elm.gitignore new file mode 100644 index 0000000000..a594364e2c --- /dev/null +++ b/vendor/gitignore/Elm.gitignore @@ -0,0 +1,4 @@ +# elm-package generated files +elm-stuff/ +# elm-repl generated files +repl-temp-* diff --git a/vendor/gitignore/Erlang.gitignore b/vendor/gitignore/Erlang.gitignore new file mode 100644 index 0000000000..8e46d5a07f --- /dev/null +++ b/vendor/gitignore/Erlang.gitignore @@ -0,0 +1,10 @@ +.eunit +deps +*.o +*.beam +*.plt +erl_crash.dump +ebin +rel/example_project +.concrete/DEV_MODE +.rebar diff --git a/vendor/gitignore/ExpressionEngine.gitignore b/vendor/gitignore/ExpressionEngine.gitignore new file mode 100644 index 0000000000..314e4df123 --- /dev/null +++ b/vendor/gitignore/ExpressionEngine.gitignore @@ -0,0 +1,19 @@ +.DS_Store + +# Images +images/avatars/ +images/captchas/ +images/smileys/ +images/member_photos/ +images/signature_attachments/ +images/pm_attachments/ + +# For security do not publish the following files +system/expressionengine/config/database.php +system/expressionengine/config/config.php + +# Caches +sized/ +thumbs/ +_thumbs/ +*/expressionengine/cache/* diff --git a/vendor/gitignore/ExtJs.gitignore b/vendor/gitignore/ExtJs.gitignore new file mode 100644 index 0000000000..5ffc21546e --- /dev/null +++ b/vendor/gitignore/ExtJs.gitignore @@ -0,0 +1,4 @@ +.architect +bootstrap.json +build/ +ext/ diff --git a/vendor/gitignore/Fancy.gitignore b/vendor/gitignore/Fancy.gitignore new file mode 100644 index 0000000000..70d6e631e5 --- /dev/null +++ b/vendor/gitignore/Fancy.gitignore @@ -0,0 +1,2 @@ +*.rbc +*.fyc diff --git a/vendor/gitignore/Finale.gitignore b/vendor/gitignore/Finale.gitignore new file mode 100644 index 0000000000..7ef08e0c34 --- /dev/null +++ b/vendor/gitignore/Finale.gitignore @@ -0,0 +1,13 @@ +*.bak +*.db +*.avi +*.pdf +*.ps +*.mid +*.midi +*.mp3 +*.aif +*.wav +# Some versions of Finale have a bug and randomly save extra copies of +# the music source as " copy.mus" +*copy.mus diff --git a/vendor/gitignore/ForceDotCom.gitignore b/vendor/gitignore/ForceDotCom.gitignore new file mode 100644 index 0000000000..3933cd4dd5 --- /dev/null +++ b/vendor/gitignore/ForceDotCom.gitignore @@ -0,0 +1,4 @@ +.project +.settings +salesforce.schema +Referenced Packages diff --git a/vendor/gitignore/Fortran.gitignore b/vendor/gitignore/Fortran.gitignore new file mode 120000 index 0000000000..5daba98a3e --- /dev/null +++ b/vendor/gitignore/Fortran.gitignore @@ -0,0 +1 @@ +C++.gitignore \ No newline at end of file diff --git a/vendor/gitignore/FuelPHP.gitignore b/vendor/gitignore/FuelPHP.gitignore new file mode 100644 index 0000000000..d69f71f433 --- /dev/null +++ b/vendor/gitignore/FuelPHP.gitignore @@ -0,0 +1,21 @@ +# the composer package lock file and install directory +# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +# /composer.lock +/fuel/vendor + +# the fuelphp document +/docs/ + +# you may install these packages with `oil package`. +# http://fuelphp.com/docs/packages/oil/package.html +# /fuel/packages/auth/ +# /fuel/packages/email/ +# /fuel/packages/oil/ +# /fuel/packages/orm/ +# /fuel/packages/parser/ + +# dynamically generated files +/fuel/app/logs/*/*/* +/fuel/app/cache/*/* +/fuel/app/config/crypt.php diff --git a/vendor/gitignore/GWT.gitignore b/vendor/gitignore/GWT.gitignore new file mode 100644 index 0000000000..07704e54bb --- /dev/null +++ b/vendor/gitignore/GWT.gitignore @@ -0,0 +1,28 @@ +*.class + +# Package Files # +*.jar +*.war + +# gwt caches and compiled units # +war/gwt_bree/ +gwt-unitCache/ + +# boilerplate generated classes # +.apt_generated/ + +# more caches and things from deploy # +war/WEB-INF/deploy/ +war/WEB-INF/classes/ + +#compilation logs +.gwt/ + +#caching for already compiled files +gwt-unitCache/ + +#gwt junit compilation files +www-test/ + +#old GWT (1.5) created this dir +.gwt-tmp/ diff --git a/vendor/gitignore/Gcov.gitignore b/vendor/gitignore/Gcov.gitignore new file mode 100644 index 0000000000..a6451430e1 --- /dev/null +++ b/vendor/gitignore/Gcov.gitignore @@ -0,0 +1,5 @@ +# gcc coverage testing tool files + +*.gcno +*.gcda +*.gcov diff --git a/vendor/gitignore/GitBook.gitignore b/vendor/gitignore/GitBook.gitignore new file mode 100644 index 0000000000..4cb12d8db7 --- /dev/null +++ b/vendor/gitignore/GitBook.gitignore @@ -0,0 +1,16 @@ +# Node rules: +## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +## Dependency directory +## Commenting this out is preferred by some people, see +## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git +node_modules + +# Book build output +_book + +# eBook build output +*.epub +*.mobi +*.pdf diff --git a/vendor/gitignore/Global/Anjuta.gitignore b/vendor/gitignore/Global/Anjuta.gitignore new file mode 100644 index 0000000000..20dd42c53e --- /dev/null +++ b/vendor/gitignore/Global/Anjuta.gitignore @@ -0,0 +1,3 @@ +# Local configuration folder and symbol database +/.anjuta/ +/.anjuta_sym_db.db diff --git a/vendor/gitignore/Global/Archives.gitignore b/vendor/gitignore/Global/Archives.gitignore new file mode 100644 index 0000000000..e9eda68baf --- /dev/null +++ b/vendor/gitignore/Global/Archives.gitignore @@ -0,0 +1,27 @@ +# It's better to unpack these files and commit the raw source because +# git has its own built in compression methods. +*.7z +*.jar +*.rar +*.zip +*.gz +*.bzip +*.bz2 +*.xz +*.lzma +*.cab + +#packing-only formats +*.iso +*.tar + +#package management formats +*.dmg +*.xpi +*.gem +*.egg +*.deb +*.rpm +*.msi +*.msm +*.msp diff --git a/vendor/gitignore/Global/BricxCC.gitignore b/vendor/gitignore/Global/BricxCC.gitignore new file mode 100644 index 0000000000..c1d16a46c9 --- /dev/null +++ b/vendor/gitignore/Global/BricxCC.gitignore @@ -0,0 +1,4 @@ +# Bricx Command Center IDE +# http://bricxcc.sourceforge.net +*.bak +*.sym diff --git a/vendor/gitignore/Global/CVS.gitignore b/vendor/gitignore/Global/CVS.gitignore new file mode 100644 index 0000000000..1695352e14 --- /dev/null +++ b/vendor/gitignore/Global/CVS.gitignore @@ -0,0 +1,4 @@ +/CVS/* +**/CVS/* +.cvsignore +*/.cvsignore diff --git a/vendor/gitignore/Global/Calabash.gitignore b/vendor/gitignore/Global/Calabash.gitignore new file mode 100644 index 0000000000..8a75b329dc --- /dev/null +++ b/vendor/gitignore/Global/Calabash.gitignore @@ -0,0 +1,10 @@ +# Calabash / Cucumber +rerun/ +reports/ +screenshots/ +screenshot*.png +test-servers/ + +# bundler +.bundle +vendor diff --git a/vendor/gitignore/Global/Cloud9.gitignore b/vendor/gitignore/Global/Cloud9.gitignore new file mode 100644 index 0000000000..3f4384df50 --- /dev/null +++ b/vendor/gitignore/Global/Cloud9.gitignore @@ -0,0 +1,3 @@ +# Cloud9 IDE - http://c9.io +.c9revisions +.c9 diff --git a/vendor/gitignore/Global/CodeKit.gitignore b/vendor/gitignore/Global/CodeKit.gitignore new file mode 100644 index 0000000000..bd9e67fcca --- /dev/null +++ b/vendor/gitignore/Global/CodeKit.gitignore @@ -0,0 +1,3 @@ +# General CodeKit files to ignore +config.codekit +/min diff --git a/vendor/gitignore/Global/DartEditor.gitignore b/vendor/gitignore/Global/DartEditor.gitignore new file mode 100644 index 0000000000..948920b420 --- /dev/null +++ b/vendor/gitignore/Global/DartEditor.gitignore @@ -0,0 +1,2 @@ +.project +.buildlog diff --git a/vendor/gitignore/Global/Dreamweaver.gitignore b/vendor/gitignore/Global/Dreamweaver.gitignore new file mode 100644 index 0000000000..0621a3d53b --- /dev/null +++ b/vendor/gitignore/Global/Dreamweaver.gitignore @@ -0,0 +1,7 @@ +# DW Dreamweaver added files +_notes +_compareTemp +configs/ +dwsync.xml +dw_php_codehinting.config +*.mno diff --git a/vendor/gitignore/Global/Dropbox.gitignore b/vendor/gitignore/Global/Dropbox.gitignore new file mode 100644 index 0000000000..40f4a469d2 --- /dev/null +++ b/vendor/gitignore/Global/Dropbox.gitignore @@ -0,0 +1,4 @@ +# Dropbox settings and caches +.dropbox +.dropbox.attr +.dropbox.cache diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore new file mode 100644 index 0000000000..31c9fb3116 --- /dev/null +++ b/vendor/gitignore/Global/Eclipse.gitignore @@ -0,0 +1,51 @@ + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ diff --git a/vendor/gitignore/Global/EiffelStudio.gitignore b/vendor/gitignore/Global/EiffelStudio.gitignore new file mode 100644 index 0000000000..f41b4f7021 --- /dev/null +++ b/vendor/gitignore/Global/EiffelStudio.gitignore @@ -0,0 +1,2 @@ +# The compilation directory +EIFGENs diff --git a/vendor/gitignore/Global/Emacs.gitignore b/vendor/gitignore/Global/Emacs.gitignore new file mode 100644 index 0000000000..0c96c9ad06 --- /dev/null +++ b/vendor/gitignore/Global/Emacs.gitignore @@ -0,0 +1,42 @@ +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile \ No newline at end of file diff --git a/vendor/gitignore/Global/Ensime.gitignore b/vendor/gitignore/Global/Ensime.gitignore new file mode 100644 index 0000000000..f2daebb9f4 --- /dev/null +++ b/vendor/gitignore/Global/Ensime.gitignore @@ -0,0 +1,4 @@ +# Ensime specific +.ensime +.ensime_cache/ +.ensime_lucene/ diff --git a/vendor/gitignore/Global/Espresso.gitignore b/vendor/gitignore/Global/Espresso.gitignore new file mode 100644 index 0000000000..1234530b5b --- /dev/null +++ b/vendor/gitignore/Global/Espresso.gitignore @@ -0,0 +1 @@ +*.esproj diff --git a/vendor/gitignore/Global/FlexBuilder.gitignore b/vendor/gitignore/Global/FlexBuilder.gitignore new file mode 100644 index 0000000000..bbbfb91d9e --- /dev/null +++ b/vendor/gitignore/Global/FlexBuilder.gitignore @@ -0,0 +1,3 @@ +bin/ +bin-debug/ +bin-release/ diff --git a/vendor/gitignore/Global/GPG.gitignore b/vendor/gitignore/Global/GPG.gitignore new file mode 100644 index 0000000000..7740a01538 --- /dev/null +++ b/vendor/gitignore/Global/GPG.gitignore @@ -0,0 +1,2 @@ +secring.* + diff --git a/vendor/gitignore/Global/IPythonNotebook.gitignore b/vendor/gitignore/Global/IPythonNotebook.gitignore new file mode 100644 index 0000000000..27c13510bf --- /dev/null +++ b/vendor/gitignore/Global/IPythonNotebook.gitignore @@ -0,0 +1,2 @@ +# Temporary data +.ipynb_checkpoints/ diff --git a/vendor/gitignore/Global/JDeveloper.gitignore b/vendor/gitignore/Global/JDeveloper.gitignore new file mode 100644 index 0000000000..5bba6f3773 --- /dev/null +++ b/vendor/gitignore/Global/JDeveloper.gitignore @@ -0,0 +1,13 @@ +# default application storage directory used by the IDE Performance Cache feature +.data/ + +# used for ADF styles caching +temp/ + +# default output directories +classes/ +deploy/ +javadoc/ + +# lock file, a part of Oracle Credential Store Framework +cwallet.sso.lck \ No newline at end of file diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore new file mode 100644 index 0000000000..ea83a5eb62 --- /dev/null +++ b/vendor/gitignore/Global/JetBrains.gitignore @@ -0,0 +1,44 @@ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties diff --git a/vendor/gitignore/Global/KDevelop4.gitignore b/vendor/gitignore/Global/KDevelop4.gitignore new file mode 100644 index 0000000000..7ac57b1add --- /dev/null +++ b/vendor/gitignore/Global/KDevelop4.gitignore @@ -0,0 +1,2 @@ +*.kdev4 +.kdev4/ diff --git a/vendor/gitignore/Global/Kate.gitignore b/vendor/gitignore/Global/Kate.gitignore new file mode 100644 index 0000000000..7ff06ce539 --- /dev/null +++ b/vendor/gitignore/Global/Kate.gitignore @@ -0,0 +1,3 @@ +# Swap Files # +.*.kate-swp +.swp.* diff --git a/vendor/gitignore/Global/Lazarus.gitignore b/vendor/gitignore/Global/Lazarus.gitignore new file mode 100644 index 0000000000..b32943f1c6 --- /dev/null +++ b/vendor/gitignore/Global/Lazarus.gitignore @@ -0,0 +1,30 @@ +# Lazarus compiler-generated binaries (safe to delete) +*.exe +*.dll +*.so +*.dylib +*.lrs +*.res +*.compiled +*.dbg +*.ppu +*.o +*.or +*.a + +# Lazarus autogenerated files (duplicated info) +*.rst +*.rsj +*.lrt + +# Lazarus local files (user-specific info) +*.lps + +# Lazarus backups and unit output folders. +# These can be changed by user in Lazarus/project options. +backup/ +*.bak +lib/ + +# Application bundle for Mac OS +*.app/ diff --git a/vendor/gitignore/Global/LibreOffice.gitignore b/vendor/gitignore/Global/LibreOffice.gitignore new file mode 100644 index 0000000000..586beac91d --- /dev/null +++ b/vendor/gitignore/Global/LibreOffice.gitignore @@ -0,0 +1,2 @@ +# LibreOffice locks +.~lock.*# diff --git a/vendor/gitignore/Global/Linux.gitignore b/vendor/gitignore/Global/Linux.gitignore new file mode 100644 index 0000000000..cc9586893b --- /dev/null +++ b/vendor/gitignore/Global/Linux.gitignore @@ -0,0 +1,10 @@ +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* diff --git a/vendor/gitignore/Global/LyX.gitignore b/vendor/gitignore/Global/LyX.gitignore new file mode 100644 index 0000000000..8efe0195cf --- /dev/null +++ b/vendor/gitignore/Global/LyX.gitignore @@ -0,0 +1,4 @@ +# Ignore LyX backup and autosave files +# http://www.lyx.org/ +*.lyx~ +*.lyx# diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore new file mode 100644 index 0000000000..32a5ad4c77 --- /dev/null +++ b/vendor/gitignore/Global/Matlab.gitignore @@ -0,0 +1,19 @@ +##--------------------------------------------------- +## Remove autosaves generated by the Matlab editor +## We have git for backups! +##--------------------------------------------------- + +# Windows default autosave extension +*.asv + +# OSX / *nix default autosave extension +*.m~ + +# Compiled MEX binaries (all platforms) +*.mex* + +# Simulink Code Generation +slprj/ + +# Session info +octave-workspace diff --git a/vendor/gitignore/Global/Mercurial.gitignore b/vendor/gitignore/Global/Mercurial.gitignore new file mode 100644 index 0000000000..e65d113798 --- /dev/null +++ b/vendor/gitignore/Global/Mercurial.gitignore @@ -0,0 +1,6 @@ +.hg/ +.hgignore +.hgsigs +.hgsub +.hgsubstate +.hgtags diff --git a/vendor/gitignore/Global/MicrosoftOffice.gitignore b/vendor/gitignore/Global/MicrosoftOffice.gitignore new file mode 100644 index 0000000000..cb89174566 --- /dev/null +++ b/vendor/gitignore/Global/MicrosoftOffice.gitignore @@ -0,0 +1,16 @@ +*.tmp + +# Word temporary +~$*.doc* + +# Excel temporary +~$*.xls* + +# Excel Backup File +*.xlk + +# PowerPoint temporary +~$*.ppt* + +# Visio autosave temporary files +*.~vsdx diff --git a/vendor/gitignore/Global/ModelSim.gitignore b/vendor/gitignore/Global/ModelSim.gitignore new file mode 100644 index 0000000000..46592b8643 --- /dev/null +++ b/vendor/gitignore/Global/ModelSim.gitignore @@ -0,0 +1,23 @@ +# ignore ModelSim generated files and directories (temp files and so on) +[_@]* + +# ignore compilation output of ModelSim +*.mti +*.dat +*.dbs +*.psm +*.bak +*.cmp +*.jpg +*.html +*.bsf + +# ignore simulation output of ModelSim +wlf* +*.wlf +*.vstf +*.ucdb +cov*/ +transcript* +sc_dpiheader.h +vsim.dbg diff --git a/vendor/gitignore/Global/Momentics.gitignore b/vendor/gitignore/Global/Momentics.gitignore new file mode 100644 index 0000000000..b14db2d864 --- /dev/null +++ b/vendor/gitignore/Global/Momentics.gitignore @@ -0,0 +1,8 @@ +# Built files +x86/ +arm/ +arm-p/ +translations/*.qm + +# IDE settings +.settings/ diff --git a/vendor/gitignore/Global/MonoDevelop.gitignore b/vendor/gitignore/Global/MonoDevelop.gitignore new file mode 100644 index 0000000000..ef38d06b08 --- /dev/null +++ b/vendor/gitignore/Global/MonoDevelop.gitignore @@ -0,0 +1,8 @@ +#User Specific +*.userprefs +*.usertasks + +#Mono Project Files +*.pidb +*.resources +test-results/ diff --git a/vendor/gitignore/Global/NetBeans.gitignore b/vendor/gitignore/Global/NetBeans.gitignore new file mode 100644 index 0000000000..520d91ff58 --- /dev/null +++ b/vendor/gitignore/Global/NetBeans.gitignore @@ -0,0 +1,7 @@ +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +.nb-gradle/ diff --git a/vendor/gitignore/Global/Ninja.gitignore b/vendor/gitignore/Global/Ninja.gitignore new file mode 100644 index 0000000000..50e58f24cc --- /dev/null +++ b/vendor/gitignore/Global/Ninja.gitignore @@ -0,0 +1,2 @@ +.ninja_deps +.ninja_log diff --git a/vendor/gitignore/Global/NotepadPP.gitignore b/vendor/gitignore/Global/NotepadPP.gitignore new file mode 100644 index 0000000000..8fbda83a2c --- /dev/null +++ b/vendor/gitignore/Global/NotepadPP.gitignore @@ -0,0 +1,2 @@ +# Notepad++ backups # +*.bak diff --git a/vendor/gitignore/Global/OSX.gitignore b/vendor/gitignore/Global/OSX.gitignore new file mode 100644 index 0000000000..660b31353e --- /dev/null +++ b/vendor/gitignore/Global/OSX.gitignore @@ -0,0 +1,24 @@ +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/vendor/gitignore/Global/Otto.gitignore b/vendor/gitignore/Global/Otto.gitignore new file mode 100644 index 0000000000..5aa263f9db --- /dev/null +++ b/vendor/gitignore/Global/Otto.gitignore @@ -0,0 +1 @@ +.otto/ diff --git a/vendor/gitignore/Global/Redcar.gitignore b/vendor/gitignore/Global/Redcar.gitignore new file mode 100644 index 0000000000..b4a9d1d68e --- /dev/null +++ b/vendor/gitignore/Global/Redcar.gitignore @@ -0,0 +1 @@ +.redcar diff --git a/vendor/gitignore/Global/Redis.gitignore b/vendor/gitignore/Global/Redis.gitignore new file mode 100644 index 0000000000..57c1c230f9 --- /dev/null +++ b/vendor/gitignore/Global/Redis.gitignore @@ -0,0 +1,3 @@ +# Ignore redis binary dump (dump.rdb) files + +*.rdb diff --git a/vendor/gitignore/Global/SBT.gitignore b/vendor/gitignore/Global/SBT.gitignore new file mode 100644 index 0000000000..970d897c75 --- /dev/null +++ b/vendor/gitignore/Global/SBT.gitignore @@ -0,0 +1,9 @@ +# Simple Build Tool +# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control + +target/ +lib_managed/ +src_managed/ +project/boot/ +.history +.cache diff --git a/vendor/gitignore/Global/SVN.gitignore b/vendor/gitignore/Global/SVN.gitignore new file mode 100644 index 0000000000..1b53ace613 --- /dev/null +++ b/vendor/gitignore/Global/SVN.gitignore @@ -0,0 +1 @@ +.svn/ diff --git a/vendor/gitignore/Global/SlickEdit.gitignore b/vendor/gitignore/Global/SlickEdit.gitignore new file mode 100644 index 0000000000..f30b8da457 --- /dev/null +++ b/vendor/gitignore/Global/SlickEdit.gitignore @@ -0,0 +1,11 @@ +# SlickEdit workspace and project files are ignored by default because +# typically they are considered to be developer-specific and not part of a +# project. +*.vpw +*.vpj + +# SlickEdit workspace history and tag files always contain user-specific +# data so they should not be stored in a repository. +*.vpwhistu +*.vpwhist +*.vtg diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore new file mode 100644 index 0000000000..1d4e613759 --- /dev/null +++ b/vendor/gitignore/Global/SublimeText.gitignore @@ -0,0 +1,14 @@ +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json diff --git a/vendor/gitignore/Global/SynopsysVCS.gitignore b/vendor/gitignore/Global/SynopsysVCS.gitignore new file mode 100644 index 0000000000..eed2432fb7 --- /dev/null +++ b/vendor/gitignore/Global/SynopsysVCS.gitignore @@ -0,0 +1,36 @@ +# Waveform formats +*.vcd +*.vpd +*.evcd +*.fsdb + +# Default name of the simulation executable. A different name can be +# specified with this switch (the associated daidir database name is +# also taken from here): -o / +simv + +# Generated for Verilog and VHDL top configs +simv.daidir/ +simv.db.dir/ + +# Infrastructure necessary to co-simulate SystemC models with +# Verilog/VHDL models. An alternate directory may be specified with this +# switch: -Mdir= +csrc/ + +# Log file - the following switch allows to specify the file that will be +# used to write all messages from simulation: -l +*.log + +# Coverage results (generated with urg) and database location. The +# following switch can also be used: urg -dir .vdb +simv.vdb/ +urgReport/ + +# DVE and UCLI related files. +DVEfiles/ +ucli.key + +# When the design is elaborated for DirectC, the following file is created +# with declarations for C/C++ functions. +vc_hdrs.h diff --git a/vendor/gitignore/Global/Tags.gitignore b/vendor/gitignore/Global/Tags.gitignore new file mode 100644 index 0000000000..c0318165a2 --- /dev/null +++ b/vendor/gitignore/Global/Tags.gitignore @@ -0,0 +1,16 @@ +# Ignore tags created by etags, ctags, gtags (GNU global) and cscope +TAGS +.TAGS +!TAGS/ +tags +.tags +!tags/ +gtags.files +GTAGS +GRTAGS +GPATH +cscope.files +cscope.out +cscope.in.out +cscope.po.out + diff --git a/vendor/gitignore/Global/TextMate.gitignore b/vendor/gitignore/Global/TextMate.gitignore new file mode 100644 index 0000000000..41e8d07a94 --- /dev/null +++ b/vendor/gitignore/Global/TextMate.gitignore @@ -0,0 +1,3 @@ +*.tmproj +*.tmproject +tmtags diff --git a/vendor/gitignore/Global/TortoiseGit.gitignore b/vendor/gitignore/Global/TortoiseGit.gitignore new file mode 100644 index 0000000000..db89590a62 --- /dev/null +++ b/vendor/gitignore/Global/TortoiseGit.gitignore @@ -0,0 +1,2 @@ +# Project-level settings +/.tgitconfig diff --git a/vendor/gitignore/Global/Vagrant.gitignore b/vendor/gitignore/Global/Vagrant.gitignore new file mode 100644 index 0000000000..a977916f65 --- /dev/null +++ b/vendor/gitignore/Global/Vagrant.gitignore @@ -0,0 +1 @@ +.vagrant/ diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore new file mode 100644 index 0000000000..bdc04a0b52 --- /dev/null +++ b/vendor/gitignore/Global/Vim.gitignore @@ -0,0 +1,10 @@ +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags diff --git a/vendor/gitignore/Global/VirtualEnv.gitignore b/vendor/gitignore/Global/VirtualEnv.gitignore new file mode 100644 index 0000000000..b2c22f2af7 --- /dev/null +++ b/vendor/gitignore/Global/VirtualEnv.gitignore @@ -0,0 +1,12 @@ +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +.Python +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.venv +pip-selfcheck.json diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore new file mode 100644 index 0000000000..faa18382a3 --- /dev/null +++ b/vendor/gitignore/Global/VisualStudioCode.gitignore @@ -0,0 +1,2 @@ +.vscode + diff --git a/vendor/gitignore/Global/WebMethods.gitignore b/vendor/gitignore/Global/WebMethods.gitignore new file mode 100644 index 0000000000..b383c25ca3 --- /dev/null +++ b/vendor/gitignore/Global/WebMethods.gitignore @@ -0,0 +1,14 @@ +**/IntegrationServer/datastore/ +**/IntegrationServer/db/ +**/IntegrationServer/DocumentStore/ +**/IntegrationServer/lib/ +**/IntegrationServer/logs/ +**/IntegrationServer/replicate/ +**/IntegrationServer/sdk/ +**/IntegrationServer/support/ +**/IntegrationServer/update/ +**/IntegrationServer/userFtpRoot/ +**/IntegrationServer/web/ +**/IntegrationServer/WmRepository4/ +**/IntegrationServer/XAStore/ +**/IntegrationServer/packages/Wm*/ diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore new file mode 100644 index 0000000000..a0d31452b0 --- /dev/null +++ b/vendor/gitignore/Global/Windows.gitignore @@ -0,0 +1,18 @@ +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore new file mode 100644 index 0000000000..37de8bb479 --- /dev/null +++ b/vendor/gitignore/Global/Xcode.gitignore @@ -0,0 +1,23 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint diff --git a/vendor/gitignore/Global/XilinxISE.gitignore b/vendor/gitignore/Global/XilinxISE.gitignore new file mode 100644 index 0000000000..4475f843da --- /dev/null +++ b/vendor/gitignore/Global/XilinxISE.gitignore @@ -0,0 +1,67 @@ +# intermediate build files +*.bgn +*.bit +*.bld +*.cmd_log +*.drc +*.ll +*.lso +*.msd +*.msk +*.ncd +*.ngc +*.ngd +*.ngr +*.pad +*.par +*.pcf +*.prj +*.ptwx +*.rbb +*.rbd +*.stx +*.syr +*.twr +*.twx +*.unroutes +*.ut +*.xpi +*.xst +*_bitgen.xwbt +*_envsettings.html +*_map.map +*_map.mrp +*_map.ngm +*_map.xrpt +*_ngdbuild.xrpt +*_pad.csv +*_pad.txt +*_par.xrpt +*_summary.html +*_summary.xml +*_usage.xml +*_xst.xrpt + +# iMPACT generated files +_impactbatch.log +impact.xsl +impact_impact.xwbt +ise_impact.cmd +webtalk_impact.xml + +# Core Generator generated files +xaw2verilog.log + +# project-wide generated files +*.gise +par_usage_statistics.html +usage_statistics_webtalk.html +webtalk.log +webtalk_pn.xml + +# generated folders +iseconfig/ +xlnx_auto_0_xdb/ +xst/ +_ngo/ +_xmsgs/ diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore new file mode 100644 index 0000000000..daf913b1b3 --- /dev/null +++ b/vendor/gitignore/Go.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/gitignore/Gradle.gitignore b/vendor/gitignore/Gradle.gitignore new file mode 100644 index 0000000000..77617a15c3 --- /dev/null +++ b/vendor/gitignore/Gradle.gitignore @@ -0,0 +1,14 @@ +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties diff --git a/vendor/gitignore/Grails.gitignore b/vendor/gitignore/Grails.gitignore new file mode 100644 index 0000000000..9185f14c37 --- /dev/null +++ b/vendor/gitignore/Grails.gitignore @@ -0,0 +1,33 @@ +# .gitignore for Grails 1.2 and 1.3 +# Although this should work for most versions of grails, it is +# suggested that you use the "grails integrate-with --git" command +# to generate your .gitignore file. + +# web application files +/web-app/WEB-INF/classes + +# default HSQL database files for production mode +/prodDb.* + +# general HSQL database files +*Db.properties +*Db.script + +# logs +/stacktrace.log +/test/reports +/logs + +# project release file +/*.war + +# plugin release files +/*.zip +/plugin.xml + +# older plugin install locations +/plugins +/web-app/plugins + +# "temporary" build files +/target diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore new file mode 100644 index 0000000000..096abdd90b --- /dev/null +++ b/vendor/gitignore/Haskell.gitignore @@ -0,0 +1,18 @@ +dist +dist-* +cabal-dev +*.o +*.hi +*.chi +*.chs.h +*.dyn_o +*.dyn_hi +.hpc +.hsenv +.cabal-sandbox/ +cabal.sandbox.config +*.prof +*.aux +*.hp +*.eventlog +.stack-work/ diff --git a/vendor/gitignore/IGORPro.gitignore b/vendor/gitignore/IGORPro.gitignore new file mode 100644 index 0000000000..c62be65003 --- /dev/null +++ b/vendor/gitignore/IGORPro.gitignore @@ -0,0 +1,5 @@ +# Avoid including Experiment files: they can be created and edited locally to test the ipf files +*.pxp +*.pxt +*.uxp +*.uxt diff --git a/vendor/gitignore/Idris.gitignore b/vendor/gitignore/Idris.gitignore new file mode 100644 index 0000000000..c28bc7cc67 --- /dev/null +++ b/vendor/gitignore/Idris.gitignore @@ -0,0 +1,2 @@ +*.ibc +*.o diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore new file mode 100644 index 0000000000..32858aad3c --- /dev/null +++ b/vendor/gitignore/Java.gitignore @@ -0,0 +1,12 @@ +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/vendor/gitignore/Jboss.gitignore b/vendor/gitignore/Jboss.gitignore new file mode 100644 index 0000000000..75d1731ed9 --- /dev/null +++ b/vendor/gitignore/Jboss.gitignore @@ -0,0 +1,19 @@ +jboss/server/all/deploy/project.ext +jboss/server/default/deploy/project.ext +jboss/server/minimal/deploy/project.ext +jboss/server/all/log/*.log +jboss/server/all/tmp/**/* +jboss/server/all/data/**/* +jboss/server/all/work/**/* +jboss/server/default/log/*.log +jboss/server/default/tmp/**/* +jboss/server/default/data/**/* +jboss/server/default/work/**/* +jboss/server/minimal/log/*.log +jboss/server/minimal/tmp/**/* +jboss/server/minimal/data/**/* +jboss/server/minimal/work/**/* + +# deployed package files # + +*.DEPLOYED diff --git a/vendor/gitignore/Jekyll.gitignore b/vendor/gitignore/Jekyll.gitignore new file mode 100644 index 0000000000..5c91b60c06 --- /dev/null +++ b/vendor/gitignore/Jekyll.gitignore @@ -0,0 +1,3 @@ +_site/ +.sass-cache/ +.jekyll-metadata diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore new file mode 100644 index 0000000000..0d7a0de298 --- /dev/null +++ b/vendor/gitignore/Joomla.gitignore @@ -0,0 +1,546 @@ +/.gitignore +/.htaccess +/administrator/cache/* +/administrator/components/com_admin/* +/administrator/components/com_ajax/* +/administrator/components/com_tags/* +/administrator/components/com_banners/* +/administrator/components/com_cache/* +/administrator/components/com_postinstall/* +/administrator/components/com_joomlaupdate/* +/administrator/components/com_contenthistory/* +/administrator/components/com_categories/* +/administrator/components/com_checkin/* +/administrator/components/com_config/* +/administrator/components/com_contact/* +/administrator/components/com_content/* +/administrator/components/com_cpanel/* +/administrator/components/com_finder/* +/administrator/components/com_installer/* +/administrator/components/com_languages/* +/administrator/components/com_login/* +/administrator/components/com_media/* +/administrator/components/com_menus/* +/administrator/components/com_messages/* +/administrator/components/com_modules/* +/administrator/components/com_newsfeeds/* +/administrator/components/com_plugins/* +/administrator/components/com_redirect/* +/administrator/components/com_search/* +/administrator/components/com_templates/* +/administrator/components/com_users/* +/administrator/components/com_weblinks/* +/administrator/components/index.html +/administrator/help/* +/administrator/includes/* +/administrator/language/en-GB/en-GB.com_ajax.ini +/administrator/language/en-GB/en-GB.com_ajax.sys.ini +/administrator/language/en-GB/en-GB.com_contenthistory.ini +/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini +/administrator/language/en-GB/en-GB.com_joomlaupdate.ini +/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini +/administrator/language/en-GB/en-GB.com_postinstall.ini +/administrator/language/en-GB/en-GB.com_postinstall.sys.ini +/administrator/language/en-GB/en-GB.com_sitemapjen.sys.ini +/administrator/language/en-GB/en-GB.com_tags.ini +/administrator/language/en-GB/en-GB.com_tags.sys.ini +/administrator/language/en-GB/en-GB.mod_stats_admin.ini +/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini +/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini +/administrator/language/en-GB/en-GB.plg_content_contact.ini +/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini +/administrator/language/en-GB/en-GB.plg_content_finder.ini +/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_categories.ini +/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_contacts.ini +/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_content.ini +/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini +/administrator/language/en-GB/en-GB.plg_finder_tags.ini +/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini +/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini +/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini +/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini +/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini +/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini +/administrator/language/en-GB/en-GB.plg_search_tags.ini +/administrator/language/en-GB/en-GB.plg_search_tags.sys.ini +/administrator/language/en-GB/en-GB.plg_system_languagecode.ini +/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini +/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini +/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini +/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini +/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini +/administrator/language/en-GB/en-GB.tpl_isis.ini +/administrator/language/en-GB/en-GB.tpl_isis.sys.ini +/administrator/language/en-GB/install.xml +/administrator/language/en-GB/en-GB.com_admin.ini +/administrator/language/en-GB/en-GB.com_admin.sys.ini +/administrator/language/en-GB/en-GB.com_banners.ini +/administrator/language/en-GB/en-GB.com_banners.sys.ini +/administrator/language/en-GB/en-GB.com_cache.ini +/administrator/language/en-GB/en-GB.com_cache.sys.ini +/administrator/language/en-GB/en-GB.com_categories.ini +/administrator/language/en-GB/en-GB.com_categories.sys.ini +/administrator/language/en-GB/en-GB.com_checkin.ini +/administrator/language/en-GB/en-GB.com_checkin.sys.ini +/administrator/language/en-GB/en-GB.com_config.ini +/administrator/language/en-GB/en-GB.com_config.sys.ini +/administrator/language/en-GB/en-GB.com_contact.ini +/administrator/language/en-GB/en-GB.com_contact.sys.ini +/administrator/language/en-GB/en-GB.com_content.ini +/administrator/language/en-GB/en-GB.com_content.sys.ini +/administrator/language/en-GB/en-GB.com_cpanel.ini +/administrator/language/en-GB/en-GB.com_cpanel.sys.ini +/administrator/language/en-GB/en-GB.com_finder.ini +/administrator/language/en-GB/en-GB.com_finder.sys.ini +/administrator/language/en-GB/en-GB.com_installer.ini +/administrator/language/en-GB/en-GB.com_installer.sys.ini +/administrator/language/en-GB/en-GB.com_languages.ini +/administrator/language/en-GB/en-GB.com_languages.sys.ini +/administrator/language/en-GB/en-GB.com_login.ini +/administrator/language/en-GB/en-GB.com_login.sys.ini +/administrator/language/en-GB/en-GB.com_mailto.sys.ini +/administrator/language/en-GB/en-GB.com_media.ini +/administrator/language/en-GB/en-GB.com_media.sys.ini +/administrator/language/en-GB/en-GB.com_menus.ini +/administrator/language/en-GB/en-GB.com_menus.sys.ini +/administrator/language/en-GB/en-GB.com_messages.ini +/administrator/language/en-GB/en-GB.com_messages.sys.ini +/administrator/language/en-GB/en-GB.com_modules.ini +/administrator/language/en-GB/en-GB.com_modules.sys.ini +/administrator/language/en-GB/en-GB.com_newsfeeds.ini +/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini +/administrator/language/en-GB/en-GB.com_plugins.ini +/administrator/language/en-GB/en-GB.com_plugins.sys.ini +/administrator/language/en-GB/en-GB.com_redirect.ini +/administrator/language/en-GB/en-GB.com_redirect.sys.ini +/administrator/language/en-GB/en-GB.com_search.ini +/administrator/language/en-GB/en-GB.com_search.sys.ini +/administrator/language/en-GB/en-GB.com_templates.ini +/administrator/language/en-GB/en-GB.com_templates.sys.ini +/administrator/language/en-GB/en-GB.com_users.ini +/administrator/language/en-GB/en-GB.com_users.sys.ini +/administrator/language/en-GB/en-GB.com_weblinks.ini +/administrator/language/en-GB/en-GB.com_weblinks.sys.ini +/administrator/language/en-GB/en-GB.com_wrapper.ini +/administrator/language/en-GB/en-GB.com_wrapper.sys.ini +/administrator/language/en-GB/en-GB.ini +/administrator/language/en-GB/en-GB.lib_joomla.ini +/administrator/language/en-GB/en-GB.localise.php +/administrator/language/en-GB/en-GB.mod_custom.ini +/administrator/language/en-GB/en-GB.mod_custom.sys.ini +/administrator/language/en-GB/en-GB.mod_feed.ini +/administrator/language/en-GB/en-GB.mod_feed.sys.ini +/administrator/language/en-GB/en-GB.mod_latest.ini +/administrator/language/en-GB/en-GB.mod_latest.sys.ini +/administrator/language/en-GB/en-GB.mod_logged.ini +/administrator/language/en-GB/en-GB.mod_logged.sys.ini +/administrator/language/en-GB/en-GB.mod_login.ini +/administrator/language/en-GB/en-GB.mod_login.sys.ini +/administrator/language/en-GB/en-GB.mod_menu.ini +/administrator/language/en-GB/en-GB.mod_menu.sys.ini +/administrator/language/en-GB/en-GB.mod_multilangstatus.ini +/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini +/administrator/language/en-GB/en-GB.mod_online.ini +/administrator/language/en-GB/en-GB.mod_online.sys.ini +/administrator/language/en-GB/en-GB.mod_popular.ini +/administrator/language/en-GB/en-GB.mod_popular.sys.ini +/administrator/language/en-GB/en-GB.mod_quickicon.ini +/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini +/administrator/language/en-GB/en-GB.mod_status.ini +/administrator/language/en-GB/en-GB.mod_status.sys.ini +/administrator/language/en-GB/en-GB.mod_submenu.ini +/administrator/language/en-GB/en-GB.mod_submenu.sys.ini +/administrator/language/en-GB/en-GB.mod_title.ini +/administrator/language/en-GB/en-GB.mod_title.sys.ini +/administrator/language/en-GB/en-GB.mod_toolbar.ini +/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini +/administrator/language/en-GB/en-GB.mod_unread.ini +/administrator/language/en-GB/en-GB.mod_unread.sys.ini +/administrator/language/en-GB/en-GB.mod_version.ini +/administrator/language/en-GB/en-GB.mod_version.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_example.ini +/administrator/language/en-GB/en-GB.plg_authentication_example.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini +/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini +/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini +/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini +/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini +/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini +/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini +/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini +/administrator/language/en-GB/en-GB.plg_content_geshi.ini +/administrator/language/en-GB/en-GB.plg_content_geshi.sys.ini +/administrator/language/en-GB/en-GB.plg_content_joomla.ini +/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini +/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini +/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini +/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini +/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini +/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini +/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini +/administrator/language/en-GB/en-GB.plg_content_vote.ini +/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini +/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini +/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini +/administrator/language/en-GB/en-GB.plg_editors_none.ini +/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini +/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini +/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini +/administrator/language/en-GB/en-GB.plg_extension_joomla.ini +/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini +/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini +/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini +/administrator/language/en-GB/en-GB.plg_search_categories.ini +/administrator/language/en-GB/en-GB.plg_search_categories.sys.ini +/administrator/language/en-GB/en-GB.plg_search_contacts.ini +/administrator/language/en-GB/en-GB.plg_search_contacts.sys.ini +/administrator/language/en-GB/en-GB.plg_search_content.ini +/administrator/language/en-GB/en-GB.plg_search_content.sys.ini +/administrator/language/en-GB/en-GB.plg_search_newsfeeds.ini +/administrator/language/en-GB/en-GB.plg_search_newsfeeds.sys.ini +/administrator/language/en-GB/en-GB.plg_search_weblinks.ini +/administrator/language/en-GB/en-GB.plg_search_weblinks.sys.ini +/administrator/language/en-GB/en-GB.plg_system_cache.ini +/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini +/administrator/language/en-GB/en-GB.plg_system_debug.ini +/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini +/administrator/language/en-GB/en-GB.plg_system_highlight.ini +/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini +/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini +/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini +/administrator/language/en-GB/en-GB.plg_system_log.ini +/administrator/language/en-GB/en-GB.plg_system_logout.ini +/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini +/administrator/language/en-GB/en-GB.plg_system_log.sys.ini +/administrator/language/en-GB/en-GB.plg_system_p3p.ini +/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini +/administrator/language/en-GB/en-GB.plg_system_redirect.ini +/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini +/administrator/language/en-GB/en-GB.plg_system_remember.ini +/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini +/administrator/language/en-GB/en-GB.plg_system_sef.ini +/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini +/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini +/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini +/administrator/language/en-GB/en-GB.plg_user_joomla.ini +/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini +/administrator/language/en-GB/en-GB.plg_user_profile.ini +/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini +/administrator/language/en-GB/en-GB.tpl_bluestork.ini +/administrator/language/en-GB/en-GB.tpl_bluestork.sys.ini +/administrator/language/en-GB/en-GB.tpl_hathor.ini +/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini +/administrator/language/en-GB/en-GB.xml +/administrator/language/en-GB/index.html +/administrator/language/overrides/* +/administrator/language/index.html +/administrator/manifests/* +/administrator/modules/mod_custom/* +/administrator/modules/mod_feed/* +/administrator/modules/mod_latest/* +/administrator/modules/mod_logged/* +/administrator/modules/mod_login/* +/administrator/modules/mod_menu/* +/administrator/modules/mod_multilangstatus/* +/administrator/modules/mod_online/* +/administrator/modules/mod_popular/* +/administrator/modules/mod_quickicon/* +/administrator/modules/mod_status/* +/administrator/modules/mod_submenu/* +/administrator/modules/mod_title/* +/administrator/modules/mod_toolbar/* +/administrator/modules/mod_unread/* +/administrator/modules/mod_version/* +/administrator/modules/mod_stats_admin/* +/administrator/modules/index.html +/administrator/templates/bluestork/* +/administrator/templates/isis/* +/administrator/templates/hathor/* +/administrator/templates/system/* +/administrator/templates/index.html +/administrator/index.php +/cache/* +/bin/* +/cli/* +/components/com_banners/* +/components/com_ajax/* +/components/com_config/* +/components/com_contenthistory/* +/components/com_tags/* +/components/com_contact/* +/components/com_content/* +/components/com_finder/* +/components/com_mailto/* +/components/com_media/* +/components/com_newsfeeds/* +/components/com_search/* +/components/com_users/* +/components/com_weblinks/* +/components/com_wrapper/* +/components/index.html +/images/banners/* +/images/headers/* +/images/sampledata/* +/images/joomla* +/images/index.html +/images/powered_by.png +/includes/* +/installation/* +/language/en-GB/en-GB.com_ajax.ini +/language/en-GB/en-GB.com_config.ini +/language/en-GB/en-GB.com_contact.ini +/language/en-GB/en-GB.com_finder.ini +/language/en-GB/en-GB.com_tags.ini +/language/en-GB/en-GB.finder_cli.ini +/language/en-GB/en-GB.lib_fof.sys.ini +/language/en-GB/en-GB.lib_fof.ini +/language/en-GB/en-GB.com_content.ini +/language/en-GB/en-GB.lib_idna_convert.sys.ini +/language/en-GB/en-GB.com_mailto.ini +/language/en-GB/en-GB.lib_joomla.sys.ini +/language/en-GB/en-GB.lib_phpass.sys.ini +/language/en-GB/en-GB.lib_phpmailer.sys.ini +/language/en-GB/en-GB.lib_phputf8.sys.ini +/language/en-GB/en-GB.lib_simplepie.sys.ini +/language/en-GB/en-GB.com_media.ini +/language/en-GB/en-GB.mod_finder.ini +/language/en-GB/en-GB.com_messages.ini +/language/en-GB/en-GB.mod_tags_popular.ini +/language/en-GB/en-GB.mod_tags_popular.sys.ini +/language/en-GB/en-GB.mod_tags_similar.ini +/language/en-GB/en-GB.mod_tags_similar.sys.ini +/language/en-GB/en-GB.mod_finder.sys.ini +/language/en-GB/en-GB.tpl_beez3.ini +/language/en-GB/en-GB.tpl_beez3.sys.ini +/language/en-GB/en-GB.com_newsfeeds.ini +/language/en-GB/en-GB.tpl_protostar.ini +/language/en-GB/en-GB.tpl_protostar.sys.ini +/language/en-GB/en-GB.com_search.ini +/language/en-GB/en-GB.com_users.ini +/language/en-GB/en-GB.com_weblinks.ini +/language/en-GB/en-GB.com_wrapper.ini +/language/en-GB/en-GB.files_joomla.sys.ini +/language/en-GB/en-GB.ini +/language/en-GB/en-GB.lib_joomla.ini +/language/en-GB/en-GB.localise.php +/language/en-GB/en-GB.mod_articles_archive.ini +/language/en-GB/en-GB.mod_articles_archive.sys.ini +/language/en-GB/en-GB.mod_articles_categories.ini +/language/en-GB/en-GB.mod_articles_categories.sys.ini +/language/en-GB/en-GB.mod_articles_category.ini +/language/en-GB/en-GB.mod_articles_category.sys.ini +/language/en-GB/en-GB.mod_articles_latest.ini +/language/en-GB/en-GB.mod_articles_latest.sys.ini +/language/en-GB/en-GB.mod_articles_news.ini +/language/en-GB/en-GB.mod_articles_news.sys.ini +/language/en-GB/en-GB.mod_articles_popular.ini +/language/en-GB/en-GB.mod_articles_popular.sys.ini +/language/en-GB/en-GB.mod_banners.ini +/language/en-GB/en-GB.mod_banners.sys.ini +/language/en-GB/en-GB.mod_breadcrumbs.ini +/language/en-GB/en-GB.mod_breadcrumbs.sys.ini +/language/en-GB/en-GB.mod_custom.ini +/language/en-GB/en-GB.mod_custom.sys.ini +/language/en-GB/en-GB.mod_feed.ini +/language/en-GB/en-GB.mod_feed.sys.ini +/language/en-GB/en-GB.mod_footer.ini +/language/en-GB/en-GB.mod_footer.sys.ini +/language/en-GB/en-GB.mod_languages.ini +/language/en-GB/en-GB.mod_languages.sys.ini +/language/en-GB/en-GB.mod_login.ini +/language/en-GB/en-GB.mod_login.sys.ini +/language/en-GB/en-GB.mod_menu.ini +/language/en-GB/en-GB.mod_menu.sys.ini +/language/en-GB/en-GB.mod_random_image.ini +/language/en-GB/en-GB.mod_random_image.sys.ini +/language/en-GB/en-GB.mod_related_items.ini +/language/en-GB/en-GB.mod_related_items.sys.ini +/language/en-GB/en-GB.mod_search.ini +/language/en-GB/en-GB.mod_search.sys.ini +/language/en-GB/en-GB.mod_stats.ini +/language/en-GB/en-GB.mod_stats.sys.ini +/language/en-GB/en-GB.mod_syndicate.ini +/language/en-GB/en-GB.mod_syndicate.sys.ini +/language/en-GB/en-GB.mod_users_latest.ini +/language/en-GB/en-GB.mod_users_latest.sys.ini +/language/en-GB/en-GB.mod_weblinks.ini +/language/en-GB/en-GB.mod_weblinks.sys.ini +/language/en-GB/en-GB.mod_whosonline.ini +/language/en-GB/en-GB.mod_whosonline.sys.ini +/language/en-GB/en-GB.mod_wrapper.ini +/language/en-GB/en-GB.mod_wrapper.sys.ini +/language/en-GB/en-GB.tpl_atomic.ini +/language/en-GB/en-GB.tpl_atomic.sys.ini +/language/en-GB/en-GB.tpl_beez_20.ini +/language/en-GB/en-GB.tpl_beez_20.sys.ini +/language/en-GB/en-GB.tpl_beez5.ini +/language/en-GB/en-GB.tpl_beez5.sys.ini +/language/en-GB/en-GB.xml +/language/en-GB/index.html +/language/en-GB/install.xml +/language/overrides/* +/language/index.html +/layouts/joomla/* +/layouts/libraries/* +/layouts/plugins/* +/layouts/index.html +/libraries/cms.php +/libraries/cms/* +/libraries/fof/* +/libraries/idna_convert/* +/libraries/joomla/* +/libraries/legacy/* +/libraries/phpass/* +/libraries/phpmailer/* +/libraries/phputf8/* +/libraries/simplepie/* +/libraries/vendor/* +/libraries/classmap.php +/libraries/import.legacy.php +/libraries/index.html +/libraries/import.php +/libraries/loader.php +/libraries/platform.php +/logs/* +/media/cms/* +/media/com_contenthistory/* +/media/com_finder/* +/media/com_joomlaupdate/* +/media/com_wrapper/* +/media/contacts/* +/media/editors/* +/media/jui/* +/media/mailto/* +/media/media/* +/media/mod_languages/* +/media/overrider/* +/media/plg_quickicon_extensionupdate/* +/media/plg_quickicon_joomlaupdate/* +/media/plg_system_highlight/* +/media/system/* +/media/index.html +/modules/mod_articles_archive/* +/modules/mod_articles_categories/* +/modules/mod_articles_category/* +/modules/mod_articles_latest/* +/modules/mod_articles_news/* +/modules/mod_articles_popular/* +/modules/mod_banners/* +/modules/mod_breadcrumbs/* +/modules/mod_custom/* +/modules/mod_feed/* +/modules/mod_finder/* +/modules/mod_footer/* +/modules/mod_languages/* +/modules/mod_login/* +/modules/mod_menu/* +/modules/mod_random_image/* +/modules/mod_related_items/* +/modules/mod_search/* +/modules/mod_stats/* +/modules/mod_syndicate/* +/modules/mod_tags_popular/* +/modules/mod_tags_similar/* +/modules/mod_users_latest/* +/modules/mod_weblinks/* +/modules/mod_whosonline/* +/modules/mod_wrapper/* +/modules/index.html +/plugins/authentication/example/* +/plugins/authentication/gmail/* +/plugins/authentication/joomla/* +/plugins/authentication/ldap/* +/plugins/authentication/cookie/* +/plugins/authentication/index.html +/plugins/captcha/recaptcha/* +/plugins/captcha/index.html +/plugins/content/emailcloak/* +/plugins/content/example/* +/plugins/content/finder/* +/plugins/content/geshi/* +/plugins/content/joomla/* +/plugins/content/loadmodule/* +/plugins/content/pagebreak/* +/plugins/content/pagenavigation/* +/plugins/content/vote/* +/plugins/content/contact/* +/plugins/content/index.html +/plugins/editors/codemirror/* +/plugins/editors/none/* +/plugins/editors/tinymce/* +/plugins/editors/index.html +/plugins/editors-xtd/article/* +/plugins/editors-xtd/image/* +/plugins/editors-xtd/pagebreak/* +/plugins/editors-xtd/readmore/* +/plugins/editors-xtd/index.html +/plugins/extension/example/* +/plugins/extension/joomla/* +/plugins/extension/index.html +/plugins/finder/index.html +/plugins/finder/categories/* +/plugins/finder/contacts/* +/plugins/finder/content/* +/plugins/finder/newsfeeds/* +/plugins/finder/tags/* +/plugins/finder/weblinks/* +/plugins/installer/* +/plugins/quickicon/extensionupdate/* +/plugins/quickicon/joomlaupdate/* +/plugins/quickicon/index.html +/plugins/search/categories/* +/plugins/search/contacts/* +/plugins/search/content/* +/plugins/search/newsfeeds/* +/plugins/search/weblinks/* +/plugins/search/tags/* +/plugins/search/index.html +/plugins/system/cache/* +/plugins/system/debug/* +/plugins/system/highlight/* +/plugins/system/languagecode/* +/plugins/system/languagefilter/* +/plugins/system/log/* +/plugins/system/logout/* +/plugins/system/p3p/* +/plugins/system/redirect/* +/plugins/system/remember/* +/plugins/system/sef/* +/plugins/system/index.html +/plugins/twofactorauth/* +/plugins/user/contactcreator/* +/plugins/user/example/* +/plugins/user/joomla/* +/plugins/user/profile/* +/plugins/user/index.html +/plugins/index.html +/templates/atomic/* +/templates/beez3/* +/templates/beez_20/* +/templates/beez5/* +/templates/protostar/* +/templates/system/* +/templates/index.html +/tmp/* +/configuration.php +/index.php +/joomla.xml +/*.txt +/robots.txt.dist diff --git a/vendor/gitignore/KiCad.gitignore b/vendor/gitignore/KiCad.gitignore new file mode 100644 index 0000000000..606ed1c7b4 --- /dev/null +++ b/vendor/gitignore/KiCad.gitignore @@ -0,0 +1,20 @@ +# For PCBs designed using KiCad: http://www.kicad-pcb.org/ + +# Temporary files +*.000 +*.bak +*.bck +*.kicad_pcb-bak +*~ +_autosave-* +*.tmp + +# Netlist files (exported from Eeschema) +*.net + +# Autorouter files (exported from Pcbnew) +.dsn + +# Exported BOM files +*.xml +*.csv diff --git a/vendor/gitignore/Kohana.gitignore b/vendor/gitignore/Kohana.gitignore new file mode 100644 index 0000000000..8b2ab01a80 --- /dev/null +++ b/vendor/gitignore/Kohana.gitignore @@ -0,0 +1,2 @@ +application/cache/* +application/logs/* diff --git a/vendor/gitignore/LabVIEW.gitignore b/vendor/gitignore/LabVIEW.gitignore new file mode 100644 index 0000000000..122450865c --- /dev/null +++ b/vendor/gitignore/LabVIEW.gitignore @@ -0,0 +1,16 @@ +# Libraries +*.lvlibp +*.llb + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe + +# Metadata +*.aliases +*.lvlps diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore new file mode 100644 index 0000000000..c491fa2bc6 --- /dev/null +++ b/vendor/gitignore/Laravel.gitignore @@ -0,0 +1,16 @@ +vendor/ +node_modules/ + +# Laravel 4 specific +bootstrap/compiled.php +app/storage/ + +# Laravel 5 & Lumen specific +bootstrap/cache/ +storage/ +.env.*.php +.env.php +.env + +# Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer +.rocketeer/ diff --git a/vendor/gitignore/Leiningen.gitignore b/vendor/gitignore/Leiningen.gitignore new file mode 100644 index 0000000000..47fed6c20d --- /dev/null +++ b/vendor/gitignore/Leiningen.gitignore @@ -0,0 +1,12 @@ +pom.xml +pom.xml.asc +*jar +/lib/ +/classes/ +/target/ +/checkouts/ +.lein-deps-sum +.lein-repl-history +.lein-plugins/ +.lein-failures +.nrepl-port diff --git a/vendor/gitignore/LemonStand.gitignore b/vendor/gitignore/LemonStand.gitignore new file mode 100644 index 0000000000..c7d94ad34b --- /dev/null +++ b/vendor/gitignore/LemonStand.gitignore @@ -0,0 +1,21 @@ +boot.php +index.php +install.php +/config/* +!/config/config.php +/controllers/* +/init/* +/logs/* +/phproad/* +/temp/* +/uploaded/* +/installer_files/* +/modules/backend/* +/modules/blog/* +/modules/cms/* +/modules/core/* +/modules/session/* +/modules/shop/* +/modules/system/* +/modules/users/* +# add content_*.php if you don't want erase client changes to content diff --git a/vendor/gitignore/Lilypond.gitignore b/vendor/gitignore/Lilypond.gitignore new file mode 100644 index 0000000000..513e6edd9c --- /dev/null +++ b/vendor/gitignore/Lilypond.gitignore @@ -0,0 +1,6 @@ +*.pdf +*.ps +*.midi +*.mid +*.log +*~ diff --git a/vendor/gitignore/Lithium.gitignore b/vendor/gitignore/Lithium.gitignore new file mode 100644 index 0000000000..7b22568ea8 --- /dev/null +++ b/vendor/gitignore/Lithium.gitignore @@ -0,0 +1,2 @@ +libraries/* +resources/tmp/* diff --git a/vendor/gitignore/Lua.gitignore b/vendor/gitignore/Lua.gitignore new file mode 100644 index 0000000000..6fd0a376de --- /dev/null +++ b/vendor/gitignore/Lua.gitignore @@ -0,0 +1,41 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore new file mode 100644 index 0000000000..195c9b7a02 --- /dev/null +++ b/vendor/gitignore/Magento.gitignore @@ -0,0 +1,104 @@ +.htaccess.sample +.modgit/ +.modman/ +app/code/community/Phoenix/Moneybookers/ +app/code/community/Cm/RedisSession/ +app/code/core/ +app/design/adminhtml/default/default/ +app/design/frontend/base/ +app/design/frontend/rwd/ +app/design/frontend/default/blank/ +app/design/frontend/default/default/ +app/design/frontend/default/iphone/ +app/design/frontend/default/modern/ +app/design/frontend/enterprise/default +app/design/install/ +app/etc/modules/Enterprise_* +app/etc/modules/Mage_*.xml +app/etc/modules/Phoenix_Moneybookers.xml +app/etc/modules/Cm_RedisSession.xml +app/etc/applied.patches.list +app/etc/config.xml +app/etc/enterprise.xml +app/etc/local.xml.additional +app/etc/local.xml.template +app/etc/local.xml +app/.htaccess +app/bootstrap.php +app/locale/en_US/ +app/Mage.php +/cron.php +cron.sh +dev/.htaccess +dev/tests/functional/ +downloader/ +errors/ +favicon.ico +/get.php +includes/ +/index.php +index.php.sample +/install.php +js/blank.html +js/calendar/ +js/enterprise/ +js/extjs/ +js/firebug/ +js/flash/ +js/index.php +js/jscolor/ +js/lib/ +js/mage/ +js/prototype/ +js/scriptaculous/ +js/spacer.gif +js/tiny_mce/ +js/varien/ +lib/3Dsecure/ +lib/Apache/ +lib/flex/ +lib/googlecheckout/ +lib/.htaccess +lib/LinLibertineFont/ +lib/Mage/ +lib/PEAR/ +lib/Pelago/ +lib/phpseclib/ +lib/Varien/ +lib/Zend/ +lib/Cm/ +lib/Credis/ +lib/Magento/ +LICENSE_AFL.txt +LICENSE.html +LICENSE.txt +LICENSE_EE* +/mage +media/ +/api.php +nbproject/ +pear +pear/ +php.ini.sample +pkginfo/ +RELEASE_NOTES.txt +shell/.htaccess +shell/abstract.php +shell/compiler.php +shell/indexer.php +shell/log.php +sitemap.xml +skin/adminhtml/default/default/ +skin/adminhtml/default/enterprise +skin/frontend/base/ +skin/frontend/rwd/ +skin/frontend/default/blank/ +skin/frontend/default/blue/ +skin/frontend/default/default/ +skin/frontend/default/french/ +skin/frontend/default/german/ +skin/frontend/default/iphone/ +skin/frontend/default/modern/ +skin/frontend/enterprise +skin/install/ +var/ diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore new file mode 100644 index 0000000000..1cdc9f7fd4 --- /dev/null +++ b/vendor/gitignore/Maven.gitignore @@ -0,0 +1,9 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties diff --git a/vendor/gitignore/Mercury.gitignore b/vendor/gitignore/Mercury.gitignore new file mode 100644 index 0000000000..70ec869397 --- /dev/null +++ b/vendor/gitignore/Mercury.gitignore @@ -0,0 +1,13 @@ +Mercury/ +Mercury.modules +*.mh +*.err +*.init +*.dll +*.exe +*.a +*.so +*.dylib +*.beams +*.d +*.c_date diff --git a/vendor/gitignore/MetaProgrammingSystem.gitignore b/vendor/gitignore/MetaProgrammingSystem.gitignore new file mode 100644 index 0000000000..3e75841041 --- /dev/null +++ b/vendor/gitignore/MetaProgrammingSystem.gitignore @@ -0,0 +1,16 @@ +workspace.xml +junitvmwatcher*.properties +build.properties + +# generated java classes and java source files +# manually add any custom artifacts that can't be generated from the models +# http://confluence.jetbrains.com/display/MPSD25/HowTo+--+MPS+and+Git +classes_gen +source_gen +source_gen.caches + +# generated test code and test results +test_gen +test_gen.caches +TEST-*.xml +junit*.properties diff --git a/vendor/gitignore/Nanoc.gitignore b/vendor/gitignore/Nanoc.gitignore new file mode 100644 index 0000000000..abc21828a3 --- /dev/null +++ b/vendor/gitignore/Nanoc.gitignore @@ -0,0 +1,10 @@ +# For projects using nanoc (http://nanoc.ws/) + +# Default location for output, needs to match output_dir's value found in config.yaml +output/ + +# Temporary file directory +tmp/ + +# Crash Log +crash.log diff --git a/vendor/gitignore/Nim.gitignore b/vendor/gitignore/Nim.gitignore new file mode 100644 index 0000000000..67d9b34c6c --- /dev/null +++ b/vendor/gitignore/Nim.gitignore @@ -0,0 +1 @@ +nimcache/ diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore new file mode 100644 index 0000000000..5148e527a7 --- /dev/null +++ b/vendor/gitignore/Node.gitignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history diff --git a/vendor/gitignore/OCaml.gitignore b/vendor/gitignore/OCaml.gitignore new file mode 100644 index 0000000000..f7817ae5c3 --- /dev/null +++ b/vendor/gitignore/OCaml.gitignore @@ -0,0 +1,20 @@ +*.annot +*.cmo +*.cma +*.cmi +*.a +*.o +*.cmx +*.cmxs +*.cmxa + +# ocamlbuild working directory +_build/ + +# ocamlbuild targets +*.byte +*.native + +# oasis generated files +setup.data +setup.log diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore new file mode 100644 index 0000000000..3020bc327a --- /dev/null +++ b/vendor/gitignore/Objective-C.gitignore @@ -0,0 +1,51 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xcuserstate + +## Obj-C/Swift specific +*.hmap +*.ipa + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +fastlane/report.xml +fastlane/screenshots diff --git a/vendor/gitignore/Opa.gitignore b/vendor/gitignore/Opa.gitignore new file mode 100644 index 0000000000..74c6219ced --- /dev/null +++ b/vendor/gitignore/Opa.gitignore @@ -0,0 +1,13 @@ +_build +_tracks + +opa-debug-js + +*.opp +*.opx +*.opx.broken +*.dump +*.api +*.api-txt +*.exe +*.log diff --git a/vendor/gitignore/OpenCart.gitignore b/vendor/gitignore/OpenCart.gitignore new file mode 100644 index 0000000000..28e45aa6aa --- /dev/null +++ b/vendor/gitignore/OpenCart.gitignore @@ -0,0 +1,13 @@ +.htaccess +/config.php +admin/config.php + +!index.html + +download/ +image/data/ +image/cache/ +system/cache/ +system/logs/ + +system/storage/ diff --git a/vendor/gitignore/OracleForms.gitignore b/vendor/gitignore/OracleForms.gitignore new file mode 100644 index 0000000000..699a494011 --- /dev/null +++ b/vendor/gitignore/OracleForms.gitignore @@ -0,0 +1,8 @@ +# Compiled Form Modules +*.fmx + +# Compiled Menu Modules +*.mmx + +# Compiled Pre-Linked Libraries +*.plx diff --git a/vendor/gitignore/Packer.gitignore b/vendor/gitignore/Packer.gitignore new file mode 100644 index 0000000000..1b7a03efdd --- /dev/null +++ b/vendor/gitignore/Packer.gitignore @@ -0,0 +1,5 @@ +# Cache objects +packer_cache/ + +# For built boxes +*.box diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore new file mode 100644 index 0000000000..ae2ad536ab --- /dev/null +++ b/vendor/gitignore/Perl.gitignore @@ -0,0 +1,20 @@ +/blib/ +/.build/ +_build/ +cover_db/ +inc/ +Build +!Build/ +Build.bat +.last_cover_stats +/Makefile +/Makefile.old +/MANIFEST.bak +/META.yml +/META.json +/MYMETA.* +nytprof.out +/pm_to_blib +*.o +*.bs +/_eumm/ diff --git a/vendor/gitignore/Phalcon.gitignore b/vendor/gitignore/Phalcon.gitignore new file mode 100644 index 0000000000..6ffe3aa220 --- /dev/null +++ b/vendor/gitignore/Phalcon.gitignore @@ -0,0 +1,2 @@ +/cache/ +/config/development/ diff --git a/vendor/gitignore/PlayFramework.gitignore b/vendor/gitignore/PlayFramework.gitignore new file mode 100644 index 0000000000..6d67f11917 --- /dev/null +++ b/vendor/gitignore/PlayFramework.gitignore @@ -0,0 +1,15 @@ +# Ignore Play! working directory # +bin/ +/db +.eclipse +/lib/ +/logs/ +/modules +/project/target +/target +tmp/ +test-result +server.pid +*.eml +/dist/ +.cache diff --git a/vendor/gitignore/Plone.gitignore b/vendor/gitignore/Plone.gitignore new file mode 100644 index 0000000000..770a8681ac --- /dev/null +++ b/vendor/gitignore/Plone.gitignore @@ -0,0 +1,18 @@ +*.pyc +*.pyo +*.tmp* +*.mo +*.egg +*.EGG +*.egg-info +*.EGG-INFO +.*.cfg +bin/ +build/ +develop-eggs/ +downloads/ +eggs/ +fake-eggs/ +parts/ +dist/ +var/ diff --git a/vendor/gitignore/Prestashop.gitignore b/vendor/gitignore/Prestashop.gitignore new file mode 100644 index 0000000000..7c6ae1e31c --- /dev/null +++ b/vendor/gitignore/Prestashop.gitignore @@ -0,0 +1,32 @@ +# Private files +# The following files contain your database credentials and other personal data. + +config/settings.*.php + +# Cache, temp and generated files +# The following files are generated by PrestaShop. + +admin-dev/autoupgrade/ +/cache/ +!/cache/index.php +!/cache/cachefs/index.php +!/cache/purifier/index.php +!/cache/push/index.php +!/cache/sandbox/index.php +!/cache/smarty/index.php +!/cache/tcpdf/index.php +config/xml/*.xml +/log/* +*sitemap.xml +themes/*/cache/ +modules/*/config*.xml + +# Site content +# The following folders contain product images, virtual products, CSV's, etc. + +admin-dev/backups/ +admin-dev/export/ +admin-dev/import/ +download/ +/img/* +upload/ diff --git a/vendor/gitignore/Processing.gitignore b/vendor/gitignore/Processing.gitignore new file mode 100644 index 0000000000..85f269a89f --- /dev/null +++ b/vendor/gitignore/Processing.gitignore @@ -0,0 +1,7 @@ +.DS_Store +applet +application.linux32 +application.linux64 +application.windows32 +application.windows64 +application.macosx diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore new file mode 100644 index 0000000000..72364f99fe --- /dev/null +++ b/vendor/gitignore/Python.gitignore @@ -0,0 +1,89 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject diff --git a/vendor/gitignore/Qooxdoo.gitignore b/vendor/gitignore/Qooxdoo.gitignore new file mode 100644 index 0000000000..d0c64102d8 --- /dev/null +++ b/vendor/gitignore/Qooxdoo.gitignore @@ -0,0 +1,5 @@ +cache +cache-downloads +inspector +api +source/inspector.html diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore new file mode 100644 index 0000000000..fa24b2efee --- /dev/null +++ b/vendor/gitignore/Qt.gitignore @@ -0,0 +1,38 @@ +# C++ objects and libs + +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.dll +*.dylib + +# Qt-es + +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +qrc_*.cpp +ui_*.h +Makefile* +*build-* + +# QtCreator + +*.autosave + +# QtCtreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCtreator CMake +CMakeLists.txt.user + diff --git a/vendor/gitignore/R.gitignore b/vendor/gitignore/R.gitignore new file mode 100644 index 0000000000..fcff087aeb --- /dev/null +++ b/vendor/gitignore/R.gitignore @@ -0,0 +1,33 @@ +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# Example code in package build process +*-Ex.R + +# Output files from R CMD build +/*.tar.gz + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +/*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md diff --git a/vendor/gitignore/ROS.gitignore b/vendor/gitignore/ROS.gitignore new file mode 100644 index 0000000000..f8bcd11737 --- /dev/null +++ b/vendor/gitignore/ROS.gitignore @@ -0,0 +1,47 @@ +build/ +bin/ +lib/ +msg_gen/ +srv_gen/ +msg/*Action.msg +msg/*ActionFeedback.msg +msg/*ActionGoal.msg +msg/*ActionResult.msg +msg/*Feedback.msg +msg/*Goal.msg +msg/*Result.msg +msg/_*.py + +# Generated by dynamic reconfigure +*.cfgc +/cfg/cpp/ +/cfg/*.py + +# Ignore generated docs +*.dox +*.wikidoc + +# eclipse stuff +.project +.cproject + +# qcreator stuff +CMakeLists.txt.user + +srv/_*.py +*.pcd +*.pyc +qtcreator-* +*.user + +/planning/cfg +/planning/docs +/planning/src + +*~ + +# Emacs +.#* + +# Catkin custom files +CATKIN_IGNORE diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore new file mode 100644 index 0000000000..2121e0a803 --- /dev/null +++ b/vendor/gitignore/Rails.gitignore @@ -0,0 +1,38 @@ +*.rbc +capybara-*.html +.rspec +/log +/tmp +/db/*.sqlite3 +/db/*.sqlite3-journal +/public/system +/coverage/ +/spec/tmp +**.orig +rerun.txt +pickle-email-*.html + +# TODO Comment out these rules if you are OK with secrets being uploaded to the repo +config/initializers/secret_token.rb +config/secrets.yml + +## Environment normalization: +/.bundle +/vendor/bundle + +# these should all be checked in to normalize the environment: +# Gemfile.lock, .ruby-version, .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc + +# if using bower-rails ignore default bower_components path bower.json files +/vendor/assets/bower_components +*.bowerrc +bower.json + +# Ignore pow environment settings +.powenv + +# Ignore Byebug command history file. +.byebug_history diff --git a/vendor/gitignore/RhodesRhomobile.gitignore b/vendor/gitignore/RhodesRhomobile.gitignore new file mode 100644 index 0000000000..a211dcc3b0 --- /dev/null +++ b/vendor/gitignore/RhodesRhomobile.gitignore @@ -0,0 +1,9 @@ +rholog-* +sim-* +bin/libs +bin/RhoBundle +bin/tmp +bin/target +bin/*.ap_ +*.o +*.jar diff --git a/vendor/gitignore/Ruby.gitignore b/vendor/gitignore/Ruby.gitignore new file mode 100644 index 0000000000..5e1422c9c3 --- /dev/null +++ b/vendor/gitignore/Ruby.gitignore @@ -0,0 +1,50 @@ +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +# Used by dotenv library to load environment variables. +# .env + +## Specific to RubyMotion: +.dat* +.repl_history +build/ +*.bridgesupport +build-iPhoneOS/ +build-iPhoneSimulator/ + +## Specific to RubyMotion (use of CocoaPods): +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# vendor/Pods/ + +## Documentation cache and generated files: +/.yardoc/ +/_yardoc/ +/doc/ +/rdoc/ + +## Environment normalization: +/.bundle/ +/vendor/bundle +/lib/bundler/man/ + +# for a library or gem, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# Gemfile.lock +# .ruby-version +# .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc diff --git a/vendor/gitignore/Rust.gitignore b/vendor/gitignore/Rust.gitignore new file mode 100644 index 0000000000..cb14a42064 --- /dev/null +++ b/vendor/gitignore/Rust.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/vendor/gitignore/SCons.gitignore b/vendor/gitignore/SCons.gitignore new file mode 100644 index 0000000000..39d9743a08 --- /dev/null +++ b/vendor/gitignore/SCons.gitignore @@ -0,0 +1,2 @@ +# for projects that use SCons for building: http://http://www.scons.org/ +.sconsign.dblite diff --git a/vendor/gitignore/Sass.gitignore b/vendor/gitignore/Sass.gitignore new file mode 100644 index 0000000000..486b32ce90 --- /dev/null +++ b/vendor/gitignore/Sass.gitignore @@ -0,0 +1,2 @@ +.sass-cache/ +*.css.map diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore new file mode 100644 index 0000000000..c58d83b318 --- /dev/null +++ b/vendor/gitignore/Scala.gitignore @@ -0,0 +1,17 @@ +*.class +*.log + +# sbt specific +.cache +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ + +# Scala-IDE specific +.scala_dependencies +.worksheet diff --git a/vendor/gitignore/Scheme.gitignore b/vendor/gitignore/Scheme.gitignore new file mode 100644 index 0000000000..cbb89d78da --- /dev/null +++ b/vendor/gitignore/Scheme.gitignore @@ -0,0 +1,7 @@ +*.ss~ +*.ss#* +.#*.ss + +*.scm~ +*.scm#* +.#*.scm diff --git a/vendor/gitignore/Scrivener.gitignore b/vendor/gitignore/Scrivener.gitignore new file mode 100644 index 0000000000..3b39c66ba1 --- /dev/null +++ b/vendor/gitignore/Scrivener.gitignore @@ -0,0 +1,7 @@ +/Files/binder.autosave +/Files/binder.backup +/Files/search.indexes +/Files/user.lock +/Files/Docs/docs.checksum +/QuickLook/ +/Settings/ui.plist diff --git a/vendor/gitignore/Sdcc.gitignore b/vendor/gitignore/Sdcc.gitignore new file mode 100644 index 0000000000..07ee7d59ab --- /dev/null +++ b/vendor/gitignore/Sdcc.gitignore @@ -0,0 +1,8 @@ +# SDCC stuff +*.lnk +*.lst +*.map +*.mem +*.rel +*.rst +*.sym diff --git a/vendor/gitignore/SeamGen.gitignore b/vendor/gitignore/SeamGen.gitignore new file mode 100644 index 0000000000..a418cf376c --- /dev/null +++ b/vendor/gitignore/SeamGen.gitignore @@ -0,0 +1,26 @@ +/bootstrap/data +/bootstrap/tmp +/classes/ +/dist/ +/exploded-archives/ +/test-build/ +/test-output/ +/test-report/ +/target/ +temp-testng-customsuite.xml + +# based on http://stackoverflow.com/a/8865858/422476 I am removing inline comments + +#/classes/ all class files +#/dist/ contains generated war files for deployment +#/exploded-archives/ war content generation during deploy (or explode) +#/test-build/ test compilation (ant target for Seam) +#/test-output/ test results +#/test-report/ test report generation for, e.g., Hudson +#/target/ maven output folder +#temp-testng-customsuite.xml generated when running test cases under Eclipse + +# Thanks to @VonC and @kraftan for their helpful answers on a related question +# on StackOverflow.com: +# http://stackoverflow.com/questions/4176687 +# /what-is-the-recommended-source-control-ignore-pattern-for-seam-projects diff --git a/vendor/gitignore/SketchUp.gitignore b/vendor/gitignore/SketchUp.gitignore new file mode 100644 index 0000000000..5160df3c6b --- /dev/null +++ b/vendor/gitignore/SketchUp.gitignore @@ -0,0 +1 @@ +*.skb diff --git a/vendor/gitignore/Smalltalk.gitignore b/vendor/gitignore/Smalltalk.gitignore new file mode 100644 index 0000000000..75272b2347 --- /dev/null +++ b/vendor/gitignore/Smalltalk.gitignore @@ -0,0 +1,18 @@ +# changes file +*.changes + +# system image +*.image + +# Pharo Smalltalk Debug log file +PharoDebug.log + +# Squeak Smalltalk Debug log file +SqueakDebug.log + +# Monticello package cache +/package-cache + +# Metacello-github cache +/github-cache +github-*.zip diff --git a/vendor/gitignore/Stella.gitignore b/vendor/gitignore/Stella.gitignore new file mode 100644 index 0000000000..402a543837 --- /dev/null +++ b/vendor/gitignore/Stella.gitignore @@ -0,0 +1,12 @@ +# Atari 2600 (Stella) support for multiple assemblers +# - DASM +# - CC65 + +# Assembled binaries and object directories +obj/ +a.out +*.bin +*.a26 + +# Add in special Atari 7800-based binaries for good measure +*.a78 diff --git a/vendor/gitignore/SugarCRM.gitignore b/vendor/gitignore/SugarCRM.gitignore new file mode 100644 index 0000000000..842c3ec518 --- /dev/null +++ b/vendor/gitignore/SugarCRM.gitignore @@ -0,0 +1,25 @@ +## SugarCRM +# Ignore custom .htaccess stuff. +/.htaccess +# Ignore the cache directory completely. +# This will break the current behaviour. Which was often leading to +# the misuse of the repository as backup replacement. +# For development the cache directory can be safely ignored and +# therefore it is ignored. +/cache/ +# Ignore some files and directories from the custom directory. +/custom/history/ +/custom/modulebuilder/ +/custom/working/ +/custom/modules/*/Ext/ +/custom/application/Ext/ +# Custom configuration should also be ignored. +/config.php +/config_override.php +# The silent upgrade scripts aren't needed. +/silentUpgrade*.php +# Logs files can safely be ignored. +*.log +# Ignore the new upload directories. +/upload/ +/upload_backup/ diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore new file mode 100644 index 0000000000..8a29fa52af --- /dev/null +++ b/vendor/gitignore/Swift.gitignore @@ -0,0 +1,63 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xcuserstate + +## Obj-C/Swift specific +*.hmap +*.ipa + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore new file mode 100644 index 0000000000..7d56f982f8 --- /dev/null +++ b/vendor/gitignore/Symfony.gitignore @@ -0,0 +1,48 @@ +# Cache and logs (Symfony2) +/app/cache/* +/app/logs/* +!app/cache/.gitkeep +!app/logs/.gitkeep + +# Email spool folder +/app/spool/* + +# Cache, session files and logs (Symfony3) +/var/cache/* +/var/logs/* +/var/sessions/* +!var/cache/.gitkeep +!var/logs/.gitkeep +!var/sessions/.gitkeep + +# Parameters +/app/config/parameters.yml +/app/config/parameters.ini + +# Managed by Composer +/app/bootstrap.php.cache +/var/bootstrap.php.cache +/bin/* +!bin/console +!bin/symfony_requirements +/vendor/ + +# Assets and user uploads +/web/bundles/ +/web/uploads/ + +# Assets managed by Bower +/web/assets/vendor/ + +# PHPUnit +/app/phpunit.xml +/phpunit.xml + +# Build data +/build/ + +# Composer PHAR +/composer.phar + +# Backup entities generated with doctrine:generate:entities command +*/Entity/*~ diff --git a/vendor/gitignore/SymphonyCMS.gitignore b/vendor/gitignore/SymphonyCMS.gitignore new file mode 100644 index 0000000000..671c7ff9e3 --- /dev/null +++ b/vendor/gitignore/SymphonyCMS.gitignore @@ -0,0 +1,6 @@ +manifest/cache/ +manifest/logs/ +manifest/tmp/ +symphony/ +workspace/uploads/ +install-log.txt diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore new file mode 100644 index 0000000000..4123a577c4 --- /dev/null +++ b/vendor/gitignore/TeX.gitignore @@ -0,0 +1,180 @@ +## Core latex/pdflatex auxiliary files: +*.aux +*.lof +*.log +*.lot +*.fls +*.out +*.toc +*.fmt +*.fot +*.cb +*.cb2 + +## Intermediate documents: +*.dvi +*-converted-to.* +# these rules might exclude image files for figures etc. +# *.ps +# *.eps +# *.pdf + +## Bibliography auxiliary files (bibtex/biblatex/biber): +*.bbl +*.bcf +*.blg +*-blx.aux +*-blx.bib +*.brf +*.run.xml + +## Build tool auxiliary files: +*.fdb_latexmk +*.synctex +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync + +## Auxiliary and intermediate files from other packages: +# algorithms +*.alg +*.loa + +# achemso +acs-*.bib + +# amsthm +*.thm + +# beamer +*.nav +*.snm +*.vrb + +# cprotect +*.cpt + +# fixme +*.lox + +#(r)(e)ledmac/(r)(e)ledpar +*.end +*.?end +*.[1-9] +*.[1-9][0-9] +*.[1-9][0-9][0-9] +*.[1-9]R +*.[1-9][0-9]R +*.[1-9][0-9][0-9]R +*.eledsec[1-9] +*.eledsec[1-9]R +*.eledsec[1-9][0-9] +*.eledsec[1-9][0-9]R +*.eledsec[1-9][0-9][0-9] +*.eledsec[1-9][0-9][0-9]R + +# glossaries +*.acn +*.acr +*.glg +*.glo +*.gls +*.glsdefs + +# gnuplottex +*-gnuplottex-* + +# hyperref +*.brf + +# knitr +*-concordance.tex +# TODO Comment the next line if you want to keep your tikz graphics files +*.tikz +*-tikzDictionary + +# listings +*.lol + +# makeidx +*.idx +*.ilg +*.ind +*.ist + +# minitoc +*.maf +*.mlf +*.mlt +*.mtc +*.mtc[0-9] +*.mtc[1-9][0-9] + +# minted +_minted* +*.pyg + +# morewrites +*.mw + +# mylatexformat +*.fmt + +# nomencl +*.nlo + +# sagetex +*.sagetex.sage +*.sagetex.py +*.sagetex.scmd + +# sympy +*.sout +*.sympy +sympy-plots-for-*.tex/ + +# pdfcomment +*.upa +*.upb + +# pythontex +*.pytxcode +pythontex-files-*/ + +# thmtools +*.loe + +# TikZ & PGF +*.dpth +*.md5 +*.auxlock + +# todonotes +*.tdo + +# xindy +*.xdy + +# xypic precompiled matrices +*.xyc + +# endfloat +*.ttt +*.fff + +# Latexian +TSWLatexianTemp* + +## Editors: +# WinEdt +*.bak +*.sav + +# Texpad +.texpadtmp + +# Kile +*.backup + +# KBibTeX +*~[0-9]* diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore new file mode 100644 index 0000000000..7868d16d21 --- /dev/null +++ b/vendor/gitignore/Terraform.gitignore @@ -0,0 +1,3 @@ +# Compiled files +*.tfstate +*.tfstate.backup diff --git a/vendor/gitignore/Textpattern.gitignore b/vendor/gitignore/Textpattern.gitignore new file mode 100644 index 0000000000..3805636d62 --- /dev/null +++ b/vendor/gitignore/Textpattern.gitignore @@ -0,0 +1,11 @@ +.htaccess +css.php +rpc/ +sites/site*/admin/ +sites/site*/private/ +sites/site*/public/admin/ +sites/site*/public/setup/ +sites/site*/public/theme/ +textpattern/ +HISTORY.txt +README.txt diff --git a/vendor/gitignore/TurboGears2.gitignore b/vendor/gitignore/TurboGears2.gitignore new file mode 100644 index 0000000000..122b3de221 --- /dev/null +++ b/vendor/gitignore/TurboGears2.gitignore @@ -0,0 +1,20 @@ +*.py[co] + +# Default development database +devdata.db + +# Default data directory +data/* + +# Packages +*.egg +*.egg-info +dist +build + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox diff --git a/vendor/gitignore/Typo3.gitignore b/vendor/gitignore/Typo3.gitignore new file mode 100644 index 0000000000..cb024fefe9 --- /dev/null +++ b/vendor/gitignore/Typo3.gitignore @@ -0,0 +1,20 @@ +## TYPO3 v6.2 +# Ignore several upload and file directories. +/fileadmin/user_upload/ +/fileadmin/_temp_/ +/fileadmin/_processed_/ +/uploads/ +# Ignore cache +/typo3conf/temp_CACHED* +/typo3conf/temp_fieldInfo.php +/typo3conf/deprecation_*.log +/typo3conf/AdditionalConfiguration.php +# Ignore system folders, you should have them symlinked. +# If not comment out the following entries. +/typo3 +/typo3_src +/typo3_src-* +/.htaccess +/index.php +# Ignore temp directory. +/typo3temp/ diff --git a/vendor/gitignore/Umbraco.gitignore b/vendor/gitignore/Umbraco.gitignore new file mode 100644 index 0000000000..ea05e1fb2a --- /dev/null +++ b/vendor/gitignore/Umbraco.gitignore @@ -0,0 +1,19 @@ +# Note: VisualStudio gitignore rules may also be relevant + +# Umbraco +# Ignore unimportant folders generated by Umbraco +**/App_Data/Logs/ +**/App_Data/[Pp]review/ +**/App_Data/TEMP/ +**/App_Data/NuGetBackup/ + +# Ignore Umbraco content cache file +**/App_Data/umbraco.config + +# Don't ignore Umbraco packages (VisualStudio.gitignore mistakes this for a NuGet packages folder) +# Make sure to include details from VisualStudio.gitignore BEFORE this +!**/App_Data/[Pp]ackages/ +!**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages + +# ImageProcessor DiskCache +**/App_Data/cache/ diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore new file mode 100644 index 0000000000..5aafcbb7f1 --- /dev/null +++ b/vendor/gitignore/Unity.gitignore @@ -0,0 +1,30 @@ +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/Assets/AssetStoreTools* + +# Autogenerated VS/MD solution and project files +ExportedObj/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd + + +# Unity3D generated meta files +*.pidb.meta + +# Unity3D Generated File On Crash Reports +sysinfo.txt + +# Builds +*.apk +*.unitypackage diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore new file mode 100644 index 0000000000..75b1186b0a --- /dev/null +++ b/vendor/gitignore/UnrealEngine.gitignore @@ -0,0 +1,62 @@ +# Visual Studio 2015 user specific files +.vs/ + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app +*.ipa + +# These project files can be generated by the engine +*.xcodeproj +*.sln +*.suo +*.opensdf +*.sdf +*.VC.opendb + +# Precompiled Assets +SourceArt/**/*.png +SourceArt/**/*.tga + +# Binary Files +Binaries/* + +# Builds +Build/* + +# Don't ignore icon files in Build +!Build/**/*.ico + +# Configuration files generated by the Editor +Saved/* + +# Compiled source files for the engine to use +Intermediate/* + +# Cache files for the editor to use +DerivedDataCache/* diff --git a/vendor/gitignore/VVVV.gitignore b/vendor/gitignore/VVVV.gitignore new file mode 100644 index 0000000000..5df4324603 --- /dev/null +++ b/vendor/gitignore/VVVV.gitignore @@ -0,0 +1,6 @@ + +# .v4p backup files +*~.xml + +# Dynamic plugins .dll +bin/ diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore new file mode 100644 index 0000000000..f1e3d20e05 --- /dev/null +++ b/vendor/gitignore/VisualStudio.gitignore @@ -0,0 +1,252 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/vendor/gitignore/Waf.gitignore b/vendor/gitignore/Waf.gitignore new file mode 100644 index 0000000000..48e8d8f7be --- /dev/null +++ b/vendor/gitignore/Waf.gitignore @@ -0,0 +1,4 @@ +# for projects that use Waf for building: http://code.google.com/p/waf/ +.waf-* +.waf3-* +.lock-* diff --git a/vendor/gitignore/WordPress.gitignore b/vendor/gitignore/WordPress.gitignore new file mode 100644 index 0000000000..97923503c4 --- /dev/null +++ b/vendor/gitignore/WordPress.gitignore @@ -0,0 +1,18 @@ +*.log +wp-config.php +wp-content/advanced-cache.php +wp-content/backup-db/ +wp-content/backups/ +wp-content/blogs.dir/ +wp-content/cache/ +wp-content/upgrade/ +wp-content/uploads/ +wp-content/wp-cache-config.php +wp-content/plugins/hello.php + +/.htaccess +/license.txt +/readme.html +/sitemap.xml +/sitemap.xml.gz + diff --git a/vendor/gitignore/Xojo.gitignore b/vendor/gitignore/Xojo.gitignore new file mode 100644 index 0000000000..1b036dd4f2 --- /dev/null +++ b/vendor/gitignore/Xojo.gitignore @@ -0,0 +1,11 @@ +# Xojo (formerly REALbasic and Real Studio) + +Builds* +*.debug +*.debug.app +Debug*.exe +Debug*/Debug*.exe +Debug*/Debug*\ Libs +*.rbuistate +*.xojo_uistate +*.obsolete diff --git a/vendor/gitignore/Yeoman.gitignore b/vendor/gitignore/Yeoman.gitignore new file mode 100644 index 0000000000..7170d72018 --- /dev/null +++ b/vendor/gitignore/Yeoman.gitignore @@ -0,0 +1,6 @@ +node_modules/ +bower_components/ +*.log + +build/ +dist/ diff --git a/vendor/gitignore/Yii.gitignore b/vendor/gitignore/Yii.gitignore new file mode 100644 index 0000000000..70f087546f --- /dev/null +++ b/vendor/gitignore/Yii.gitignore @@ -0,0 +1,6 @@ +assets/* +!assets/.gitignore +protected/runtime/* +!protected/runtime/.gitignore +protected/data/*.db +themes/classic/views/ diff --git a/vendor/gitignore/ZendFramework.gitignore b/vendor/gitignore/ZendFramework.gitignore new file mode 100644 index 0000000000..80adb15490 --- /dev/null +++ b/vendor/gitignore/ZendFramework.gitignore @@ -0,0 +1,25 @@ +# Composer files +composer.phar +vendor/ + +# Local configs +config/autoload/*.local.php + +# Binary gettext files +*.mo + +# Data +data/logs/ +data/cache/ +data/sessions/ +data/tmp/ +temp/ + +#Doctrine 2 +data/DoctrineORMModule/Proxy/ +data/DoctrineORMModule/cache/ + + +# Legacy ZF1 +demos/ +extras/documentation diff --git a/vendor/gitignore/Zephir.gitignore b/vendor/gitignore/Zephir.gitignore new file mode 100644 index 0000000000..839cb5d707 --- /dev/null +++ b/vendor/gitignore/Zephir.gitignore @@ -0,0 +1,26 @@ +# Cache files, generates by Zephir +.temp/ +.libs/ + +# Object files, generates by linker +*.lo +*.la +*.o +*.loT + +# Files generated by configure and Zephir, +# not required for extension compilation. +ext/build/ +ext/modules/ +ext/Makefile* +ext/config* +ext/acinclude.m4 +ext/aclocal.m4 +ext/autom4te* +ext/install-sh +ext/ltmain.sh +ext/missing +ext/mkinstalldirs +ext/run-tests.php +ext/.deps +ext/libtool From 1f5fcb638d6b432d76a639ccc35acc94d8ae6ac7 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 11 May 2016 19:38:43 -0500 Subject: [PATCH 394/449] Dropdown implementation --- CHANGELOG | 2 + .../blob/blob_gitignore_selector.js.coffee | 3 +- app/assets/javascripts/gl_dropdown.js.coffee | 67 +++++++++++++++---- app/assets/stylesheets/pages/editor.scss | 2 +- app/helpers/blob_helper.rb | 8 ++- app/views/projects/blob/_editor.html.haml | 4 +- .../projects/files/gitignore_dropdown_spec.rb | 30 +++++++++ 7 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 spec/features/projects/files/gitignore_dropdown_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 457af51605..683f1b1a65 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -71,6 +71,8 @@ v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) - Fix import from GitLab.com to a private instance failure. !4181 - Fix external imports not finding the import data. !4106 + - Fix import from gitlab.com to a private instance failure + - When creating a .gitignore file a dropdown with templates will be provided v 8.7.5 - Fix relative links in wiki pages. !4050 diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee index b413f31da0..2112f5c855 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee @@ -29,7 +29,8 @@ class @BlobGitIgnoreSelector filename = @fileNameInput.val() or $('.editor-file-name').text().trim() @wrapper.toggleClass 'hidden', filename isnt '.gitignore' - onClick: (item) -> + onClick: (item, el, e) -> + e.preventDefault() @requestIgnoreFile(item.text) requestIgnoreFile: (name) -> diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 1d1bfeb2e7..0f2ac664ac 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -60,9 +60,23 @@ class GitLabDropdownFilter results = data if search_text isnt '' - results = fuzzaldrinPlus.filter(data, search_text, - key: @options.keys - ) + # handle groups + if _.isArray(data) + results = fuzzaldrinPlus.filter(data, search_text, + key: @options.keys + ) + else if _.isObject(data) + results = {} + for key, group of data + tmp = fuzzaldrinPlus.filter(group, search_text, + key: @options.keys + ) + + if tmp.length + results[key] = [] + + tmp.map (item) -> + results[key].push item @options.callback results else @@ -141,8 +155,9 @@ class GitLabDropdown searchFields = if @options.search then @options.search.fields else []; if @options.data - # If data is an array - if _.isArray @options.data + # If we provided data + # data could be an array of objects or a group of arrays + if _.isObject(@options.data) and not _.isFunction(@options.data) @fullData = @options.data @parseData @options.data else @@ -230,19 +245,33 @@ class GitLabDropdown parseData: (data) -> @renderedData = data - # Render each row - html = $.map data, (obj) => - return @renderItem(obj) - if @options.filterable and data.length is 0 # render no matching results html = [@noResults()] + else + # Handle array groups + if String(data) is "[object Object]" + html = [] + for name, groupData of data + # Add header for each group + html.push(@renderItem(header: name, name)) + + @renderData(groupData, name) + .map (item) -> + html.push item + else + # Render each row + html = @renderData(data) # Render the full menu full_html = @renderMenu(html.join("")) @appendMenu(full_html) + renderData: (data, group = false) -> + $.map data, (obj, index) => + return @renderItem(obj, group, index) + shouldPropagate: (e) => if @options.multiSelect $target = $(e.target) @@ -299,11 +328,10 @@ class GitLabDropdown selector = '.dropdown-content' if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one .dropdown-content" - $(selector, @dropdown).html html # Render the row - renderItem: (data) -> + renderItem: (data, group = false, index = false) -> html = "" # Divider @@ -346,8 +374,13 @@ class GitLabDropdown if @highlight text = @highlightTextMatches(text, @filterInput.val()) + if group + groupAttrs = "data-group='#{group}' data-index='#{index}'" + else + groupAttrs = '' + html = "
  • - + #{text}
  • " @@ -377,9 +410,15 @@ class GitLabDropdown rowClicked: (el) -> fieldName = @options.fieldName - selectedIndex = el.parent().index() if @renderedData - selectedObject = @renderedData[selectedIndex] + groupName = el.data('group') + if groupName + selectedIndex = el.data('index') + selectedObject = @renderedData[groupName][selectedIndex] + else + selectedIndex = el.parent().index() + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") if el.hasClass(ACTIVE_CLASS) diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 3030da4e6b..22679c764d 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -72,7 +72,7 @@ line-height: 21px; } - .dropdown-menu-toggle{ + .dropdown-menu-toggle { vertical-align: top; width: 220px; } diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index fb1b764946..a3723d2c34 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -186,6 +186,12 @@ module BlobHelper end def gitignores_for_select - @gitignores_for_select ||= Gitlab::Gitignore.all + return @gitignores_for_select if defined?(@gitignores_for_select) + + @gitignores_for_select = { + Global: Gitlab::Gitignore.global.map{ |v| { text: v.name } }, + # Note that the key here doesn't cover it really + Languages: Gitlab::Gitignore.languages_frameworks.map{ |v| { text: v.name } } + } end end diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 5efec92cf6..678eadafe1 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -17,9 +17,7 @@ = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name} .gitignore-selector.hidden - = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter projects", data: { filenames: Gitlab::Gitignore.all.map.with_index{ |v| {text: v.name} } } } ) - - -# = select_tag :gitignore_template, options_for_select(Gitlab::Gitignore.all.map(&:name)), include_blank: true, class: 'select2 gitignore-select', data: {placeholder: 'Choose a .gitignore template'} + = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignores_for_select } } ) .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb new file mode 100644 index 0000000000..073a83b689 --- /dev/null +++ b/spec/features/projects/files/gitignore_dropdown_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +feature 'User wants to add a .gitignore file', feature: true do + include WaitForAjax + + before do + user = create(:user) + project = create(:project) + project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitignore') + end + + scenario 'user can see .gitignore dropdown' do + expect(page).to have_css('.gitignore-selector') + end + + scenario 'user can pick a .gitignore file from the dropdown', js: true do + find('.js-gitignore-selector').click + wait_for_ajax + within '.gitignore-selector' do + find('.dropdown-input-field').set('rails') + find('.dropdown-content li', text: 'Rails').click + end + wait_for_ajax + + expect(page).to have_content('/.bundle') + expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset') + end +end From 79620c501da19bfda5818b8dca75b6ec9c10e762 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 12 May 2016 13:27:25 +0200 Subject: [PATCH 395/449] Update API and fetching task --- lib/gitlab/gitignore.rb | 40 ++++++++--------------- lib/tasks/gitlab/update_gitignore.rake | 44 +++++++++++++++++++------- spec/lib/gitlab/gitignore_spec.rb | 4 ++- spec/requests/api/gitignores_spec.rb | 4 +-- 4 files changed, 51 insertions(+), 41 deletions(-) diff --git a/lib/gitlab/gitignore.rb b/lib/gitlab/gitignore.rb index a2de2831e3..f46b43b61a 100644 --- a/lib/gitlab/gitignore.rb +++ b/lib/gitlab/gitignore.rb @@ -2,15 +2,16 @@ module Gitlab class Gitignore FILTER_REGEX = /\.gitignore\z/.freeze - attr_accessor :name, :directory + def initialize(path) + @path = path + end - def initialize(name, directory) - @name = name - @directory = directory + def name + File.basename(@path, '.gitignore') end def content - File.read(path) + File.read(@path) end class << self @@ -22,46 +23,33 @@ module Gitlab file_name = "#{key}.gitignore" directory = select_directory(file_name) - directory ? new(key, directory) : nil + directory ? new(File.join(directory, file_name)) : nil end def global - files_for_folder(global_dir).map { |f| new(f, global_dir) } + files_for_folder(global_dir).map { |file| new(File.join(global_dir, file)) } end def languages_frameworks - files_for_folder(gitignore_dir).map { |f| new(f, gitignore_dir) } + files_for_folder(gitignore_dir).map { |file| new(File.join(gitignore_dir, file)) } end - end - private + private - def path - File.expand_path("#{name}.gitignore", directory) - end - - class << self def select_directory(file_name) - [self.gitignore_dir, self.global_dir].find { |dir| File.exist?(File.expand_path(file_name, dir)) } + [gitignore_dir, global_dir].find { |dir| File.exist?(File.join(dir, file_name)) } end def global_dir - File.expand_path('Global', gitignore_dir) + File.join(gitignore_dir, 'Global') end def gitignore_dir - File.expand_path('vendor/gitignore', Rails.root) + Rails.root.join('vendor/gitignore') end def files_for_folder(dir) - gitignores = [] - Dir.entries(dir).each do |e| - next unless e.end_with?('.gitignore') - - gitignores << e.gsub(FILTER_REGEX, '') - end - - gitignores + Dir.glob("#{dir.to_s}/*.gitignore").map { |file| file.gsub(FILTER_REGEX, '') } end end end diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake index 61cbfd6737..84aa312002 100644 --- a/lib/tasks/gitlab/update_gitignore.rake +++ b/lib/tasks/gitlab/update_gitignore.rake @@ -1,26 +1,46 @@ namespace :gitlab do desc "GitLab | Update gitignore" task :update_gitignore do - dir = File.expand_path('vendor', Rails.root) - FileUtils.cd(dir) + unless clone_gitignores + puts "Cloning the gitignores failed".red + return + end - dir = File.expand_path('gitignore', dir) - clone_gitignores(dir) - remove_unneeded_files(dir) + remove_unneeded_files(gitignore_directory) + remove_unneeded_files(global_directory) puts "Done".green end - def clone_gitignores(dir) - FileUtils.rm_rf(dir) if Dir.exist?(dir) + def clone_gitignores + FileUtils.rm_rf(gitignore_directory) if Dir.exist?(gitignore_directory) + FileUtils.cd vendor_directory + system('git clone --depth=1 --branch=master https://github.com/github/gitignore.git') end - def remove_unneeded_files(dir) - [File.expand_path('Global', dir), dir].each do |path| - Dir.entries(path).reject { |e| e =~ /(\.{1,2}|Global|\.gitignore)\z/ }.each do |file| - FileUtils.rm_rf File.expand_path(file, path) - end + # Retain only certain files: + # - The LICENSE, because we have to + # - The sub dir global + # - The gitignores themself + # - Dir.entires returns also the entries '.' and '..' + def remove_unneeded_files(path) + Dir.foreach(path) do |file| + FileUtils.rm_rf(File.join(path, file)) unless file =~ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/ end end + + private + + def vendor_directory + Rails.root.join('vendor') + end + + def gitignore_directory + File.join(vendor_directory, 'gitignore') + end + + def global_directory + File.join(gitignore_directory, 'Global') + end end diff --git a/spec/lib/gitlab/gitignore_spec.rb b/spec/lib/gitlab/gitignore_spec.rb index 5dab821a8e..72baa516cc 100644 --- a/spec/lib/gitlab/gitignore_spec.rb +++ b/spec/lib/gitlab/gitignore_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::Gitignore do subject { Gitlab::Gitignore } + describe '.all' do it 'strips the gitignore suffix' do expect(subject.all.first.name).not_to end_with('.gitignore') @@ -24,12 +25,13 @@ describe Gitlab::Gitignore do ruby = subject.find('Ruby') expect(ruby).to be_a Gitlab::Gitignore + expect(ruby.name).to eq('Ruby') end end describe '#content' do it 'loads the full file' do - gitignore = subject.new('Ruby', File.expand_path('vendor/gitignore', Rails.root)) + gitignore = subject.new(Rails.root.join('vendor/gitignore/Ruby.gitignore')) expect(gitignore.name).to eq 'Ruby' expect(gitignore.content).to start_with('*.gem') diff --git a/spec/requests/api/gitignores_spec.rb b/spec/requests/api/gitignores_spec.rb index d0e576e637..aab2d8c81b 100644 --- a/spec/requests/api/gitignores_spec.rb +++ b/spec/requests/api/gitignores_spec.rb @@ -13,8 +13,8 @@ describe API::Gitignores, api: true do describe 'Entity GitignoresList' do before { get api('/gitignores') } - it { expect(json_response.first['name']).to be_truthy } - it { expect(json_response.first['content']).to be_falsey } + it { expect(json_response.first['name']).not_to be_nil } + it { expect(json_response.first['content']).to be_nil } end describe 'GET /gitignores' do From ab96ca2bf1ae72817ff5cedf1792c8f7563ebdef Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 13 May 2016 10:57:03 -0500 Subject: [PATCH 396/449] Dropdown implementation --- CHANGELOG | 3 +- app/assets/javascripts/api.js.coffee | 34 ++++++------- .../blob/blob_gitignore_selector.js.coffee | 51 +++++++++---------- .../javascripts/blob/edit_blob.js.coffee | 2 +- app/assets/javascripts/gl_dropdown.js.coffee | 43 ++++++++++------ .../javascripts/lib/type_utility.js.coffee | 9 ++++ app/helpers/blob_helper.rb | 10 ++-- app/models/repository.rb | 6 ++- app/views/projects/blob/_editor.html.haml | 2 +- lib/api/gitignores.rb | 10 ++-- spec/javascripts/project_title_spec.js.coffee | 1 + 11 files changed, 96 insertions(+), 75 deletions(-) create mode 100644 app/assets/javascripts/lib/type_utility.js.coffee diff --git a/CHANGELOG b/CHANGELOG index 683f1b1a65..e87fde3fec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -66,13 +66,12 @@ v 8.8.0 (unreleased) - All Grape API helpers are now instrumented - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) - Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine) + - When creating a .gitignore file a dropdown with templates will be provided v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) - Fix import from GitLab.com to a private instance failure. !4181 - Fix external imports not finding the import data. !4106 - - Fix import from gitlab.com to a private instance failure - - When creating a .gitignore file a dropdown with templates will be provided v 8.7.5 - Fix relative links in wiki pages. !4050 diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index c654f8de20..3f61ea1eaf 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -1,15 +1,15 @@ @Api = - groups_path: "/api/:version/groups.json" - group_path: "/api/:version/groups/:id.json" - namespaces_path: "/api/:version/namespaces.json" - group_projects_path: "/api/:version/groups/:id/projects.json" - projects_path: "/api/:version/projects.json" - labels_path: "/api/:version/projects/:id/labels" - license_path: "/api/:version/licenses/:key" - gitignoretext_path: "/api/:version/gitignores/:key" + groupsPath: "/api/:version/groups.json" + groupPath: "/api/:version/groups/:id.json" + namespacesPath: "/api/:version/namespaces.json" + groupProjectsPath: "/api/:version/groups/:id/projects.json" + projectsPath: "/api/:version/projects.json" + labelsPath: "/api/:version/projects/:id/labels" + licensePath: "/api/:version/licenses/:key" + gitignorePath: "/api/:version/gitignores/:key" group: (group_id, callback) -> - url = Api.buildUrl(Api.group_path) + url = Api.buildUrl(Api.groupPath) url = url.replace(':id', group_id) $.ajax( @@ -23,7 +23,7 @@ # Return groups list. Filtered by query # Only active groups retrieved groups: (query, skip_ldap, callback) -> - url = Api.buildUrl(Api.groups_path) + url = Api.buildUrl(Api.groupsPath) $.ajax( url: url @@ -37,7 +37,7 @@ # Return namespaces list. Filtered by query namespaces: (query, callback) -> - url = Api.buildUrl(Api.namespaces_path) + url = Api.buildUrl(Api.namespacesPath) $.ajax( url: url @@ -51,7 +51,7 @@ # Return projects list. Filtered by query projects: (query, order, callback) -> - url = Api.buildUrl(Api.projects_path) + url = Api.buildUrl(Api.projectsPath) $.ajax( url: url @@ -65,7 +65,7 @@ callback(projects) newLabel: (project_id, data, callback) -> - url = Api.buildUrl(Api.labels_path) + url = Api.buildUrl(Api.labelsPath) url = url.replace(':id', project_id) data.private_token = gon.api_token @@ -81,7 +81,7 @@ # Return group projects list. Filtered by query groupProjects: (group_id, query, callback) -> - url = Api.buildUrl(Api.group_projects_path) + url = Api.buildUrl(Api.groupProjectsPath) url = url.replace(':id', group_id) $.ajax( @@ -96,7 +96,7 @@ # Return text for a specific license licenseText: (key, data, callback) -> - url = Api.buildUrl(Api.license_path).replace(':key', key) + url = Api.buildUrl(Api.licensePath).replace(':key', key) $.ajax( url: url @@ -104,8 +104,8 @@ ).done (license) -> callback(license) - gitIgnoreText: (key, callback) -> - url = Api.buildUrl(Api.gitignoretext_path).replace(':key', key) + gitignoreText: (key, callback) -> + url = Api.buildUrl(Api.gitignorePath).replace(':key', key) $.get url, (gitignore) -> callback(gitignore) diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee index 2112f5c855..cc8a497d08 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee @@ -1,10 +1,10 @@ -class @BlobGitIgnoreSelector +class @BlobGitignoreSelector constructor: (opts) -> { @dropdown @editor - @wrapper = @dropdown.parents('.gitignore-selector') - @fileNameInput = $('#file_name') + @$wrapper = @dropdown.closest('.gitignore-selector') + @$filenameInput = $('#file_name') @data = @dropdown.data('filenames') } = opts @@ -13,51 +13,46 @@ class @BlobGitIgnoreSelector filterable: true, selectable: true, search: - fields: ['text'] - clicked: @onClick.bind(@) + fields: ['name'] + clicked: @onClick + text: (gitignore) -> + gitignore.name ) - @toggleGitIgnoreSelector() + @toggleGitignoreSelector() @bindEvents() bindEvents: -> - @fileNameInput + @$filenameInput .on 'keyup blur', (e) => - @toggleGitIgnoreSelector() + @toggleGitignoreSelector() - toggleGitIgnoreSelector: -> - filename = @fileNameInput.val() or $('.editor-file-name').text().trim() - @wrapper.toggleClass 'hidden', filename isnt '.gitignore' + toggleGitignoreSelector: -> + filename = @$filenameInput.val() or $('.editor-file-name').text().trim() + @$wrapper.toggleClass 'hidden', filename isnt '.gitignore' - onClick: (item, el, e) -> + onClick: (item, el, e) => e.preventDefault() - @requestIgnoreFile(item.text) + @requestIgnoreFile(item.name) requestIgnoreFile: (name) -> - Api.gitIgnoreText name, @requestIgnoreFileSuccess.bind(@) + Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@) requestIgnoreFileSuccess: (gitignore) -> - @editor.setValue(gitignore.content, -1) - - # Move cursor position to end of file - row = @editor.session.getLength() - 1 - column = @editor.session.getLine(row).length - @editor.gotoLine(row + 1, column) + @editor.setValue(gitignore.content, 1) @editor.focus() -class @BlobGitIgnoreSelectors +class @BlobGitignoreSelectors constructor: (opts) -> - _this = @ - { - @dropdowns = $('.js-gitignore-selector') + @$dropdowns = $('.js-gitignore-selector') @editor } = opts - @dropdowns.each -> - $dropdown = $(@) + @$dropdowns.each (i, dropdown) => + $dropdown = $(dropdown) - new BlobGitIgnoreSelector( + new BlobGitignoreSelector( dropdown: $dropdown, - editor: _this.editor + editor: @editor ) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index f2efeb3960..79141e768b 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -13,7 +13,7 @@ class @EditBlob @initModePanesAndLinks() new BlobLicenseSelector(@editor) - new BlobGitIgnoreSelectors(editor: @editor) + new BlobGitignoreSelectors(editor: @editor) initModePanesAndLinks: -> @$editModePanes = $(".js-edit-mode-pane") diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 0f2ac664ac..8cb31b2dbd 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -60,23 +60,36 @@ class GitLabDropdownFilter results = data if search_text isnt '' - # handle groups + # When data is an array of objects therefore [object Array] e.g. + # [ + # { prop: 'foo' }, + # { prop: 'baz' } + # ] if _.isArray(data) results = fuzzaldrinPlus.filter(data, search_text, key: @options.keys ) - else if _.isObject(data) - results = {} - for key, group of data - tmp = fuzzaldrinPlus.filter(group, search_text, - key: @options.keys - ) + else + # If data is grouped therefore an [object Object]. e.g. + # { + # groupName1: [ + # { prop: 'foo' }, + # { prop: 'baz' } + # ], + # groupName2: [ + # { prop: 'abc' }, + # { prop: 'def' } + # ] + # } + if gl.utils.isObject data + results = {} + for key, group of data + tmp = fuzzaldrinPlus.filter(group, search_text, + key: @options.keys + ) - if tmp.length - results[key] = [] - - tmp.map (item) -> - results[key].push item + if tmp.length + results[key] = tmp.map (item) -> item @options.callback results else @@ -250,7 +263,7 @@ class GitLabDropdown html = [@noResults()] else # Handle array groups - if String(data) is "[object Object]" + if gl.utils.isObject data html = [] for name, groupData of data # Add header for each group @@ -269,7 +282,7 @@ class GitLabDropdown @appendMenu(full_html) renderData: (data, group = false) -> - $.map data, (obj, index) => + data.map (obj, index) => return @renderItem(obj, group, index) shouldPropagate: (e) => @@ -416,7 +429,7 @@ class GitLabDropdown selectedIndex = el.data('index') selectedObject = @renderedData[groupName][selectedIndex] else - selectedIndex = el.parent().index() + selectedIndex = el.closest('li').index() selectedObject = @renderedData[selectedIndex] value = if @options.id then @options.id(selectedObject, el) else selectedObject.id diff --git a/app/assets/javascripts/lib/type_utility.js.coffee b/app/assets/javascripts/lib/type_utility.js.coffee new file mode 100644 index 0000000000..957f0d86b3 --- /dev/null +++ b/app/assets/javascripts/lib/type_utility.js.coffee @@ -0,0 +1,9 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.isObject = (obj) -> + obj? and (obj.constructor is Object) + +) window diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index a3723d2c34..cec2dc753f 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -185,13 +185,13 @@ module BlobHelper } end - def gitignores_for_select - return @gitignores_for_select if defined?(@gitignores_for_select) + def gitignore_names + return @gitignore_names if defined?(@gitignore_names) - @gitignores_for_select = { - Global: Gitlab::Gitignore.global.map{ |v| { text: v.name } }, + @gitignore_names = { + Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } }, # Note that the key here doesn't cover it really - Languages: Gitlab::Gitignore.languages_frameworks.map{ |v| { text: v.name } } + Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } } } end end diff --git a/app/models/repository.rb b/app/models/repository.rb index f26278cc3a..ecc8795c95 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -245,7 +245,7 @@ class Repository def cache_keys %i(size branch_names tag_names commit_count readme version contribution_guide changelog - license_blob license_key) + license_blob license_key gitignore) end def build_cache @@ -256,6 +256,10 @@ class Repository end end + def expire_gitignore + cache.expire(:gitignore) + end + def expire_tags_cache cache.expire(:tag_names) @tags = nil diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 678eadafe1..4071b59c00 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -17,7 +17,7 @@ = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name} .gitignore-selector.hidden - = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignores_for_select } } ) + = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignore_names } } ) .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' diff --git a/lib/api/gitignores.rb b/lib/api/gitignores.rb index 1af9ba6b31..270c9501dd 100644 --- a/lib/api/gitignores.rb +++ b/lib/api/gitignores.rb @@ -12,15 +12,15 @@ module API # Get the text for a specific gitignore # # Parameters: - # key (required) - The key of a license + # name (required) - The name of a license # # Example Request: - # GET /gitignores/elixir + # GET /gitignores/Elixir # - get 'gitignores/:key' do - required_attributes! [:key] + get 'gitignores/:name' do + required_attributes! [:name] - gitignore = Gitlab::Gitignore.find(params[:key]) + gitignore = Gitlab::Gitignore.find(params[:name]) not_found!('.gitignore') unless gitignore present gitignore, with: Entities::Gitignore diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee index 3d8de2ff98..1cf34d4d2d 100644 --- a/spec/javascripts/project_title_spec.js.coffee +++ b/spec/javascripts/project_title_spec.js.coffee @@ -1,5 +1,6 @@ #= require bootstrap #= require select2 +#= require lib/type_utility #= require gl_dropdown #= require api #= require project_select From 4474eab4f6a17861a6ed494034bc68acb0724e3c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 20 May 2016 18:43:11 -0500 Subject: [PATCH 397/449] Fix container deletion permission issue --- .../container_registry_authentication_service.rb | 2 +- .../projects/container_registry/index.html.haml | 2 +- ...ontainer_registry_authentication_service_spec.rb | 13 ++++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 3144e96ba3..f807b8ec09 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -20,7 +20,7 @@ module Auth token.issuer = registry.issuer token.audience = AUDIENCE token[:access] = names.map do |name| - { type: 'repository', name: name, actions: %w(pull push) } + { type: 'repository', name: name, actions: %w(*) } end token.encoded end diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index 40957993b2..e1e762410f 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -4,7 +4,7 @@ %hr %ul.content-list - .light.prepend-top-default + %li.light.prepend-top-default %p A 'container image' is a snapshot of a container. You can host your container images with GitLab. diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 6c9f56a4fb..73b8c3f048 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -10,7 +10,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do subject { described_class.new(current_project, current_user, current_params).execute } before do - stub_container_registry_config(enabled: true, issuer: 'rspec', key: nil) + allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil) allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key) end @@ -60,6 +60,17 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it { is_expected.to_not include(:token) } end + describe '#full_access_token' do + let(:project) { create(:empty_project) } + let(:token) { described_class.full_access_token(project.path_with_namespace) } + + subject { { token: token } } + + it_behaves_like 'a accessible' do + let(:actions) { ['*'] } + end + end + context 'user authorization' do let(:project) { create(:project) } let(:current_user) { create(:user) } From 446059889634bf5682fca1f76fef17c6c8c31aa9 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 20 May 2016 20:50:13 -0500 Subject: [PATCH 398/449] Add README for updating .gitignore templates --- vendor/gitignore/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 vendor/gitignore/README.md diff --git a/vendor/gitignore/README.md b/vendor/gitignore/README.md new file mode 100644 index 0000000000..43131e815c --- /dev/null +++ b/vendor/gitignore/README.md @@ -0,0 +1,14 @@ +# .gitignore templates + +This directory contains language-specific .gitignore templates that are used by GitLab. + +These files were automatically pulled from [this repository](https://github.com/github/gitignore). +Please submit pull requests to that repository. There is no need to edit the files in this directory. + +## Bulk Update + +To update this directory with the latest changes in the repository, run: + +```sh +bundle exec rake gitlab:update_gitignore +``` From ad13d357e739216b4617848ce43d2c9f3557ef75 Mon Sep 17 00:00:00 2001 From: Tony Chu Date: Thu, 19 May 2016 06:13:46 +0000 Subject: [PATCH 399/449] fix user.name length in milestones participants tab --- app/views/shared/milestones/_participants_tab.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml index 67ae85ac27..549d2e2f61 100644 --- a/app/views/shared/milestones/_participants_tab.html.haml +++ b/app/views/shared/milestones/_participants_tab.html.haml @@ -3,6 +3,6 @@ %li = link_to user, title: user.name, class: "darken" do = image_tag avatar_icon(user, 32), class: "avatar s32" - %strong= truncate(user.name, lenght: 40) + %strong= truncate(user.name, length: 40) %br %small.cgray= user.username From c6411a785b0c49d17b831a9b4bc05ef4bb68b87f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 01:18:10 -0500 Subject: [PATCH 400/449] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 67fca2c6f6..01585ede58 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.8.0 (unreleased) - Added inline diff styling for `change_title` system notes. (Adam Butler) - Project#open_branches has been cleaned up and no longer loads entire records into memory. - Escape HTML in commit titles in system note messages + - Fix scope used when accessing container registry - Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios - Improve multiple branch push performance by memoizing permission checking - Log to application.log when an admin starts and stops impersonating a user From 69a07cf4d4771b9e754ce783b7973b7898480719 Mon Sep 17 00:00:00 2001 From: Sanster Date: Sat, 21 May 2016 22:33:54 +0800 Subject: [PATCH 401/449] fix group members api doc: remove emial --- doc/api/groups.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/api/groups.md b/doc/api/groups.md index 2821bc21b8..1ccb9715e9 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -265,7 +265,6 @@ GET /groups/:id/members { "id": 1, "username": "raymond_smith", - "email": "ray@smith.org", "name": "Raymond Smith", "state": "active", "created_at": "2012-10-22T14:13:35Z", @@ -274,7 +273,6 @@ GET /groups/:id/members { "id": 2, "username": "john_doe", - "email": "joh@doe.org", "name": "John Doe", "state": "active", "created_at": "2012-10-22T14:13:35Z", From 2264bb136b78a07b3dde3aaacbae7ba3918775e0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 22 May 2016 00:41:20 -0500 Subject: [PATCH 402/449] Copyedit health check docs --- doc/monitoring/health_check.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md index 5157cf686a..defbf37ac1 100644 --- a/doc/monitoring/health_check.md +++ b/doc/monitoring/health_check.md @@ -1,5 +1,6 @@ # Health Check -_**Note:** This feature was [introduced][ce-3888] in GitLab 8.8_ + +>**Note:** This feature was [introduced][ce-3888] in GitLab 8.8. GitLab provides a health check endpoint for uptime monitoring on the `health_check` web endpoint. The health check reports on the overall system status based on the status of @@ -14,11 +15,13 @@ accepted token can be found on the `admin/heath_check` page of your GitLab insta ![access token](img/health_check_token.png) -The access token can be passed as a url parameter: +The access token can be passed as a URL parameter: -`https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN` +``` +https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN +``` -or as a http header: +or as an HTTP header: ```bash curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json @@ -39,16 +42,22 @@ You can also ask for the status of specific services: - `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN` - `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN` -Example output: +For example, the JSON output of the following health check: + ```bash curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +``` + +would be like: + +``` {"healthy":true,"message":"success"} ``` ## Status -On failure the endpoint will return a `500` http status code. On success the endpoint -will return a valid successful http status code, and a `success` message. Ideally your +On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint +will return a valid successful HTTP status code, and a `success` message. Ideally your uptime monitoring should look for the success message. [ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888 From 18dddc0c84f2f88e9b37883f1a745dbbeb74b6f3 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 13 May 2016 11:44:48 -0400 Subject: [PATCH 403/449] Initial docker container registry configuration docs. --- doc/administration/container_registry.md | 127 +++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 doc/administration/container_registry.md diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md new file mode 100644 index 0000000000..d89cfd1d43 --- /dev/null +++ b/doc/administration/container_registry.md @@ -0,0 +1,127 @@ +# GitLab Container Registry Administration + +Documentation on how to use Container Registry are under [TODO](TODO.md). + +## Configuration + +Containers can be large in size and they are stored on the server GitLab is +installed on. + +The Container Registry works under HTTPS by default. +This means that the Container Registry requires a SSL certificate. +There are two options on how this can be configured: + +1. Use its own domain - needs a SSL certificate for that specific domain +(eg. registry.example.com) or a wildcard certificate if hosted under a subdomain +(eg. registry.gitlab.example.com) +1. Use existing GitLab domain and expose the registry on a port - can reuse +existing GitLab SSL certificate + +Note that using HTTP is possible, +[see insecure Registry document.](https://github.com/docker/distribution/blob/master/docs/insecure.md) + +Please take this into consideration before configuring Container Registry for +the first time. + +## Container Registry under its own domain + +Lets assume that you want the Container Registry to be accessible at +`https://registry.gitlab.example.com`. + +### Omnibus GitLab packages + +Place your SSL certificate and key in +`/etc/gitlab/ssl/registry.gitlab.example.com.crt` +and +`/etc/gitlab/ssl/registry.gitlab.example.com.key` and make sure they have +correct permissions: + +```bash +chmod 600 /etc/gitlab/ssl/registry.gitlab.example.com.* +``` + +Once the SSL certificate is in place, edit `/etc/gitlab/gitlab.rb` with: + +```ruby +registry_external_url 'https://registry.gitlab.example.com' +``` + +Save the file and [reconfigure GitLab][] for the changes to take effect. + +Users should now be able to login to the Container Registry using: + +```bash +docker login registry.gitlab.example.com +``` + +with their GitLab credentials. + +If you have a [wildcard certificate][], you need to specify the path to the +certificate in addition to the URL, in this case `/etc/gitlab/gitlab.rb` will +look like: + +```ruby +registry_external_url 'https://registry.gitlab.example.com' +registry_nginx['ssl_certificate'] = "/etc/gitlab/ssl/certificate.pem" +registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key" +``` + +## Container Registry under existing GitLab domain + +Lets assume that your GitLab instance is accessible at +`https://gitlab.example.com`. You can expose the Container Registry under +a separate port. + +Lets assume that you've exposed port `4567` in your network firewall. + +### Omnibus GitLab packages + +Your `/etc/gitlab/gitlab.rb` should contain the Container Registry URL as +well as the path to the existing SSL certificate and key used by GitLab. + +```ruby +registry_external_url 'https://gitlab.example.com:4567' + +## If your SSL certificate is not in /etc/gitlab/ssl/gitlab.example.com.crt +## and key not in /etc/gitlab/ssl/gitlab.example.com.key uncomment the lines +## below + +# registry_nginx['ssl_certificate'] = "/path/to/certificate.pem" +# registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key" +``` + +Save the file and [reconfigure GitLab][] for the changes to take effect. + +Users should now be able to login to the Container Registry using: + +```bash +docker login gitlab.example.com:4567 +``` + +with their GitLab credentials. + +## Container Registry storage path + +It is possible to change path where containers will be stored by the Container +Registry. + +### Omnibus GitLab packages + +By default, the path Container Registry is using to store the containers is in +`/var/opt/gitlab/gitlab-rails/shared/registry`. +This path is accessible to the user running the Container Registry daemon, +user running GitLab and to the user running Nginx web server. + +In `/etc/gitlab/gitlab.rb`: + +```ruby +gitlab_rails['registry_path'] = "/path/to/registry/storage" +``` + +Save the file and [reconfigure GitLab][] for the changes to take effect. + +**NOTE** You should confirm that the GitLab, registry and the web server user +have access to this directory. + +[reconfigure gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation" +[wildcard certificate]: "https://en.wikipedia.org/wiki/Wildcard_certificate" From 63c1792eface9ae8345d2dfe814b83ab8a401ea4 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 15 May 2016 10:02:20 -0500 Subject: [PATCH 404/449] Rearrange headings --- doc/administration/container_registry.md | 42 +++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index d89cfd1d43..2b7e143f68 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -1,6 +1,18 @@ # GitLab Container Registry Administration -Documentation on how to use Container Registry are under [TODO](TODO.md). +> **Note:** +This feature was [introduced][ce-4040] in GitLab 8.8. + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Configuration](#configuration) + - [Container Registry under its own domain](#container-registry-under-its-own-domain) + - [Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) +- [Container Registry storage path](#container-registry-storage-path) + + ## Configuration @@ -12,23 +24,25 @@ This means that the Container Registry requires a SSL certificate. There are two options on how this can be configured: 1. Use its own domain - needs a SSL certificate for that specific domain -(eg. registry.example.com) or a wildcard certificate if hosted under a subdomain -(eg. registry.gitlab.example.com) + (eg. registry.example.com) or a wildcard certificate if hosted under a subdomain + (eg. registry.gitlab.example.com) 1. Use existing GitLab domain and expose the registry on a port - can reuse -existing GitLab SSL certificate + existing GitLab SSL certificate -Note that using HTTP is possible, -[see insecure Registry document.](https://github.com/docker/distribution/blob/master/docs/insecure.md) +Note that using HTTP is possible but not recommended, +[see insecure Registry document][docker-insecure]. Please take this into consideration before configuring Container Registry for the first time. -## Container Registry under its own domain +### Container Registry under its own domain Lets assume that you want the Container Registry to be accessible at `https://registry.gitlab.example.com`. -### Omnibus GitLab packages +--- + +**Omnibus GitLab packages** Place your SSL certificate and key in `/etc/gitlab/ssl/registry.gitlab.example.com.crt` @@ -66,7 +80,7 @@ registry_nginx['ssl_certificate'] = "/etc/gitlab/ssl/certificate.pem" registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key" ``` -## Container Registry under existing GitLab domain +### Container Registry under existing GitLab domain Lets assume that your GitLab instance is accessible at `https://gitlab.example.com`. You can expose the Container Registry under @@ -74,7 +88,9 @@ a separate port. Lets assume that you've exposed port `4567` in your network firewall. -### Omnibus GitLab packages +**Omnibus GitLab packages** + +--- Your `/etc/gitlab/gitlab.rb` should contain the Container Registry URL as well as the path to the existing SSL certificate and key used by GitLab. @@ -105,7 +121,9 @@ with their GitLab credentials. It is possible to change path where containers will be stored by the Container Registry. -### Omnibus GitLab packages +**Omnibus GitLab packages** + +--- By default, the path Container Registry is using to store the containers is in `/var/opt/gitlab/gitlab-rails/shared/registry`. @@ -125,3 +143,5 @@ have access to this directory. [reconfigure gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation" [wildcard certificate]: "https://en.wikipedia.org/wiki/Wildcard_certificate" +[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 +[docker-insecure]: https://github.com/docker/distribution/blob/master/docs/insecure.md From 04d231ef8743db55784f5c1a1f50438445ed908d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 16 May 2016 19:21:06 -0500 Subject: [PATCH 405/449] Rearrange configuration parts --- doc/administration/container_registry.md | 76 ++++++++++++------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 2b7e143f68..1791c95df6 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -8,8 +8,8 @@ This feature was [introduced][ce-4040] in GitLab 8.8. **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [Configuration](#configuration) - - [Container Registry under its own domain](#container-registry-under-its-own-domain) - [Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) + - [Container Registry under its own domain](#container-registry-under-its-own-domain) - [Container Registry storage path](#container-registry-storage-path) @@ -26,7 +26,7 @@ There are two options on how this can be configured: 1. Use its own domain - needs a SSL certificate for that specific domain (eg. registry.example.com) or a wildcard certificate if hosted under a subdomain (eg. registry.gitlab.example.com) -1. Use existing GitLab domain and expose the registry on a port - can reuse +1. Use the existing GitLab domain and expose the registry on a port - can reuse existing GitLab SSL certificate Note that using HTTP is possible but not recommended, @@ -35,6 +35,42 @@ Note that using HTTP is possible but not recommended, Please take this into consideration before configuring Container Registry for the first time. +### Container Registry under existing GitLab domain + +Lets assume that your GitLab instance is accessible at +`https://gitlab.example.com`. You can expose the Container Registry under +a separate port. + +Lets assume that you've exposed port `4567` in your network firewall. + +**Omnibus GitLab packages** + +--- + +Your `/etc/gitlab/gitlab.rb` should contain the Container Registry URL as +well as the path to the existing SSL certificate and key used by GitLab. + +```ruby +registry_external_url 'https://gitlab.example.com:4567' + +## If your SSL certificate is not in /etc/gitlab/ssl/gitlab.example.com.crt +## and key not in /etc/gitlab/ssl/gitlab.example.com.key uncomment the lines +## below + +# registry_nginx['ssl_certificate'] = "/path/to/certificate.pem" +# registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key" +``` + +Save the file and [reconfigure GitLab][] for the changes to take effect. + +Users should now be able to login to the Container Registry using: + +```bash +docker login gitlab.example.com:4567 +``` + +with their GitLab credentials. + ### Container Registry under its own domain Lets assume that you want the Container Registry to be accessible at @@ -80,42 +116,6 @@ registry_nginx['ssl_certificate'] = "/etc/gitlab/ssl/certificate.pem" registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key" ``` -### Container Registry under existing GitLab domain - -Lets assume that your GitLab instance is accessible at -`https://gitlab.example.com`. You can expose the Container Registry under -a separate port. - -Lets assume that you've exposed port `4567` in your network firewall. - -**Omnibus GitLab packages** - ---- - -Your `/etc/gitlab/gitlab.rb` should contain the Container Registry URL as -well as the path to the existing SSL certificate and key used by GitLab. - -```ruby -registry_external_url 'https://gitlab.example.com:4567' - -## If your SSL certificate is not in /etc/gitlab/ssl/gitlab.example.com.crt -## and key not in /etc/gitlab/ssl/gitlab.example.com.key uncomment the lines -## below - -# registry_nginx['ssl_certificate'] = "/path/to/certificate.pem" -# registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key" -``` - -Save the file and [reconfigure GitLab][] for the changes to take effect. - -Users should now be able to login to the Container Registry using: - -```bash -docker login gitlab.example.com:4567 -``` - -with their GitLab credentials. - ## Container Registry storage path It is possible to change path where containers will be stored by the Container From 0695cdee178ac995f7dec81dcd734eef7e28e36e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 18 May 2016 12:03:57 -0500 Subject: [PATCH 406/449] Add assumptions section and refactor domain configuration --- doc/administration/container_registry.md | 56 +++++++++++++++++------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 1791c95df6..17a0c229b9 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -7,33 +7,49 @@ This feature was [introduced][ce-4040] in GitLab 8.8. **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [Configuration](#configuration) +- [Assumptions](#assumptions) +- [Container Registry domain configuration](#container-registry-domain-configuration) - [Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) - [Container Registry under its own domain](#container-registry-under-its-own-domain) - [Container Registry storage path](#container-registry-storage-path) +- [Disable Container Registry](#disable-container-registry) +- [Changelog](#changelog) -## Configuration +## Assumptions -Containers can be large in size and they are stored on the server GitLab is -installed on. +If you are using Omnibus, you have to bare in mind the following: -The Container Registry works under HTTPS by default. -This means that the Container Registry requires a SSL certificate. -There are two options on how this can be configured: +- The container Registry will be enabled by default if GitLab is configured + with HTTPS and it will listen on port `5005`. If you want the Registry to + listen on a port other than `5005` which is the default, read [#Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) + on how to achieve that. You will also have to configure your firewall to allow + connections to that port. +- The Container Registry works under HTTPS by default. Note that using HTTP is + possible but not recommended and out of the scope of this document, + [see the insecure Registry documentation][docker-insecure] if you want to + implement this. -1. Use its own domain - needs a SSL certificate for that specific domain - (eg. registry.example.com) or a wildcard certificate if hosted under a subdomain - (eg. registry.gitlab.example.com) -1. Use the existing GitLab domain and expose the registry on a port - can reuse - existing GitLab SSL certificate +## Container Registry domain configuration -Note that using HTTP is possible but not recommended, -[see insecure Registry document][docker-insecure]. +There are two ways you can configure the container Registry domain. Either use +the existing GitLab domain where in that case the Registry will listen on a port, +or use a completely separate domain. Since the container Registry requires a +TLS certificate, in the end it all boils down to how easy or pricey is to +get a new TLS certificate. -Please take this into consideration before configuring Container Registry for -the first time. +1. If the Registry is configured to use its own domain, you will need a TLS + certificate for that specific domain (e.g., `registry.example.com`) or maybe + a wildcard certificate if hosted under a subdomain (e.g., `registry.gitlab.example.com`). +1. If the Registry is configured to use the existing GitLab domain, you can + expose the Registry on a port so that you can reuse the existing GitLab TLS + certificate. + +Please take this into consideration before configuring the Container Registry +for the first time. + +Read more about Docker Registry at https://docs.docker.com/registry/introduction/. ### Container Registry under existing GitLab domain @@ -141,7 +157,13 @@ Save the file and [reconfigure GitLab][] for the changes to take effect. **NOTE** You should confirm that the GitLab, registry and the web server user have access to this directory. +## Disable Container Registry + + +## Changelog + + [reconfigure gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation" [wildcard certificate]: "https://en.wikipedia.org/wiki/Wildcard_certificate" [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 -[docker-insecure]: https://github.com/docker/distribution/blob/master/docs/insecure.md +[docker-insecure]: https://docs.docker.com/registry/insecure/ From 0cdc080a4a4d8146f6c874c91b8bff6960214c56 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 20 May 2016 13:18:18 -0500 Subject: [PATCH 407/449] Refactor container docs --- doc/administration/container_registry.md | 181 ++++++++++++++++++----- 1 file changed, 148 insertions(+), 33 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 17a0c229b9..15c3c1a747 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -3,11 +3,18 @@ > **Note:** This feature was [introduced][ce-4040] in GitLab 8.8. +With the Docker container Registry integrated into GitLab, every project can +have its own space for Docker images. + +You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. + +--- + **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [Assumptions](#assumptions) +- [Differences between Omnibus and source installations](#differences-between-omnibus-and-source-installations) - [Container Registry domain configuration](#container-registry-domain-configuration) - [Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) - [Container Registry under its own domain](#container-registry-under-its-own-domain) @@ -17,67 +24,110 @@ This feature was [introduced][ce-4040] in GitLab 8.8. -## Assumptions + +## Differences between Omnibus and source installations If you are using Omnibus, you have to bare in mind the following: - The container Registry will be enabled by default if GitLab is configured with HTTPS and it will listen on port `5005`. If you want the Registry to - listen on a port other than `5005` which is the default, read [#Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) + listen on a port other than `5005`, read [#Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) on how to achieve that. You will also have to configure your firewall to allow connections to that port. -- The Container Registry works under HTTPS by default. Note that using HTTP is - possible but not recommended and out of the scope of this document, +- The container Registry works under HTTPS by default. Using HTTP is possible + but not recommended and out of the scope of this document, [see the insecure Registry documentation][docker-insecure] if you want to implement this. +--- + +If you have installed GitLab from source: +- Omnibus has some things configured for you + +- You will have to install Docker Registry by yourself. You can follow the + [official documentation][registry-deploy]. +- The container Registry will not be enabled by default, you will have to + configure it in `gitlab.yml`. + +The contents of `gitlab.yml` are: + +``` +registry: + enabled: true + host: registry.gitlab.example.com + port: 5005 + api_url: http://localhost:5000/ + key_path: config/registry.key + path: shared/registry + issuer: gitlab-issuer +``` + +where: + +| Parameter | Description | +| --------- | ----------- | +| `enabled` | Enables the Registry in GitLab. By default this is false. | +| `host` | The host URL under which the Registry will run and the users will be able to use. | +| `port` | The port under which the external Registry domain will listen on. | +| `api_url` | The internal API URL under which the Registry is exposed to. It defaults to `http://localhost:5000`. | +| `key_path`| The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation][token-config]. | +| `path` | This should be the same directory like specified in Registry's `rootdirectory`. Read the [storage configuration documentation][storage-config]. | +| `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. | + ## Container Registry domain configuration -There are two ways you can configure the container Registry domain. Either use +There are two ways you can configure the Registry's external domain. Either use the existing GitLab domain where in that case the Registry will listen on a port, or use a completely separate domain. Since the container Registry requires a TLS certificate, in the end it all boils down to how easy or pricey is to get a new TLS certificate. -1. If the Registry is configured to use its own domain, you will need a TLS - certificate for that specific domain (e.g., `registry.example.com`) or maybe - a wildcard certificate if hosted under a subdomain (e.g., `registry.gitlab.example.com`). -1. If the Registry is configured to use the existing GitLab domain, you can - expose the Registry on a port so that you can reuse the existing GitLab TLS - certificate. - Please take this into consideration before configuring the Container Registry for the first time. -Read more about Docker Registry at https://docs.docker.com/registry/introduction/. - ### Container Registry under existing GitLab domain -Lets assume that your GitLab instance is accessible at -`https://gitlab.example.com`. You can expose the Container Registry under -a separate port. +If the Registry is configured to use the existing GitLab domain, you can +expose the Registry on a port so that you can reuse the existing GitLab TLS +certificate. -Lets assume that you've exposed port `4567` in your network firewall. +Assuming that the GitLab domain is `https://gitlab.example.com` and the port the +Registry is exposed to the outside world is `4567`, here is what you need to set +in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed +GitLab from source respectively. **Omnibus GitLab packages** +1. Your `/etc/gitlab/gitlab.rb` should contain the Registry URL as well as the + path to the existing TLS certificate and key used by GitLab. + + ```ruby + registry_external_url 'https://gitlab.example.com:4567' + + ## If your SSL certificate is not in /etc/gitlab/ssl/gitlab.example.com.crt + ## and key not in /etc/gitlab/ssl/gitlab.example.com.key uncomment the lines + ## below + + # registry_nginx['ssl_certificate'] = "/path/to/certificate.pem" + # registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + --- -Your `/etc/gitlab/gitlab.rb` should contain the Container Registry URL as -well as the path to the existing SSL certificate and key used by GitLab. +**Installation from source** -```ruby -registry_external_url 'https://gitlab.example.com:4567' - -## If your SSL certificate is not in /etc/gitlab/ssl/gitlab.example.com.crt -## and key not in /etc/gitlab/ssl/gitlab.example.com.key uncomment the lines -## below - -# registry_nginx['ssl_certificate'] = "/path/to/certificate.pem" -# registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key" ``` - -Save the file and [reconfigure GitLab][] for the changes to take effect. +registry: + enabled: true + host: registry.gitlab.example.com + port: 5005 + api_url: http://localhost:5000/ + key_path: config/registry.key + path: shared/registry + issuer: gitlab-issuer +``` Users should now be able to login to the Container Registry using: @@ -89,7 +139,11 @@ with their GitLab credentials. ### Container Registry under its own domain -Lets assume that you want the Container Registry to be accessible at +If the Registry is configured to use its own domain, you will need a TLS +certificate for that specific domain (e.g., `registry.example.com`) or maybe +a wildcard certificate if hosted under a subdomain (e.g., `registry.gitlab.example.com`). + +Let's assume that you want the container Registry to be accessible at `https://registry.gitlab.example.com`. --- @@ -114,6 +168,17 @@ registry_external_url 'https://registry.gitlab.example.com' Save the file and [reconfigure GitLab][] for the changes to take effect. +``` +registry: + enabled: true + host: registry.gitlab.example.com + port: 5005 + api_url: http://localhost:5000/ + key_path: config/registry.key + path: shared/registry + issuer: gitlab-issuer +``` + Users should now be able to login to the Container Registry using: ```bash @@ -132,6 +197,17 @@ registry_nginx['ssl_certificate'] = "/etc/gitlab/ssl/certificate.pem" registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key" ``` +``` +registry: + enabled: true + host: registry.gitlab.example.com + port: 5005 + api_url: http://localhost:5000/ + key_path: config/registry.key + path: shared/registry + issuer: gitlab-issuer +``` + ## Container Registry storage path It is possible to change path where containers will be stored by the Container @@ -152,6 +228,17 @@ In `/etc/gitlab/gitlab.rb`: gitlab_rails['registry_path'] = "/path/to/registry/storage" ``` +``` +registry: + enabled: true + host: registry.gitlab.example.com + port: 5005 + api_url: http://localhost:5000/ + key_path: config/registry.key + path: shared/registry + issuer: gitlab-issuer +``` + Save the file and [reconfigure GitLab][] for the changes to take effect. **NOTE** You should confirm that the GitLab, registry and the web server user @@ -159,6 +246,31 @@ have access to this directory. ## Disable Container Registry +**Omnibus GitLab** + +``` +# Settings used by GitLab application +# gitlab_rails['registry_enabled'] = true +``` + +``` +# gitlab_rails['registry_host'] = "registry.gitlab.example.com" +# gitlab_rails['registry_api_url'] = "http://localhost:5000" +# gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key" +# gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry" +# gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer" + +# Settings used by Registry application +# registry['enable'] = true +# registry['username'] = "registry" +# registry['group'] = "registry" +# registry['uid'] = nil +# registry['gid'] = nil +# registry['dir'] = "/var/opt/gitlab/registry" +# registry['log_directory'] = "/var/log/gitlab/registry" +# registry['log_level'] = "info" +# registry['rootcertbundle'] = "/var/opt/gitlab/registry/certificate.crt" +``` ## Changelog @@ -167,3 +279,6 @@ have access to this directory. [wildcard certificate]: "https://en.wikipedia.org/wiki/Wildcard_certificate" [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 [docker-insecure]: https://docs.docker.com/registry/insecure/ +[registry-deploy]: https://docs.docker.com/registry/deploying/ +[storage-config]: https://docs.docker.com/registry/configuration/#storage +[token-config]: https://docs.docker.com/registry/configuration/#token From c6cd07ba3a8ef149ff69e0c1520a810b3001b65f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 20 May 2016 13:18:39 -0500 Subject: [PATCH 408/449] Add link to container registry --- doc/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/README.md b/doc/README.md index 8aed060d24..a297ce54f3 100644 --- a/doc/README.md +++ b/doc/README.md @@ -44,6 +44,7 @@ - [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint - [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs - [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability +- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab ## Contributor documentation From 26c768b42cbcb7af47f08254baa305de37828cf0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 20 May 2016 13:18:55 -0500 Subject: [PATCH 409/449] Add proper config values for Registry in gitlab.yml --- config/gitlab.yml.example | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 0cef2794f4..0510e7df59 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -179,11 +179,11 @@ production: &base registry: # enabled: true # host: registry.example.com - # port: 5000 - # api_url: http://localhost:5000/ - # key: config/registry.key - # issuer: omnibus-certificate + # port: 5005 + # api_url: http://localhost:5000/ # internal address to the registry, will be used by GitLab to directly communicate with API + # key_path: config/registry.key # path: shared/registry + # issuer: gitlab-issuer # # 2. GitLab CI settings From c1af75192dd281e23249a5d4eb4126dce5bc5efd Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 20 May 2016 13:19:42 -0500 Subject: [PATCH 410/449] Add NGINX config file for Registry --- lib/support/nginx/registry-ssl | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 lib/support/nginx/registry-ssl diff --git a/lib/support/nginx/registry-ssl b/lib/support/nginx/registry-ssl new file mode 100644 index 0000000000..af607db012 --- /dev/null +++ b/lib/support/nginx/registry-ssl @@ -0,0 +1,51 @@ +## Lines starting with two hashes (##) are comments with information. +## Lines starting with one hash (#) are configuration parameters that can be uncommented. +## +################################### +## configuration ## +################################### + +## Redirects all HTTP traffic to the HTTPS host +server { + listen *:80; + server_name registry.gitlab.example.com; + server_tokens off; ## Don't show the nginx version number, a security best practice + return 301 https://$http_host:$request_uri; + access_log /var/log/nginx/gitlab_registry_access.log gitlab_access; + error_log /var/log/nginx/gitlab_registry_error.log; +} + +server { + listen *:443 ssl http2; + server_name registry.gitlab.example.com; + server_tokens off; ## Don't show the nginx version number, a security best practice + + client_max_body_size 0; + chunked_transfer_encoding on; + + ## Strong SSL Security + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ + ssl on; + ssl_certificate /etc/gitlab/ssl/registry.gitlab.example.com.crt + ssl_certificate_key /etc/gitlab/ssl/registry.gitlab.example.com.key + + ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4'; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_session_timeout 5m; + + access_log /var/log/gitlab/nginx/gitlab_registry_access.log gitlab_access; + error_log /var/log/gitlab/nginx/gitlab_registry_error.log; + + location / { + proxy_set_header Host $http_host; # required for docker client's sake + proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 900; + + proxy_pass http://localhost:5000; + } + +} From e9817f21f4baa20244095f56dd0d45b5c2e9c5dc Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sat, 21 May 2016 20:08:26 -0500 Subject: [PATCH 411/449] Major overhaul of the Registry admin docs --- doc/administration/container_registry.md | 306 ++++++++++++++--------- 1 file changed, 193 insertions(+), 113 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 15c3c1a747..21f086792c 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -3,8 +3,8 @@ > **Note:** This feature was [introduced][ce-4040] in GitLab 8.8. -With the Docker container Registry integrated into GitLab, every project can -have its own space for Docker images. +With the Docker Container Registry integrated into GitLab, every project can +have its own space to store its Docker images. You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. @@ -14,18 +14,22 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [Differences between Omnibus and source installations](#differences-between-omnibus-and-source-installations) +- [How to enable the Container Registry](#how-to-enable-the-container-registry) - [Container Registry domain configuration](#container-registry-domain-configuration) - - [Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) - - [Container Registry under its own domain](#container-registry-under-its-own-domain) + - [Configure Container Registry under an existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain) + - [Configure Container Registry under its own domain](#configure-container-registry-under-its-own-domain) +- [Disable Container Registry site-wide](#disable-container-registry-site-wide) +- [Disable Container Registry per project](#disable-container-registry-per-project) +- [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide) - [Container Registry storage path](#container-registry-storage-path) -- [Disable Container Registry](#disable-container-registry) +- [Storage limitations](#storage-limitations) - [Changelog](#changelog) +## How to enable the Container Registry -## Differences between Omnibus and source installations +**Omnibus GitLab installations** If you are using Omnibus, you have to bare in mind the following: @@ -35,19 +39,21 @@ If you are using Omnibus, you have to bare in mind the following: on how to achieve that. You will also have to configure your firewall to allow connections to that port. - The container Registry works under HTTPS by default. Using HTTP is possible - but not recommended and out of the scope of this document, - [see the insecure Registry documentation][docker-insecure] if you want to + but not recommended and out of the scope of this document. + [See the insecure Registry documentation][docker-insecure] if you want to implement this. --- -If you have installed GitLab from source: -- Omnibus has some things configured for you +**Installations from source** -- You will have to install Docker Registry by yourself. You can follow the - [official documentation][registry-deploy]. -- The container Registry will not be enabled by default, you will have to - configure it in `gitlab.yml`. +If you have installed GitLab from source: + +1. You will have to [install Docker Registry][registry-deploy] by yourself. +1. After the installation is complete, you will have to configure the Registry's + settings `gitlab.yml` in order to enable it. +1. Use the sample NGINX configuration file that is found under + [`lib/support/nginx/registry-ssl`][registry-ssl]. The contents of `gitlab.yml` are: @@ -66,26 +72,31 @@ where: | Parameter | Description | | --------- | ----------- | -| `enabled` | Enables the Registry in GitLab. By default this is false. | +| `enabled` | `true` or `false`. Enables the Registry in GitLab. By default this is `false`. | | `host` | The host URL under which the Registry will run and the users will be able to use. | | `port` | The port under which the external Registry domain will listen on. | | `api_url` | The internal API URL under which the Registry is exposed to. It defaults to `http://localhost:5000`. | | `key_path`| The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation][token-config]. | -| `path` | This should be the same directory like specified in Registry's `rootdirectory`. Read the [storage configuration documentation][storage-config]. | +| `path` | This should be the same directory like specified in Registry's `rootdirectory`. Read the [storage configuration documentation][storage-config]. This path needs to be readable by the GitLab user, the web-server user and the Registry user. Read more in [#container-registry-storage-path](#container-registry-storage-path). | | `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. | +>**Note:** +GitLab does not ship with a Registry init file. Hence, [restarting GitLab][restart gitlab] +will not restart the Registry should you modify its settings. Read the upstream +documentation on how to achieve that. + ## Container Registry domain configuration There are two ways you can configure the Registry's external domain. Either use the existing GitLab domain where in that case the Registry will listen on a port, or use a completely separate domain. Since the container Registry requires a -TLS certificate, in the end it all boils down to how easy or pricey is to -get a new TLS certificate. +TLS certificate, in the end it all boils down to how easy or pricey is to get a +new TLS certificate. Please take this into consideration before configuring the Container Registry for the first time. -### Container Registry under existing GitLab domain +### Configure Container Registry under an existing GitLab domain If the Registry is configured to use the existing GitLab domain, you can expose the Registry on a port so that you can reuse the existing GitLab TLS @@ -96,118 +107,203 @@ Registry is exposed to the outside world is `4567`, here is what you need to set in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed GitLab from source respectively. -**Omnibus GitLab packages** +--- + +**Omnibus GitLab installations** + +>**Note:** +If you are using HTTPS in your Omnibus packages, then the Registry will be +enabled by default and exposed under port `5005`. Follow the steps below only if +you want to change the default port. 1. Your `/etc/gitlab/gitlab.rb` should contain the Registry URL as well as the - path to the existing TLS certificate and key used by GitLab. + path to the existing TLS certificate and key used by GitLab: ```ruby + gitlab_rails['registry_port'] = "4567" + gitlab_rails['registry_host'] = "gitlab.example.com" + + # The following setting is needed for NGINX registry_external_url 'https://gitlab.example.com:4567' + ``` - ## If your SSL certificate is not in /etc/gitlab/ssl/gitlab.example.com.crt - ## and key not in /etc/gitlab/ssl/gitlab.example.com.key uncomment the lines - ## below + If your TLS certificate is not in `/etc/gitlab/ssl/gitlab.example.com.crt` + and key not in `/etc/gitlab/ssl/gitlab.example.com.key` uncomment the lines + below: - # registry_nginx['ssl_certificate'] = "/path/to/certificate.pem" - # registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key" + ```ruby + registry_nginx['ssl_certificate'] = "/path/to/certificate.pem" + registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key" ``` 1. Save the file and [reconfigure GitLab][] for the changes to take effect. --- -**Installation from source** +**Installations from source** -``` -registry: - enabled: true - host: registry.gitlab.example.com - port: 5005 - api_url: http://localhost:5000/ - key_path: config/registry.key - path: shared/registry - issuer: gitlab-issuer -``` +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + configure it with the following settings: -Users should now be able to login to the Container Registry using: + ``` + registry: + enabled: true + host: gitlab.example.com + port: 4567 + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. +1. Make the relevant changes in NGINX as well (domain, port, TLS certificates path). + +--- + +Users should now be able to login to the Container Registry with their GitLab +credentials using: ```bash docker login gitlab.example.com:4567 ``` -with their GitLab credentials. - -### Container Registry under its own domain +### Configure Container Registry under its own domain If the Registry is configured to use its own domain, you will need a TLS certificate for that specific domain (e.g., `registry.example.com`) or maybe -a wildcard certificate if hosted under a subdomain (e.g., `registry.gitlab.example.com`). +a wildcard certificate if hosted under a subdomain of your existing GitLab +domain (e.g., `registry.gitlab.example.com`). Let's assume that you want the container Registry to be accessible at `https://registry.gitlab.example.com`. --- -**Omnibus GitLab packages** +**Omnibus GitLab installations** -Place your SSL certificate and key in -`/etc/gitlab/ssl/registry.gitlab.example.com.crt` -and -`/etc/gitlab/ssl/registry.gitlab.example.com.key` and make sure they have -correct permissions: +1. Place your TLS certificate and key in + `/etc/gitlab/ssl/registry.gitlab.example.com.crt` and + `/etc/gitlab/ssl/registry.gitlab.example.com.key` and make sure they have + correct permissions: -```bash -chmod 600 /etc/gitlab/ssl/registry.gitlab.example.com.* -``` + ```bash + chmod 600 /etc/gitlab/ssl/registry.gitlab.example.com.* + ``` -Once the SSL certificate is in place, edit `/etc/gitlab/gitlab.rb` with: +1. Once the TLS certificate is in place, edit `/etc/gitlab/gitlab.rb` with: -```ruby -registry_external_url 'https://registry.gitlab.example.com' -``` + ```ruby + registry_external_url 'https://registry.gitlab.example.com' + ``` -Save the file and [reconfigure GitLab][] for the changes to take effect. - -``` -registry: - enabled: true - host: registry.gitlab.example.com - port: 5005 - api_url: http://localhost:5000/ - key_path: config/registry.key - path: shared/registry - issuer: gitlab-issuer -``` - -Users should now be able to login to the Container Registry using: - -```bash -docker login registry.gitlab.example.com -``` - -with their GitLab credentials. +1. Save the file and [reconfigure GitLab][] for the changes to take effect. +> **Note:** If you have a [wildcard certificate][], you need to specify the path to the certificate in addition to the URL, in this case `/etc/gitlab/gitlab.rb` will look like: - +> ```ruby registry_external_url 'https://registry.gitlab.example.com' registry_nginx['ssl_certificate'] = "/etc/gitlab/ssl/certificate.pem" registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key" ``` +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + configure it with the following settings: + + ``` + registry: + enabled: true + host: registry.gitlab.example.com + port: 4567 + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. +1. Make the relevant changes in NGINX as well (domain, port, TLS certificates path). + +--- + +Users should now be able to login to the Container Registry using their GitLab +credentials: + +```bash +docker login registry.gitlab.example.com ``` -registry: - enabled: true - host: registry.gitlab.example.com - port: 5005 - api_url: http://localhost:5000/ - key_path: config/registry.key - path: shared/registry - issuer: gitlab-issuer + +## Disable Container Registry site-wide + +**Omnibus GitLab** + +```ruby +gitlab_rails['registry_enabled'] = true ``` +``` +# gitlab_rails['registry_port'] = "5005" +# gitlab_rails['registry_host'] = "registry.gitlab.example.com" +# gitlab_rails['registry_api_url'] = "http://localhost:5000" +# gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key" +# gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry" +# gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer" + +# Settings used by Registry application +# registry['enable'] = true +# registry['username'] = "registry" +# registry['group'] = "registry" +# registry['uid'] = nil +# registry['gid'] = nil +# registry['dir'] = "/var/opt/gitlab/registry" +# registry['log_directory'] = "/var/log/gitlab/registry" +# registry['log_level'] = "info" +# registry['rootcertbundle'] = "/var/opt/gitlab/registry/certificate.crt" +``` + +## Disable Container Registry per project + +If Registry is enabled in your GitLab instance, but you don't need it for your +project, you can disable it from your project's settings. Read the user guide +on how to achieve that. + +## Disable Container Registry for new projects site-wide + +The Registry is enabled by default on all new projects. To disable this function +and let the owners of a project to enable Registry by themselves, follow the +steps below. + +--- + +**Omnibus GitLab installations** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['gitlab_default_projects_features_container_registry'] = false + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `default_projects_features` + entry and configure it so that `container_registry` is set to `false`: + + ``` + ## Default project features settings + default_projects_features: + issues: true + merge_requests: true + wiki: true + snippets: false + builds: true + container_registry: false + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + ## Container Registry storage path It is possible to change path where containers will be stored by the Container @@ -244,41 +340,25 @@ Save the file and [reconfigure GitLab][] for the changes to take effect. **NOTE** You should confirm that the GitLab, registry and the web server user have access to this directory. -## Disable Container Registry +## Storage limitations -**Omnibus GitLab** - -``` -# Settings used by GitLab application -# gitlab_rails['registry_enabled'] = true -``` - -``` -# gitlab_rails['registry_host'] = "registry.gitlab.example.com" -# gitlab_rails['registry_api_url'] = "http://localhost:5000" -# gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key" -# gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry" -# gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer" - -# Settings used by Registry application -# registry['enable'] = true -# registry['username'] = "registry" -# registry['group'] = "registry" -# registry['uid'] = nil -# registry['gid'] = nil -# registry['dir'] = "/var/opt/gitlab/registry" -# registry['log_directory'] = "/var/log/gitlab/registry" -# registry['log_level'] = "info" -# registry['rootcertbundle'] = "/var/opt/gitlab/registry/certificate.crt" -``` +Currently, there is no storage limitation, which means a user can upload an +infinite amount of Docker images with arbitrary sizes. This setting will be +configurable in future releases. ## Changelog +**GitLab 8.8 ([source docs][8-8-docs])** -[reconfigure gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation" -[wildcard certificate]: "https://en.wikipedia.org/wiki/Wildcard_certificate" +- GitLab Container Registry feature was introduced. + +[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure +[restart gitlab]: restart_gitlab.md#installations-from-source +[wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 [docker-insecure]: https://docs.docker.com/registry/insecure/ [registry-deploy]: https://docs.docker.com/registry/deploying/ [storage-config]: https://docs.docker.com/registry/configuration/#storage [token-config]: https://docs.docker.com/registry/configuration/#token +[8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md +[registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl From 27067e565fb5c425b307a8e28c5dcab95fa8b0bf Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sat, 21 May 2016 20:15:42 -0500 Subject: [PATCH 412/449] Clarify how the Registry is enabled in Omnibus --- doc/administration/container_registry.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 21f086792c..435e7f627b 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -34,14 +34,17 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr If you are using Omnibus, you have to bare in mind the following: - The container Registry will be enabled by default if GitLab is configured - with HTTPS and it will listen on port `5005`. If you want the Registry to - listen on a port other than `5005`, read [#Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) + with HTTPS in new and existing installations (no action is required from the + administrator), and it will listen on port `5005`. If you wish to change it, + read [#Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) on how to achieve that. You will also have to configure your firewall to allow - connections to that port. -- The container Registry works under HTTPS by default. Using HTTP is possible - but not recommended and out of the scope of this document. - [See the insecure Registry documentation][docker-insecure] if you want to - implement this. + incoming connections to that port. + +>**Note:** +The container Registry works under HTTPS by default. Using HTTP is possible +but not recommended and out of the scope of this document. +Read the [insecure Registry documentation][docker-insecure] if you want to +implement this. --- From 13bf612600f126faddb4de5e8376a27a466ff830 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 22 May 2016 22:40:43 +0200 Subject: [PATCH 413/449] Test migration helpers using a migration class --- spec/lib/gitlab/database/migration_helpers_spec.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index ec43165bb5..642954f8d2 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -2,15 +2,13 @@ require 'spec_helper' describe Gitlab::Database::MigrationHelpers, lib: true do let(:model) do - Class.new do - include Gitlab::Database::MigrationHelpers - - def method_missing(name, *args, &block) - ActiveRecord::Base.connection.send(name, *args, &block) - end - end.new + ActiveRecord::Migration.new.extend( + Gitlab::Database::MigrationHelpers + ) end + before { allow(model).to receive(:puts) } + describe '#add_concurrent_index' do context 'outside a transaction' do before do From 9e92b0eead3c2b6dabf377f119cd1f91fd645a42 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 22 May 2016 22:41:14 +0200 Subject: [PATCH 414/449] Add specs for migration helpers and boolean values --- spec/lib/gitlab/database/migration_helpers_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 642954f8d2..35ade7a2be 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -58,6 +58,12 @@ describe Gitlab::Database::MigrationHelpers, lib: true do expect(Project.where(import_error: 'foo').count).to eq(5) end + + it 'updates boolean values correctly' do + model.update_column_in_batches(:projects, :archived, true) + + expect(Project.where(archived: true).count).to eq(5) + end end describe '#add_column_with_default' do From 2df8b48dbbe57f64d414e7c3395f9d34b01271b0 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 22 May 2016 22:44:59 +0200 Subject: [PATCH 415/449] Add MySQL compatibility fix in migration helpers --- lib/gitlab/database/migration_helpers.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 9b662d163f..1fe6c1143d 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -39,7 +39,8 @@ module Gitlab def update_column_in_batches(table, column, value) quoted_table = quote_table_name(table) quoted_column = quote_column_name(column) - quoted_value = quote(value) + # workaround for #17711 + quoted_value = connection.quote(value) processed = 0 total = exec_query("SELECT COUNT(*) AS count FROM #{quoted_table}"). From 7f9092da1e349c51af9febaa4db4995c851ef2d2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 22 May 2016 23:31:33 +0200 Subject: [PATCH 416/449] Extend comment for migrations helper MySQL fix --- lib/gitlab/database/migration_helpers.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 1fe6c1143d..fd14234c55 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -39,7 +39,14 @@ module Gitlab def update_column_in_batches(table, column, value) quoted_table = quote_table_name(table) quoted_column = quote_column_name(column) - # workaround for #17711 + + ## + # Workaround for #17711 + # + # It looks like for MySQL `ActiveRecord::Base.conntection.quote(true)` + # returns correct value (1), but `ActiveRecord::Migration.new.quote` + # returns incorrect value ('true'), which causes migrations to fail. + # quoted_value = connection.quote(value) processed = 0 From 891888aa05bc9a0a156ea098205eb70150d53732 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 22 May 2016 23:33:08 +0200 Subject: [PATCH 417/449] Add Changelog entry for MySQL compatibility fix --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 01585ede58..13b937b8c4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. +v 8.8.1 (unreleased) + - Fix MySQL compatibility in zero downtime migrations helpers + v 8.8.0 (unreleased) - Implement GFM references for milestones (Alejandro Rodríguez) - Snippets tab under user profile. !4001 (Long Nguyen) From e955f49fb7d8edeefc0d2647539276acf34d5731 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 22 May 2016 17:23:30 -0500 Subject: [PATCH 418/449] Fix Registry docs now that it is not enabled be default --- doc/administration/container_registry.md | 43 +++++++++++++++--------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 435e7f627b..a3df14019f 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -14,7 +14,7 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [How to enable the Container Registry](#how-to-enable-the-container-registry) +- [Enable the Container Registry](#enable-the-container-registry) - [Container Registry domain configuration](#container-registry-domain-configuration) - [Configure Container Registry under an existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain) - [Configure Container Registry under its own domain](#configure-container-registry-under-its-own-domain) @@ -27,18 +27,19 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr -## How to enable the Container Registry +## Enable the Container Registry **Omnibus GitLab installations** -If you are using Omnibus, you have to bare in mind the following: +1. Open `/etc/gitlab/gitlab.rb` and edit or add the following line: -- The container Registry will be enabled by default if GitLab is configured - with HTTPS in new and existing installations (no action is required from the - administrator), and it will listen on port `5005`. If you wish to change it, - read [#Container Registry under existing GitLab domain](#container-registry-under-existing-gitlab-domain) - on how to achieve that. You will also have to configure your firewall to allow - incoming connections to that port. + ```ruby + gitlab_rails['registry_enabled'] = true + ``` + +1. The next step is to configure the domain name under which the Container + Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration) + and pick one of the two options that fits your case. >**Note:** The container Registry works under HTTPS by default. Using HTTP is possible @@ -54,9 +55,10 @@ If you have installed GitLab from source: 1. You will have to [install Docker Registry][registry-deploy] by yourself. 1. After the installation is complete, you will have to configure the Registry's - settings `gitlab.yml` in order to enable it. + settings in `gitlab.yml` in order to enable it. 1. Use the sample NGINX configuration file that is found under - [`lib/support/nginx/registry-ssl`][registry-ssl]. + [`lib/support/nginx/registry-ssl`][registry-ssl] and edit it to match the + `host`, `port` and TLS certs paths. The contents of `gitlab.yml` are: @@ -90,11 +92,15 @@ documentation on how to achieve that. ## Container Registry domain configuration -There are two ways you can configure the Registry's external domain. Either use -the existing GitLab domain where in that case the Registry will listen on a port, -or use a completely separate domain. Since the container Registry requires a -TLS certificate, in the end it all boils down to how easy or pricey is to get a -new TLS certificate. +There are two ways you can configure the Registry's external domain. + +- Either [use the existing GitLab domain][existing-domain] where in that case + the Registry will have to listen on a port and reuse GitLab's TLS certificate, +- or [use a completely separate domain][new-domain] with a new TLS certificate + for that domain. + +Since the container Registry requires a TLS certificate, in the end it all boils +down to how easy or pricey is to get a new one. Please take this into consideration before configuring the Container Registry for the first time. @@ -193,6 +199,9 @@ Let's assume that you want the container Registry to be accessible at 1. Once the TLS certificate is in place, edit `/etc/gitlab/gitlab.rb` with: ```ruby + gitlab_rails['registry_host'] = "registry.gitlab.example.com" + + # The following setting is needed for NGINX registry_external_url 'https://registry.gitlab.example.com' ``` @@ -365,3 +374,5 @@ configurable in future releases. [token-config]: https://docs.docker.com/registry/configuration/#token [8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md [registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl +[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain +[new-domain]: #configure-container-registry-under-its-own-domain From 646018a40e7d29682f31e774f453a6b3427b4216 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 14:14:12 -0500 Subject: [PATCH 419/449] Fix the CI login to Container Registry (the gitlab-ci-token user) --- CHANGELOG | 1 + app/controllers/jwt_controller.rb | 2 +- .../auth/container_registry_authentication_service.rb | 2 +- spec/requests/jwt_controller_spec.rb | 2 +- .../auth/container_registry_authentication_service_spec.rb | 4 ++-- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 13b937b8c4..434c7b554f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,7 @@ v 8.8.0 (unreleased) - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. - Support e-mail notifications for comments on project snippets + - Fix the CI login to Container Registry (the gitlab-ci-token user) - Fix API leak of notes of unauthorized issues, snippets and merge requests - Use ActionDispatch Remote IP for Akismet checking - Fix error when visiting commit builds page before build was updated diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index f5aa5397ff..156ab2811d 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -36,7 +36,7 @@ class JwtController < ApplicationController end def authenticate_project(login, password) - if login == 'gitlab_ci_token' + if login == 'gitlab-ci-token' Project.find_by(builds_enabled: true, runners_token: password) end end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index f807b8ec09..2bbab643e6 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -6,7 +6,7 @@ module Auth return error('not found', 404) unless registry.enabled if params[:offline_token] - return error('unauthorized', 401) unless current_user + return error('unauthorized', 401) unless current_user || project else return error('forbidden', 403) unless scope end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 7bb71365a4..d006ff195c 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -23,7 +23,7 @@ describe JwtController do context 'when using authorized request' do context 'using CI token' do let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } - let(:headers) { { authorization: credentials('gitlab_ci_token', project.runners_token) } } + let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } } subject! { get '/jwt/auth', parameters, headers } diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 73b8c3f048..3f4a1ced2b 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -127,12 +127,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'project authorization' do let(:current_project) { create(:empty_project) } - context 'disallow to use offline_token' do + context 'allow to use offline_token' do let(:current_params) do { offline_token: true } end - it_behaves_like 'an unauthorized' + it_behaves_like 'an authenticated' end context 'allow to pull and push images' do From edfb20f2bc067d72887caccb15b8cd323827c897 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 23 May 2016 00:09:42 +0200 Subject: [PATCH 420/449] Move registry fix Changelog entry to valid version --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 434c7b554f..ef4d72a9e9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.1 (unreleased) - Fix MySQL compatibility in zero downtime migrations helpers + - Fix the CI login to Container Registry (the gitlab-ci-token user) v 8.8.0 (unreleased) - Implement GFM references for milestones (Alejandro Rodríguez) @@ -32,7 +33,6 @@ v 8.8.0 (unreleased) - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. - Support e-mail notifications for comments on project snippets - - Fix the CI login to Container Registry (the gitlab-ci-token user) - Fix API leak of notes of unauthorized issues, snippets and merge requests - Use ActionDispatch Remote IP for Akismet checking - Fix error when visiting commit builds page before build was updated From cd26cfbc27218b734a1a78836084f2205a18c580 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 17:58:11 -0500 Subject: [PATCH 421/449] Allow anonymous user to access pipelines --- CHANGELOG | 1 + app/helpers/projects_helper.rb | 4 ++++ app/models/ability.rb | 1 + app/views/layouts/nav/_project.html.haml | 3 ++- spec/features/pipelines_spec.rb | 6 ++++++ 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index ef4d72a9e9..950002df8f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,6 +25,7 @@ v 8.8.0 (unreleased) - Updated gitlab_git to 10.1.0 - GitAccess#protected_tag? no longer loads all tags just to check if a single one exists - Reduce delay in destroying a project from 1-minute to immediately + - Fix access to Pipelines by Anonymous user - Make build status canceled if any of the jobs was canceled and none failed - Upgrade Sidekiq to 4.1.2 - Added /health_check endpoint for checking service status diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 0825b5b643..5e5d170a9f 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -144,6 +144,10 @@ module ProjectsHelper nav_tabs << :merge_requests end + if can?(current_user, :read_pipeline, project) + nav_tabs << :pipelines + end + if can?(current_user, :read_build, project) nav_tabs << :builds end diff --git a/app/models/ability.rb b/app/models/ability.rb index f7ea2fd2b1..b354b1990c 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -60,6 +60,7 @@ class Ability :read_project_member, :read_merge_request, :read_note, + :read_pipeline, :read_commit_status, :read_container_image, :download_code diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a97fefcfb4..6dff488eda 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -38,7 +38,7 @@ %span Commits - - if project_nav_tab? :builds + - if project_nav_tab? :pipelines = nav_link(controller: :pipelines) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do = icon('ship fw') @@ -46,6 +46,7 @@ Pipelines %span.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) + - if project_nav_tab? :builds = nav_link(controller: %w(builds)) do = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do = icon('cubes fw') diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 32665aadd2..1d6f4485c8 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -24,6 +24,12 @@ describe "Pipelines" do end end + context 'anonymous access' do + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_http_status(:success) } + end + context 'cancelable pipeline' do let!(:running) { create(:ci_build, :running, commit: pipeline, stage: 'test', commands: 'test') } From 11fa89dfa548832cda0f3dafbc902c9193ed3177 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 23 May 2016 00:14:23 +0200 Subject: [PATCH 422/449] Move Changelog for pipeline fix to valid version --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 950002df8f..88e7cfaf96 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.1 (unreleased) - Fix MySQL compatibility in zero downtime migrations helpers - Fix the CI login to Container Registry (the gitlab-ci-token user) + - Fix access to Pipelines by Anonymous user v 8.8.0 (unreleased) - Implement GFM references for milestones (Alejandro Rodríguez) @@ -25,7 +26,6 @@ v 8.8.0 (unreleased) - Updated gitlab_git to 10.1.0 - GitAccess#protected_tag? no longer loads all tags just to check if a single one exists - Reduce delay in destroying a project from 1-minute to immediately - - Fix access to Pipelines by Anonymous user - Make build status canceled if any of the jobs was canceled and none failed - Upgrade Sidekiq to 4.1.2 - Added /health_check endpoint for checking service status From b565cfa50fa53bede268904d318547e593931f0b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 22 May 2016 18:52:42 -0500 Subject: [PATCH 423/449] Fix remaining docs on enabling/disabling Registry --- doc/administration/container_registry.md | 65 ++++++++++++------------ 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index a3df14019f..7d7d5fd1e8 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -120,22 +120,20 @@ GitLab from source respectively. **Omnibus GitLab installations** ->**Note:** -If you are using HTTPS in your Omnibus packages, then the Registry will be -enabled by default and exposed under port `5005`. Follow the steps below only if -you want to change the default port. - 1. Your `/etc/gitlab/gitlab.rb` should contain the Registry URL as well as the path to the existing TLS certificate and key used by GitLab: ```ruby - gitlab_rails['registry_port'] = "4567" gitlab_rails['registry_host'] = "gitlab.example.com" + gitlab_rails['registry_port'] = "4567" # The following setting is needed for NGINX registry_external_url 'https://gitlab.example.com:4567' ``` + Note how the `registry_external_url` is listening on HTTPS and is a + conjunction of `registry_host` and `registry_port`. + If your TLS certificate is not in `/etc/gitlab/ssl/gitlab.example.com.crt` and key not in `/etc/gitlab/ssl/gitlab.example.com.key` uncomment the lines below: @@ -213,7 +211,6 @@ certificate in addition to the URL, in this case `/etc/gitlab/gitlab.rb` will look like: > ```ruby -registry_external_url 'https://registry.gitlab.example.com' registry_nginx['ssl_certificate'] = "/etc/gitlab/ssl/certificate.pem" registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key" ``` @@ -229,7 +226,6 @@ registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key" registry: enabled: true host: registry.gitlab.example.com - port: 4567 ``` 1. Save the file and [restart GitLab][] for the changes to take effect. @@ -246,31 +242,36 @@ docker login registry.gitlab.example.com ## Disable Container Registry site-wide +>**Note:** +Disabling the Registry in the Rails GitLab application as set by the following +steps, will not remove any existing Docker images. This is handled by the +Registry application itself. + **Omnibus GitLab** -```ruby -gitlab_rails['registry_enabled'] = true -``` +1. Open `/etc/gitlab/gitlab.rb` and set `gitlab_rails['registry_enabled']` to + `false` and comment out the `registry_external_url`: -``` -# gitlab_rails['registry_port'] = "5005" -# gitlab_rails['registry_host'] = "registry.gitlab.example.com" -# gitlab_rails['registry_api_url'] = "http://localhost:5000" -# gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key" -# gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry" -# gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer" + ```ruby + gitlab_rails['registry_enabled'] = false + # registry_external_url 'https://registry.gitlab.example.com' + ``` -# Settings used by Registry application -# registry['enable'] = true -# registry['username'] = "registry" -# registry['group'] = "registry" -# registry['uid'] = nil -# registry['gid'] = nil -# registry['dir'] = "/var/opt/gitlab/registry" -# registry['log_directory'] = "/var/log/gitlab/registry" -# registry['log_level'] = "info" -# registry['rootcertbundle'] = "/var/opt/gitlab/registry/certificate.crt" -``` +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + set `enabled` to `false`: + + ``` + registry: + enabled: false + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. ## Disable Container Registry per project @@ -280,9 +281,9 @@ on how to achieve that. ## Disable Container Registry for new projects site-wide -The Registry is enabled by default on all new projects. To disable this function -and let the owners of a project to enable Registry by themselves, follow the -steps below. +If the Container Registry is enabled, then it will be available on all new +projects. To disable this function and let the owners of a project to enable +the Container Registry by themselves, follow the steps below. --- From f55f62853162af86989a8343c6692c956f7ccbb0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 22 May 2016 19:20:28 -0500 Subject: [PATCH 424/449] Add note about port needed in NGINX config --- lib/support/nginx/registry-ssl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/support/nginx/registry-ssl b/lib/support/nginx/registry-ssl index af607db012..92511e2686 100644 --- a/lib/support/nginx/registry-ssl +++ b/lib/support/nginx/registry-ssl @@ -16,6 +16,8 @@ server { } server { + # If a different port is specified in https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/config/gitlab.yml.example#L182, + # it should be declared here as well listen *:443 ssl http2; server_name registry.gitlab.example.com; server_tokens off; ## Don't show the nginx version number, a security best practice From f896a4e0c1cb66392e069556c1c298915fc55d0e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 22 May 2016 19:31:19 -0500 Subject: [PATCH 425/449] Refactor storage location section --- doc/administration/container_registry.md | 58 ++++++++++++++---------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 7d7d5fd1e8..e9d850ca68 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -319,39 +319,49 @@ the Container Registry by themselves, follow the steps below. ## Container Registry storage path -It is possible to change path where containers will be stored by the Container -Registry. +To change the storage path where Docker images will be stored, follow the +steps below. -**Omnibus GitLab packages** +This path is accessible to: + +- the user running the Container Registry daemon, +- the user running GitLab +- and to the user running the Nginx web server. + +> **Warning** You should confirm that all GitLab, Registry and web server users +have access to this directory. --- -By default, the path Container Registry is using to store the containers is in -`/var/opt/gitlab/gitlab-rails/shared/registry`. -This path is accessible to the user running the Container Registry daemon, -user running GitLab and to the user running Nginx web server. +**Omnibus GitLab installations** -In `/etc/gitlab/gitlab.rb`: +The default location where images are stored in Omnibus, is +`/var/opt/gitlab/gitlab-rails/shared/registry`. To change it: -```ruby -gitlab_rails['registry_path'] = "/path/to/registry/storage" -``` +1. Edit `/etc/gitlab/gitlab.rb`: -``` -registry: - enabled: true - host: registry.gitlab.example.com - port: 5005 - api_url: http://localhost:5000/ - key_path: config/registry.key - path: shared/registry - issuer: gitlab-issuer -``` + ```ruby + gitlab_rails['registry_path'] = "/path/to/registry/storage" + ``` -Save the file and [reconfigure GitLab][] for the changes to take effect. +1. Save the file and [reconfigure GitLab][] for the changes to take effect. -**NOTE** You should confirm that the GitLab, registry and the web server user -have access to this directory. +--- + +**Installations from source** + +The default location where images are stored in source installations, is +`/home/git/gitlab/shared/registry`. To change it: + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + change the `path` setting: + + ``` + registry: + path: shared/registry + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. ## Storage limitations From c01f99b768d0209c67e73b16751cacede2d8d6ad Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 22 May 2016 19:53:19 -0500 Subject: [PATCH 426/449] Fixes suggested by Kamil --- doc/administration/container_registry.md | 32 +++++++----------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index e9d850ca68..caf9a5bef2 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -31,15 +31,9 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr **Omnibus GitLab installations** -1. Open `/etc/gitlab/gitlab.rb` and edit or add the following line: - - ```ruby - gitlab_rails['registry_enabled'] = true - ``` - -1. The next step is to configure the domain name under which the Container - Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration) - and pick one of the two options that fits your case. +All you have to do is configure the domain name under which the Container +Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration) +and pick one of the two options that fits your case. >**Note:** The container Registry works under HTTPS by default. Using HTTP is possible @@ -124,15 +118,11 @@ GitLab from source respectively. path to the existing TLS certificate and key used by GitLab: ```ruby - gitlab_rails['registry_host'] = "gitlab.example.com" - gitlab_rails['registry_port'] = "4567" - - # The following setting is needed for NGINX registry_external_url 'https://gitlab.example.com:4567' ``` - Note how the `registry_external_url` is listening on HTTPS and is a - conjunction of `registry_host` and `registry_port`. + Note how the `registry_external_url` is listening on HTTPS under the + existing GitLab URL, but on a different port. If your TLS certificate is not in `/etc/gitlab/ssl/gitlab.example.com.crt` and key not in `/etc/gitlab/ssl/gitlab.example.com.key` uncomment the lines @@ -197,12 +187,11 @@ Let's assume that you want the container Registry to be accessible at 1. Once the TLS certificate is in place, edit `/etc/gitlab/gitlab.rb` with: ```ruby - gitlab_rails['registry_host'] = "registry.gitlab.example.com" - - # The following setting is needed for NGINX registry_external_url 'https://registry.gitlab.example.com' ``` + Note how the `registry_external_url` is listening on HTTPS. + 1. Save the file and [reconfigure GitLab][] for the changes to take effect. > **Note:** @@ -249,12 +238,10 @@ Registry application itself. **Omnibus GitLab** -1. Open `/etc/gitlab/gitlab.rb` and set `gitlab_rails['registry_enabled']` to - `false` and comment out the `registry_external_url`: +1. Open `/etc/gitlab/gitlab.rb` and set `registry['enable']` to `false`: ```ruby - gitlab_rails['registry_enabled'] = false - # registry_external_url 'https://registry.gitlab.example.com' + registry['enable'] = false ``` 1. Save the file and [reconfigure GitLab][] for the changes to take effect. @@ -326,7 +313,6 @@ This path is accessible to: - the user running the Container Registry daemon, - the user running GitLab -- and to the user running the Nginx web server. > **Warning** You should confirm that all GitLab, Registry and web server users have access to this directory. From 2bf818c4d0e75224f832912642fe319e0bea670a Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sun, 22 May 2016 19:38:22 -0600 Subject: [PATCH 427/449] Fix some specs which would have broken in 2017 to not break anymore by using a dynamic year value. --- .../files/project_owner_creates_license_file_spec.rb | 8 ++++---- ...k_to_create_license_file_in_empty_project_spec.rb | 4 ++-- spec/requests/api/licenses_spec.rb | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 3d6ffbc4c6..ecc818eb1e 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -25,7 +25,7 @@ feature 'project owner creates a license file', feature: true, js: true do file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') - expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true click_button 'Commit Changes' @@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) expect(page).to have_content('The MIT License (MIT)') - expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end scenario 'project master creates a license file from the "Add license" link' do @@ -48,7 +48,7 @@ feature 'project owner creates a license file', feature: true, js: true do file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') - expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true click_button 'Commit Changes' @@ -56,6 +56,6 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) expect(page).to have_content('The MIT License (MIT)') - expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 3268e24020..34eda29c28 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -24,7 +24,7 @@ feature 'project owner sees a link to create a license file in empty project', f file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') - expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true # Remove pre-receive hook so we can push without auth @@ -34,6 +34,6 @@ feature 'project owner sees a link to create a license file in empty project', f expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) expect(page).to have_content('The MIT License (MIT)') - expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end end diff --git a/spec/requests/api/licenses_spec.rb b/spec/requests/api/licenses_spec.rb index c17dcb222a..3726b2f568 100644 --- a/spec/requests/api/licenses_spec.rb +++ b/spec/requests/api/licenses_spec.rb @@ -57,7 +57,7 @@ describe API::Licenses, api: true do end it 'replaces placeholder values' do - expect(json_response['content']).to include('Copyright (c) 2016 Anton') + expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton") end end @@ -70,7 +70,7 @@ describe API::Licenses, api: true do it 'replaces placeholder values' do expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include('Copyright (C) 2016 Anton') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") end end @@ -83,7 +83,7 @@ describe API::Licenses, api: true do it 'replaces placeholder values' do expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include('Copyright (C) 2016 Anton') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") end end @@ -96,7 +96,7 @@ describe API::Licenses, api: true do it 'replaces placeholder values' do expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include('Copyright (C) 2016 Anton') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") end end @@ -108,7 +108,7 @@ describe API::Licenses, api: true do end it 'replaces placeholder values' do - expect(json_response['content']).to include('Copyright 2016 Anton') + expect(json_response['content']).to include("Copyright #{Time.now.year} Anton") end end @@ -128,7 +128,7 @@ describe API::Licenses, api: true do it 'replaces the copyright owner placeholder with the name of the current user' do get api('/licenses/mit', user) - expect(json_response['content']).to include("Copyright (c) 2016 #{user.name}") + expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}") end end end From f23facabe47d0f002ce49ec39593d9a585c68268 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 22 May 2016 23:38:48 -0400 Subject: [PATCH 428/449] Update CHANGELOG for 8.8.1 [ci skip] --- CHANGELOG | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 88e7cfaf96..8f4ec3e271 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,13 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.8.1 (unreleased) +v 8.8.1 + - Add documentation for the "Health Check" feature + - Allow anonymous users to access a public project's pipelines - Fix MySQL compatibility in zero downtime migrations helpers - Fix the CI login to Container Registry (the gitlab-ci-token user) - Fix access to Pipelines by Anonymous user -v 8.8.0 (unreleased) +v 8.8.0 - Implement GFM references for milestones (Alejandro Rodríguez) - Snippets tab under user profile. !4001 (Long Nguyen) - Fix error when using link to uploads in global snippets From c135a69a877c6b3336dc4d25c2bfc2715abbe838 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 22 May 2016 23:54:29 -0400 Subject: [PATCH 429/449] Update CHANGELOG for 8.8.1. Again. [ci skip] --- CHANGELOG | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8f4ec3e271..e5cdcc2ecc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,7 +5,6 @@ v 8.8.1 - Allow anonymous users to access a public project's pipelines - Fix MySQL compatibility in zero downtime migrations helpers - Fix the CI login to Container Registry (the gitlab-ci-token user) - - Fix access to Pipelines by Anonymous user v 8.8.0 - Implement GFM references for milestones (Alejandro Rodríguez) From 8ed9b6aa7a31dad147e050d3fe992fbf814712fb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 22 May 2016 23:12:32 -0500 Subject: [PATCH 430/449] Initial version of user documentation of container registry --- doc/README.md | 1 + doc/container_registry/README.md | 84 ++++++++++++++++++ doc/container_registry/container_registry.png | Bin 0 -> 354050 bytes doc/container_registry/project_feature.png | Bin 0 -> 392842 bytes 4 files changed, 85 insertions(+) create mode 100644 doc/container_registry/README.md create mode 100644 doc/container_registry/container_registry.png create mode 100644 doc/container_registry/project_feature.png diff --git a/doc/README.md b/doc/README.md index a297ce54f3..8a6adee2fb 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,6 +13,7 @@ - [Profile Settings](profile/README.md) - [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. +- [Container Registry](container-registry/README.md) Learn how to use GitLab Container Registry. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md new file mode 100644 index 0000000000..5af47f7120 --- /dev/null +++ b/doc/container_registry/README.md @@ -0,0 +1,84 @@ +# GitLab Container Registry + +> **Note:** +This feature was [introduced][ce-4040] in GitLab 8.8. + +With the Docker Container Registry integrated into GitLab, every project can +have its own space to store its Docker images. + +You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. + +You can read more about administering GitLab Container Registry on [GitLab Container Registry Administration](../administration/container_registry.md) + +--- + +## Start using Container Registry + +1. First ask your system administrator to enable GitLab Container Registry following the [administration documentation](../administration/container_registry.md). + +2. Go to project settings and enable `Container Registry` feature on your project: + +![](project_feature.png) + +3. Login to Container Registry with your credentials: + +``` +docker login registry.example.com +``` + +## Build and push images + +Your registry is accessible under address configured via `registry_external_url`. +To start using it you need to first build and publish images: + +``` +docker build -t registry.example.com/group/project . +docker push registry.example.com/group/project +``` + +## Use images from GitLab Container Registry + +To download and run container from images hosted in GitLab Container Registry use `docker run`: + +``` +docker run [options] registry.example.com/group/project [arguments] +``` + +## Control Container Registry from GitLab + +GitLab offers simple Container Registry management. Go to your project and click **Container Registry**. +This view will show you all tags in your repository and will easily allow you to delete them. + +![](container_registry.png) + +## Build and push images using GitLab CI + +> **Note:** +This feature requires GitLab 8.8 and GitLab Runner 1.2. + +Make sure that your GitLab Runner is configured to allow building docker images. +You have to check the [Using Docker Build](../../ci/docker/using_docker_build.md). + +You can use [docker:dind](https://hub.docker.com/_/docker/) to build your images. +This is how the `.gitlab-ci.yml` looks like: + +``` + build_image: + image: docker:git + services: + - docker:dind + stage: build + script: + - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com + - docker build -t registry.gitlab.com/group/project:latest . + - docker push registry.gitlab.com/group/project:latest +``` + +You have to use special credentials `gitlab-ci-token` with password stored in `$CI_BUILD_TOKEN` in order to push to registry connected to your project. +This allows you to automated building and deployment of your images. + +## Limitations + +In order to use container image from private project as an `image:` in your `.gitlab-ci.yml` you have to follow +[Using a private Docker Registry](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry). +This workflow will be simplified in the future. diff --git a/doc/container_registry/container_registry.png b/doc/container_registry/container_registry.png new file mode 100644 index 0000000000000000000000000000000000000000..e9505a73b408e29a78898014ccfc5b5f8eb80bf9 GIT binary patch literal 354050 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�yf+{HcXUO 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
    =Si7>XP2FLWJZj$ZQ*!HL+Z#wC}JsiJi%d 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 Lm*7vI{^frFZcP$s literal 0 HcmV?d00001 diff --git a/doc/container_registry/project_feature.png b/doc/container_registry/project_feature.png new file mode 100644 index 0000000000000000000000000000000000000000..57a73d253c0b8ca6c92c80fca13068e5e764a69d GIT binary patch literal 392842 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åz*7yW%*ssUEYL{ zi)>v3fzZWeds&)E!QLjzP)tgNhw}>McrA>3eEg*-d)9 z6XIWRuy@-=OEYy6v2l8=wUAO*6Kx%|pu!M%k$;QAd`1=BxEB~UBYj%U_}7j7`|7`Y zmXp5$eQI$3{V%G3@@HtiNscN)h8#Y@(iH;Veo^c2jXAt$$z3m!8RfPk1KXYcqxPKv zktJV%iktMWSU>ESD;Vk$T2s%?c_@Xd1;6lbbN%njE}f!)ZNygziVS~ScIFwwvw?vo zuTr~|a%(YTVQ^9M=-zANUcoG=>`9uN(p^IC;mU5VnER8jcg+*j+usY-eUx1NH5&c5 z-y3?LDkfT3VeG%zjmPJ|T_eLhlvcqY+$O;uT5!L*{Epn=bMieDo(=lGlQ)Z$&63&W zE3C9!dFAgC_Q%D}W17KtEM4NEv{e84HO^mu)-xPcyjXHnKTUYEt@bABO0`E@G3ZC3 z;nGT8WMbNA<22M!Zdc6gV>XQc&^Uv`oviOQUI|{Bi=<5)2dUK4$|F|)(p>!)M?N%* zd!TXJY|SsL=D&2%Z_<3REh(w{B5RBp8O>=&9eP!QLx?Wo{afUu#MF{6uerD%_ryWq zs&`4v$JKBD49Nb(#{Y#Wd&N-z%50Izv49i@a6u!@W7it0N-2U zfS*e`{tpX(F{eO(qv?9L|ChC3|Js5g6UF1iy%{xl^Z&V{UrvTD;E#G2u-g0o!ALRzL|Q}g~e#UNRBx@q3~xgXE3^O8d?LyI&8SCC`fjNr?KvFW9U+~3b~ zJK1YL=yE8&a9QP+4jN(Hh>oYe+zC`Oy+#!$=5v~#2%V}jSy9j#W9Iq9R~wNsQdeqv zrN=za?9>{Y#14;UkLvl+34Mw5-MJuzArBZsuaeh>zDOc&u!ys85zU>cYt9rfG?v5v z>#xj~Mv+hUw8bRzp9NGUR55bJ2MZI6&*Dg=9}-R+wLTrM5#BT8EO+_I$QgV8lM^f5 zw-o00G3a!?YIfl6d*s?A>`c+CdHQ&-uH{zI2ZH+S(fZ38WVwghL{5&8GF!lU(E$9@ZPXH}f#fwzBDGua((eZNz zmAxj*{N#3NjD*ptcb5xo3fg}_9EK)fP-lCDmIYE0R<%ZGgI%5W?d%dfK)SM76hv{* zfiX1{rWd-5!V?mc#Zy1}z*C_yudfyEW!yJG2bYJC{7mRflRv08!-i`BVAmDcksW)6ke^@JO^~q0_IgIiI%s zR(hwJ+5P6^6iQeG&@Q>uiOtG)@ScynvXZ;7X9?HI1nZ22r5U6BARG?_mUQ#OT?7IY zuOyimYE`LaLEuNJ)J}KrpM*U~;)lEE-&uamwTw@Bx6w!qNEUn;Ybfg1*UFMPh&oSG zywIJ&=-02Ncb~}oJpM%bS_V={F^yK2`$R4}^bI-k-|9y%?A` z*0uhqSFUM4pH}AkBD%Y8Hcn6XEIbl5mnE&&caHq0Yx|!&IrEiqY0CFjtf`^@FIHtB z>GT!}B1;qwTAO)PfA8WYUB79D*zrmM6Je0t)9%)7zbbR_MRQT_ zK@9avZm2UR#lm@T{DWald(d&Y4ra(GGD%-rwKTJN#p>fj0TqP+Dt&?7#b1Fa@;9d3$Ba z`2UT#9ZbJex-mk$r!@WxLEHfTwCWg3Tz0<$IO0%tq#WDY?ab9;8$H~rkX~Xi(MbFL zJrzVWUy%v$V5Cf$2=SX9lw>bgVwRGMICS<3AuW%wBxf#%OxrHr53z}J9o;>Le>efT zjeF3`Xk71}_aQSsuHMJH<0R&b#=;k8X`Kn>Y4*(JCz)Him&0)Hc>@fRN3%{iFbMOZ z)IEx#nU=$-&p6)u)i$Uk{k9>*1}IMN``G}flqhyTT1+*VYKgtQWT$SO?+G;kl~sLt zf_tXQPn03HaC4S64@Nz(f1Awuaz%*on{AB@#0aF1Lb{R+WE9}IV30vWcu-PuK*fbB z86j#QC?6^TbOgENGh-^?^uyGSOUzPg%g>5`-<@!>@WybD>rFf8)pg7BoAA)E2bfr{ z4YfJ#ja*ew^IVspgqCZs7FU1gF*dzXDdJY$Rzt|w*32eka*^1xQ;Sw}i+Q*TqvK%4 zCL^OZnjWA&)5m$HW)6dWnw-n|{D}6cjJo-APNO8MuJcP4AP~&!tHxOUl*pr^nts!B zns&9MMN@iuhBJM>uoYidO!0&dgkXiPK58z?Tnt7jmd-*PLHUja^lNLC>;#8HyykAe z%2NF$Z#>QgIzrvuXUghWoLPAE3_c;g-uOqr$zxjR$Jm&$CO7JwS#4PvgPa~FCAVje zAE_dcxDAPv-7Pg%uMgD4cd0UD4#jSh4qT1ty|T*VO#qa}XR!&N5_fccj7EL9NVT`I z&s+XcH~?!qvZ-6>3&A9ZAC8O;-3eM~{;;|zJ3?}W>MzK!8h}=o0sBpmC#sf$s8M$d>%;IKku3FTUdr#`CwsZ0jE^?TtR+sZ0!gn7d4kXw5ghI&<% zjc^@r(e*MQfCyYfOzi)7a;b|0nh|wvnrfik7}a@L+G(=Uygp2wMwp6%+z9NTzE0mM zH{WO7$aG}EB=b^x3^I`B6H!ImU?;+WQm{Wr!+qGaM*}pYHP^E_$vqqNv-;zZUfy5D z_BQc{MCGVWhy3^ACTC2tL-*4vT}+SEh;3g_hRxcSiDNC<-he*Zifvt zZdA$?aw3yt*bo*ZduZ0Lel1%@t9m`HBuf1FAV#6WC>D*?!d^F{pXrejMe@2+{Jn=f7p*B`859U*nz)ojcXTjQn0 zSA3hijh8BaK6Wcl2qG?5EW^R;EABp7R_u+Kza%H=87FbSb7D_f*d_3J8`ZiFDmfam zMUMCuQsm>-lflo};d|!2%tTBQ-rz^7rTL->) zug1L)urn6xT|%Csm*vZpsznmygEO*t$;rS;-;}P^l%NBmzrU;V(=9>@-G*b?)S+dg zf#sF1WsSq~ITpaD&8z&z*5I02NKVr4@!X<3(=60|SYPzcYS&PeCyFCW%mQ$$58-Wg zO|!vF;@bC;gE3}^+4m4eF@pyK?2OIOwZ+Nc=14pFLO!%{ZsYeP5`8)X7E=!t6bx)Z zffG5*W$$Xda)bfpg43_pgIO)6k3NX7YWn-MyKIAt$i}r6i@>SAsaWyh7;|m*X3n)B z4Lt=<_A>L`cWPY0y-0-E7m~z_c;l1RdB}7-XlPBpIX}ba%$zgviOo>j;b)*q6?4+g zBFT7bv=v%HI0-p6(k z^;X!ym?(oOJ#WtM(6S;8Uxj+{lJW9|9j}R9zZYRMrk&}qF43U&$a_6^C^PeDFp0gm zRUt}6SA|%lU79YX2}~fT7ZvTl)jaV#rO(}nFQtDt>TYiM;OR)$11p%IPJa}&ub;WS z!aCd+!Kd;0Jpil(sJFxWWMgpw?Lk$DcJ~b5dBhuq;e92lmU{if{_IkW_pPqYWIDS- zgz=7Rv*ynAH``tPbiAZtAf?HlhJ1tvcA)uMM6u>p8)m*MzxA#tS#ym7WOZn`S?L5WZNhh030*x@qkBc` z3e#EpBz(@cay7!s`kg1<9E033JH*-VCcL^S$yEW5Wy_tN^1v=dJ530=PLb^j3yndj zT2Mu^`_mr*o1e`C+L74&Mov_bnVTHRU$=0q7JpUxe4pC2?_v`Z*v~uQDrf(_mP*Q@ z#bH51tJ$(ldx%N3d7~y`4QOcW@bg9B5}9NhERPqH@uJJX6Kv43K}&OCvl+ih?FFwN2f8a3I?DH3C!+D9(hh%wz?nbt_PS+XUt zHcwxws$kQiGj+JP_p@Q8NJio5;Eh9X$6kZCp9dcZ&DccW5YAyZeGV~;=S_QWaodF2 z)Ut@T0jRn@Vwh2FFw9gQ`#`YtFY(Wk?di<(z8?FPMvasCv)^LV%Qj2{ky;M;yhYB; zVzI2yY%D*ny(q4$I$9^$XPo&SnveIcRISd5hj*?=#KpvXgz843T=~#8uOX>18DvXb zVBxfSDPocOkvX6vD$Hkx_=W|NoP$cot@!RFe>Dr^WlFPrv$NQs!W+5z#o2WGRdT?& zUg7){&1Mtpr{C`9YA1-JNM$t5o|j%Ay3g(ct_kS9Floi6@q#3pH_9c+dAcv2mTljo zY1FH?i77YJa~-^c_ge5{p%^xkDL=pt>QSf|RycKrEW2MoZsnqz)Lc^#O+B1^*!m4ngw<_xyF$I*RJ4`hKgpiCszqYWg=rbzD=$} zcg?4b@+)v()IBi`p2ckTcH>S?_85jJ23{kwvFDYSt~ZwxA2sKc_aV`)#E^-KOdGS1 z4Q(;;ewNZwW!7#c$WC(~KHf$%lW1L-PD4B2OCFi3a%>AZe0oik%K>kqS0*d6Q7&e- z5CB)8AU-|B!~j%#I@7Dy8Oyw#;}zp*fdTsc^Y6Dv^P;4x2U3gsV*2rWa_-N*^78S? zu5}N+E@klEdj)hUClqU_y!~*`#xe8oZ4eQ0rH7#WNQXI^vwX+gcdJ!}kGm`+wfbqT-d9h%{aV`QoYy<1}mzypZOY z(p&Qtv$4D|-6s)hH$)unP!R%@l&|_|NC$9^I#rgp!zy1w+oNPP-T5i;@La9&+@q>qeeevMYNQuPB9Epb=t}ghE zwj{XO`EPNf<{!x%23ZU>Et_Z0Lp3?~9C`TR?*WzC+49ms4#)$x2~rE~zWA}(gzxHZ z@)!e<`Vv~LHbxGiuQ_J88K!nj=zX_+nI#xW;U4O83!KB5;UN^Rv z8?1P)9eGIq+fNXXd-yZb%eM8h(Y|9lJl_$8xeX5n01GmkjKPNe93 zX-mTfGGg+SbYqQGFK~fL`9{G=-PHBh*Whzwv9SBY3}Oj68G{MCB{atkpk}@su8w2= zj9SdgIOxnJ-lHqwSe;zoKiLrNBzo#5@CNXM)Hxn$O@vW* zO%L;(8^ujmsk(LsUZ$t#fP+FH%|FzuD>8(Pd62%^^d3(^?HS<^SvT=lkPFj;;x+*I7?ajdWP>E&;pP{2;8nbp+^*2IbKMJ$F zSG$DAWGMduWdI_iVwHWJ#`^+9(N-yAu0r}VMUFN;1Mi8VUpL0wn~21ReAztv z_YCPW5bMvL>9&mW7>oKhK@JKVDjuqQ*w$3knR>;id2`Z(FD+iH)9_Jnj3)|z(&O-< zIy=KEY}%Bk8b9VC(4g1Z%_P|i!Ie>X+NrvbX1>4`=!wY!|8X8TbIt6Ex%+()v|`$K zEfgofiioOe0cK-ZPf?S!z?RX|MV1>qHUB#IK%p|()w@cx7&sY;#=7b@7myHxXbX9E4Z?b zjl_K))vv=}m}5aR*82{Wi(zJ}d(hN=?MKn-+JGEGUT1Klw41i_ge%$~&w8sr)!(xjSnmx`Qe?-`%?1PeV(Q7tTwZI=zx&cYr;Z;htbQ?|d;j^< zy@9p8ko1BVe)axA7*^whqsTtzGy3t~i!ReC-c8k!jL-LX#yq9dQ5_3}XF?M(-keU; zqq!>^>&og01f@C-91qLu|2Nn9`Neff%cpNIpud2IGR}poJn^(4cw69h`CbPDy5hLtreZ;h-YWNPSC7 zTx+v?sKKX6BS#|E-}BawF!hP`Sa1V=#kYrS7xr~ElIx+^(v*Xv+>*M-&28G8AH}u) z{DD-dh(u4~nyKUa(y7-?%ZUAzBp23Db-RYC3;XB2ib7d3=UHF$!`aZ$NI3ENbnMCq zam0s}+%S({S_L6%e>-7B4;vO<>GA5~B8NfP?wuE)2cKAFn!M1E4&gkDjjM=frab!s zdOr1G8!(9u_M;YD9;(n7lpI_1ng(gJqTM%Iy*r;M6 zdgf{H;ZY9m-7CoG-!iwM0`!|9f_0I+g)C=JQ&lwDWr&>b8xQGJW`geR^b=DsA>J{U z_Z9k4CFvDtaUu0D6WC{-axP;#TPFT4nCoK-ks!QaCZ#AQ^PB5x=m9UFsopGRo=h22 z+$bN_BohE6EXIn064Mro;iTsT;)Q90r*TY=)&6P(A{2qgZiPSO)Az*>KBt^ZbPqe( zz)XerC3$7q6g6E$>~dO0Ut3__zH4w{qB;5AS{c5=1(WTtgJmBF5|R6stpVvh;BO@M zWPvZ;GHba{Pfh&Z#opKsA0ng}|!neIpF{J@N6~1k;XR3~!ryjYk zhjZ`x#>A}2dS-Go_W|RKT%gmXYZJB036S3LtugVSrMpvwu}BH%%}@Cpc6UBqu2o20 z#713vIoy^@K5CrWOQs3*G=H3{m$rPJ`@{?KG#~={p@{tbG%{PA;D7nT@h7Tc_L}kt zr*xO>d^p(Juq{w?N?1)|T2hP4LpTh!d3WuH@%o*hZjo!II_r|rZ2C5*XS*_0uw|z3 z4aRGJ1n~GSQYNR9g=%YVpvCKvEfT?n-M5aaC`Lk7<*w)BoVESjxh0GJeegq#H8blI z0I6BKXg;>ws4qE@Jqdhai*EM@C(pHDAk3ypleBi(*6;d6T9N>LGinyi*m3^m!I_@( zZ&D%x_$7vMT0$)|w3ClKC*;eMwoUW%yvO9`K}f4XR}*(Tu}PA9p@&TjPRf{spyMayh-sAewK}B;inC-Ft<#ok91ZcRJRojkX)0yAU09 zq0iomM|$VnOVe>3+q9vK>X+*-dI?70kSBG(wTd00BdKhnMz&7*IDiFOgTIzF^mMlT zkkI6}()?qnk%ROhq;V*IqNIo8YZ2-|*fggj^ebq?petFrJjnJN$ryrQv8kBDl*XANMz4S*SPwL+t z2S@c1uNl=^1&T-u6I+eizJK}>L>342ZtE>_$<^x2R|pJI`7yg>H4dy@wx0uY;)B>4 z>~K?(mpa819zin{9*v9~C5>g~Bnz?zrNvX%kVI@>@!tsCFy}*qmZ20+HJiv5=`+z? zU4(Nv6cf@-q0BP)s<+5qssIQq_>{11rkpAWZ=NN^We0zHI~MU_2mbnR!(Zu^tU{Nj z+X!>i`uMoJZU>HI4eoQVbF_@~bG5Q|&E$5!TKB!Gvrj>nC4RHX48o>8a*Qot4`L4r z#4lSPsx^S&Abxq>G47z=`eV#uwqpd5!_By&-68`%@-o6W9X&9yu;34lCEWyl?d*%A zEBn%j47?|@Y5MN2F4xoSVv)BtUu_I`ngtDZ2JO~_x$J)kh8PmBPp}rPhw~`)VZVB6 z;bu;edlVTgN?kjy2N~?tlkR^B*+OEFhd*kZqgnitwb(5+XU3sH-ZH!E69fYQAs`%+ zJc-S|K71W_&VBLP^<~N^xZd=ka_EFQCT&<{7<%)O~`)*QHi}=`cMw6X(%;=E`1$`&{x;>h_luR2&iA0N}DfzkEDE z1zK6`Qh}4#*K#_W={ygo33wk&h}oOEwFCxlc;Q2rj^PMH2g7O**d4^@ii#z#=xI z)$P>Vxa-cRbvDw3r&w=#4d$E^>()>um|_7A`rXeK&YHqA!DuBVv|n6JpO}^LLY|TT zLtA9~qKSS1(qW#H!*E%8@y07V8TG}>j$8vnn8y<*G`zfidP=;>D{0CL&4X1v5bm2$ zx1k4`yk607ZM#Q5VSI{pdUXb0KPWeGH6vXzuJ?gm=<)jMgomPE7FV%V?08BK)oyCm zWJfM-blfo2o=x{3js0TFwA`r@;jo-%Z(v^EZ8BW}y^v%ayZgAe0UTc?^MrHii|a;t zS!U9}zPn7Cb3`VWpE0~_4TYW9-sN;a88SF6=LNKzduxol2nrpMwhGkPbqgLaoL_9> z;yW%Vmspfhv7R)lZxDEdP&gEO|aQ7B|vaOKC^j+W=$3hAbvx-dZ4@#x%}0fPUweJRNh9#jGr#J!@rE zwfCeC)4etCZ8)fd&dRK`E}t@CFW$gE)GU^<7n06Vke5^vgrq%KRrh)EDcG%7>OvrB z=l4i~YHrVR&I1F_kQznI6c;xEyIkw=CFj*-tIEm+Fw1locAMny)@HxXUfMtz=uGS_ z`wZ{BzLc(P2=c{b?|z1NojJX)7etoYeILA)kMn9hqcE_%x6;2<3;^Dothe^+_Z8ID z`@BH(9!64{>J`NZ=9u|N!*hunAilp4QlPYRU?-o;O@%uI&%6G3-PzD5o@5RqwbhqZ z;XjRzzjiI3o~7+l?FSZ|c{#@YhE2@7sJ(Ds-*aZNWDmlwdaIZN!Oa}lK$M7JylnZ{ z3!CUTzkeZ-iSFAs+0pipJs)-)mPTf`5INEHX%5lgIqv zsb3A%6TkyVVKcW}?wqVPSPs#)C$*Tzm((;V#_xvVgJB#fM58W2Dp{%H-}1n`Tu%Yx z{mQY;oYo`Ldmr2jzLnJ3RJb}!mJ(szh`=;vnxOnq0?{A1o?*q<&VWdu}WFwh_I&)tOhS( z>-Fs+YDs6#^(2aSNO3L7GPlZRmpNY17*5-}DVnhVs&gWEozCx!6LV@n8b_0Q13?#8 zOO}g3k)=MCnS?wv3VrzUP#mlmWSsk5v*FJDx_mj(M_;;8SXzkTSowe0d(W^Yvn_0R1RN!Nh1Og!_0#ZT?ArK&dNDV#o65e>u8J%%vj`LmD zdtKlA)oy2hB*#LjcPwh$}bq$?M6K-Naxg3W;%v-ZX9J#T&gC!(zT2U(Q+xIG%jO*DF<{rxiH5sN2N5PsBnhqm#kB^F!+$T&M zNHB=#z6EE^vNGB{*1kJTQVh^1)Cm=Nn~GT7C*-~ zd9YQ#L*BAOePrN_hn~H$%dNA!L#rcEACE_}0@|cqbf?0j9q}GMJvz6O+QlP#Ki()c zd4v7p8nnGMDdoIkTA$~BY-E|UIZdKxzrW&^GOD6TzYawo-y?jOthEK(p_tRwlz!iS z`r+J1vb%j2X?AI;a1b_J+ET_cWBQ=s8r&v{xD7rm5_(@oS=B|%irwQ+>z&9PF4J@W z9ct@8a(l?(=5)M0lbTBE`bw#|hOQq@`Ser=>H@f^^%=9p^{fRY|Eyp#CzI&xkZeHB zJR|?XnB1IeP)U!;V&JvdA`@o?w`23-vj$snvoAP>MJ8VvR5ghSwg)>Gq1JDxLcQkL z5$wOFc{U*jL&i_e9>_^&^KdQGFYo&x_8~Gg3r|SPwYkRoBXoXKi-M^$R-I zlgNe~i$r+7T1F8Q3627FeXbNhm&|MuUG_>o=G(mn)xZJ(VzbsdB=$qNe(B*@9ef{9 z%~3O*=2zfkZ3weSXxmLz>sNi)z;$-Fy~Mae z-f&_)4m&RDce;Q7^@xFi`gku)%Kz44z zK8tv$y#W|i;*Al;uE?1v_$$Hwmz{=ppYLXbrw7PYmwG?e{>T&L|4PNF6qIgRF*{F$ zMK8%lC0yy`>@_r+xR}=K;0zBv;ePZE;LNcdh<)tm+>Hn3uTY;7yN4;}G%g60jk+lL zbypeOF96NERo9hlN(dS^eQM!Lc96V_jk6e8OkL-fN=pVj4>w*0u746v z-s>2%Q@U@z@EwaX3#D)mV4UVV`vR#7B7{(&j0xfn6S3B(F$e-wsHBv5aFWngwK}Av ziyt2f!`6g*O0z7=+FH0Y_nHA*_C7G!86COPvE%GVKFOM~0im(Wx`biDNm#QW>wNgYo> z9RqL&hzDrr=3c~JkCYNm?&TgXjBon$$th28(`MSd1~gPnDY7w0TH4~ahUDE=*2(!b zy)gmpX&!FSp2g04#S*_s^_gl>P6r_B-11HRkfV5`-JF5PpSwcnUyPsyFLl)+Yp2b2 zxQyMG@f8{wVisA#C5fWKyT%aTP0ZP!YXTLFJrZ?)^3nV$;8Q)&K`uvy>y zRvIB8ns23x8EvWqdG2~NXjb1^5jwnzG7f!3A@YdT1Io!IF`ot_2%ump_ zQ?6ZH4lkx*CL=ehAf%bbAma~VzMgUJ%lym(uBCY2J>e5|e#Od5GIhq4<4nwMZWUc} zQ&iBvT6I2Boi4$9Ct)zx?k8KC_k&&r+tSJb`3Ws_AjZivyUI6hWFk*<~@q<}?xt%8iB8i|tk{2T0+B}-eJm=k0 z^cTBPGKC*0?VPXDT~+LbB`wT{vy3}S*On*`RX*$KiCK&V@vAbp)aX)1M;nZ_M6b)K zwmX-;UxiQQy$KxhfRG>_9X4$n4vt4f#~{3(i$PYizXU*S!}a(5UV;4upC-HQonMBI z4S`xpk6aSFT$`U|@AH!P3O-IM>z+|=!<+Hc3q--lg&CWV(AR%Xl6ZLO)QW)L^{|}d zR5)^(FkES*9=k7txkvA{WjM62%r;uge-^Oh^ZGGX^jGJGSvvjXlD6y*;tx<3;I>`0?pis4&KF5G zBDIXxOQ+}eD+rm3CLdSxO|!V!KlS3|ka>)Z%N^n;gM%(se?Q^&d-r4q(Y+-Z@RS$3 zbF1{$7MD>1^jwEbO@~h3)C@9 zg`Ji$(;yca*k6!JP1&Wbq|w(D)c^-I*y^$p#Z58H{KC=q|CJbK$D;hn0sDnbPB;Jv zEvg0IqU*e3r+hJYNVw%#^sh`G1g@1TTgiEnj98g(PV>e=Os2c^vpMNe>*vbFm(MUV zjpnRZ_%6L^WY*Q+eJ$nYJq*vp$xhqfO<7FHl7zi6+e#08_ShxgIi2-NxNGm4=*1>- z*4<~WQ90RG-Uc ztHLP=i~aK?Q;=!Q#PBr*LadnWK&2(+3PSXPq0) zDe^dL&2@6hU3XG(hPogS*;QM5aEdb`S;S=0xpZRwiwwGj-q^O}^e7omH9QKt*B#CN z{6eJ5NWmjyuL_+YpbnjVe>V>GUCaJ<;`CAXNuiciyYhq4ORpXB7V9^Y%i-ieLuxd! zOLDmM!hxp5dr+qexm1R4U@K@(k;nwN_3Kk8wv-J(&RQO++AgMur0#SZxZCdjYgQ8F zF=o`P+a{HT9rmKDOF&co4pU7;ZDRz*j5(>HNf=|F8WXj3wx<$e&(_TlEP?d%7~hN5Hadp-yI$FTNy zjA81@*2};Aj!U4I_G4P+`}8rEE4+k9!Vgop-;opOWRfPW57S?AKi?P;>}9$W+tZyg zlynujEvwGo4(0YKZFSs!X7A^>iiYEb_fi28s91@v2*atu0Mj<7IJFhEx@qMJzb^jd zV(#g4FVaum%#}aXwma(fIR4jwdPH;OgY-!ek3LgWk~gBlpO}P;N?A_NnJKDy6y*|i zgSsnL5}Iikele|(Ci~XLSRAhm^4^y0eic{b=7BSrj55DhXWC&?m!bcQ!phkN|0n~J z1bTT#=d8okYjtMz7odD!&S1N3n*T;?Wp;^JHLx?*VzG)0G!m;I;D@8`^KI^RsVTEgA5Xl8@}DVq zmVVhGKEb|7{917=)&|MoKZDV6tFdJ`U*&{XE7{c8IS*fs>VIP+0#gzNez-PVg(N-a znu$gi=Tj)DN1z8UR8INO`BjKI#n0$R%=&}YhB9=zFqna!g(c3d3cC6$9=@BQBH^nfJoD|)wV#rGA&;*f54Q`>?nGDvdePDhKbq76}*EJAP z{wCrRQ$xCCxCZ_hKBQROp-;5%W7Q}ryU|lx(jwMY=AH7XD;?#r;rp>#~5Br8Vx$NG>I99#*33J!?^qspnkFp<- z$7G}5?85W`5&q?D%}g7n?o< zL#D*AB|njM*z04A8ok}67jQ08Nzv4KsM=OhMBz^SN(Vhjjjz*I3c_iEo;i>=TGDhn zi7_Z#FSlLCU*wr8zI?Df&>_;pR#{y-&l$Jx0OigmO8Ru_j_P@VL1UK!3`r~~3oYwv z2GQv;7pz{Bw1qg{b{;|YP!KYK&Cv1(xPT0Mn7zU90nPQS3<2bhegJ5@5Kn)8k*690 zQlJ+IL^p&3sZPx*P!#DmjFglN)q3Vvk|HAP@~Y+s1nx_IHQ>nEJ5^#BN5Uo|YW|%U8r8l z+f4(HjxIROGz9cDR*B@^(N-u>S==)=(X}w()2Z4`DSWU6ZQ*lv1KWcJPw|*6>5C4K z*sIv*<=|-?$cHq0OoSk6vy~^r}GJGDbPx8r>Wv*HQlEL+NJQgOK=R zx$yi{7h&7=V^`|vi~W+iS04~RwJg5I{ngUrf)QViz-`?)`~_)k zM>m1ZKf;3$I+^+Y8v10KL#rrBMP+5>E?ZSJZfW*RVr$q18t6hLUPM$?MWx+rdd`Yy zCqz%qz2s|>8|fbKD_C^f!{L^BiV(Isxs@6qcw@k(bGM&u`l2t+m|oh(*(4m(;-*T@ z0`?Z$TrZo@CUzf{q&gF==pzfo{Ab2TZE)ZAB&CT$DZ zW*TO^H9Z(7N#g3Zp6n=m-f5|^#{@eYpE*Tk3nkY(qBwA$MH0fU^EtDp8F>|@*V~nD zvM1z_mVS}q8y(Q=#)T|@5jSp((i5DE&Jg9Bs1eJvk-49bO(UZm?b(bP`r4c6IN8#vYiv<>tRE>| ztQqH#5Cw2&X;qxj7JAF})W&aqLl*G)ZOog(E=Fg?VilAKy{o-h-I;`fPB9dlN?YgB zqcJ#y@5SPXw-(c7`=E|I@ewi^ASq{#*TbSS_`$2fY2!`_!=gD=Q*>Tn^#&a}{2t{* zF`<_#(|g5FnicD&3VaTr?h=tB@cJShy$!pSHIeM}jx%Y`0iBMwY|UR-^;X}})Xz2(9#7@(|Wv)ZI;*N6-Vid$>Xda!6c z{^v4#Ua|6z-rOGTmjK`8I1`-AZvuThM@-iBzg^c>1?mB z(uEAeCS_!ZgJ+B;>yHX@^If^g0o$3byP)(Q%8JYPF*nP4&Q~M@>LiYBG>_3I*=w4; z@hCx=tixa@eK>FiyC%)5T}tEke2m7(<-hc0v)QW8Rs5j2dx%uVC71R_RB_7A$0GDy z*ACKryUO0DJ0WIkHIPA0wJ^kU21P*r(Um1j@(~(bH`8Tt8Cu+8X6^BY)|2A4TV0i; z?w>S7q4cEKp&2H_={|7L;K4hO(w(R-4fmr} zm||lv=Q8_!{Oe$$T1<^+IDTj)Y4g|=NhL!x5y4&iVM<7_R%OWMktC?V^nU#LrGl~h zkc_T37tKFsCQv<;ffansx{vY9T6JO-mQg#;F;&zPdEL+v34*vUI_t-IF#()OP?S z6X;;B>FB#8b!2S&rJ2EumYz<>Ihi`UR}{;iwzXn`o(jv<5E*b6Q`(Qkq#C*8&%QkI z6ocnJ#l)09%srNM`1o;y@TbM3F_QV#{_M#UUH7)Ur!x-s^NSAvYGth9>Y3I^X{$*# zpLT1%aflaUYeLLc9_JoV;uQcK9}iq~b?UE*hwHPH z^_U&TijK~|f9l}iJb;HU;w;(OAl0`716EFq-nia2lt?m4m`(6pTW`rQa2$=7+F*bE z?!gs-9^czKC_5ks$+?mIftOcVZ*4e$jeAeF&t^XC>Cu6oRB(G%%=N_|hjNI$q3r$vuOHmp>LpEY# z^YP%2&L^iQDHt{r7r79JHu9e$=WZ*vot?^G8!(HORF}!SbdrsI*F|9PWQMGrKBcB zf(?<{d=tEov^GBaV{-d!62&XYWFen&$S}NjKkcx*dZ_>{OO$;A{kWkyklBfT3(|*F z=zxYlKBr+m9M^%DP4I2ZdER^RHnaaN{>(?LuwdMkIz4x0z$8y3soh_pw^#F(Deq0W zQTL%qay*}@edLXl*Qb#;ZG|dj$V-ewW}d%@iEuV%tdz&(q!v(ZF2gj<`&P3mG}*H1 zM_6qeO~{H-W@r8Ks0T692K2Pu0)&`6>a7#SHjL`AHG-Ui#uC*YRlGo**;iM49Tf`a>18HhY z9`3GVhwhG{ePHN=f>Gnj+YR@m^b}&O2XL zt?(u)WbT<>uRCezrlF)7D$W+RF4}8=OU+cs_dn#MlW!3kIGKDopc+=GlTlHuovvj^NvBb0|(EBht$0I#wG-g&K zA|k}l?th*n-y>AytKcije=;sR2LEMT&X9GT?WfC|i^DS`88I;)2}V1E(#7S$#W%x> zj~}|p{XjI*CABz5u{8vfA*A_M&)7T4tRzC6`Sn?v_?_7*XI_b<$wd34ot~&aTg>4m zLX$JUMrW$v7DM9#vMfxEjUV`xS=U~@dB$(NoJFL^w*aXjBHWo{x^lwjRB9DyRGZ(_ zY*8A1g<}u248iKRYI+fBdAhDx0C+~Fj~)B4xV;^)SQZ%Z(SJt;!-7vJ0^Sr%rTAel z{6oLLA48KlPF_9{{cxU+f|~U?1^(I~WhCu!OX8%2VS2 ze?65OO5BNvAmJCyQp7=6qrc|3`2FAir(F(xGQ_`GCC1|v84Wi;RX5-zsJ6PUlIypxK>ltqsF;?^Fj$s9-kvSQM;|?dne5@rPL17i0SKK7~P4lq{ zhlE?GTpPHu1psJ(w6o(mJyV8-6C7iB)S zV)U|e;qPda{xkt8{|i&(f3o_Ytp1YR>W8`dyW9V1i|>Q+za{)_`TTEM{Qo*F-i04! zJ^3O^ayn4}5;y;w#q&)%-_4mHt}^q#(w6CTA77;9aa$;FonHb3Wttpr&o#2YlM-JQA?%tjS9T3pMPWpE-=s&@M{%G-jRQ$@lCZVmu zL~WS;-pqFgRxcV^``Keziti^+{?3wiTc?$X!b8wRJeOPGdo1vCN!#eUKu`&qy1&L2nU-(Rd84%IPj7t8|=4_!G=g?{3? zdP-Tokt*V*W&M+86$kYq`y#`48+~F+$g@nk@LQ^Z^A(2WcX_YFZ{2;4>C>+!l$4z- zJYj}bEz?%G$s791`zNnI+EJXoP5o^1P{vb6IO&2%5>3*I6n#@_$JVYi?34d?;JUAMy{2qJzD?zV7r7YlGfzt}6%+gfd%o?!^ko!Q;b zX;2_ntfEdv<}W%GjO4*!m>p5a$(9p+J^uXZTcVtj(d)fsou0YQQ-O0?@YC#=zCV%V z{jJ^z79dY|to1G0*|5fs9W8{l4gt|4uD3<5@o14{!e(S1Xq_#5YN_}cm9=f8J86)3 zc(5E~5YVJxYiZh+p$bLRe+aBmDrs$g{!*fdzjwKV=%uHoc)39(_tk0f^^~Hhv{r}dc>jfC#)IZ3h+(Kj7QLbzK`WnKCdy|`fLzDrn9Symap?{@A9w5 zQ3E-u-Dt@|N^6RQ374&HGauFqd8Jii6JRS&6te54g4;Xc4OJKCp=r5*oFZ~ zD__}4;Efcy>9JfK5X>Qwe=+DM`_OND6T*VLZ{vfaK>qafqGAqn9t_8~4u<2z))xd| zTUm6i^A>rd;2PP0DytcR62lEoHh_11$CP5-lL*4}ye+mSPaaCDQq(0(8q7U7@WniI zF5Va}-0id7*egfJnXZCl%s!{M>4MRv7tO#8TXLBl+NPXopgF_Bs*fg(Z(YqVjnz1# zc9E@1Xga{?i=uhmh7#Q*n~$(su0a%%A0on&v6mX^s{$z3)ReOf86O$m0I04{wzB%I zzc6SEnQB5CN($tx8V4#{Akpi#votU1{GZXQCi4W{Px5D%~{2#8UUxM+RZZ^yFClRmo zf;Hu2ieDtiy*t39mw%AL#;OW_g-zHVeeWzd_CPTNB*jH$ib5<-9uKUf7-uC{$1&w$ zotrrtEC~i`7E@D)5U&Ejhy0_HL-#Xk8t}jq?P$g;{tw*P4>qt391<414E5DIN>0O@ z7!u7Y9yKj9Tu(+vr|FrjD^!9y-efsxj-(6$QD0W^9QD&O&D9(dt}bzh%j{l_j4TDR zw?lc+QeTStoR0U#S?%o!NnoRrIcn%BH_QL2q1cmh^!#avc*f{?!tg}5Ecy!9d6&)N z3(USxzw53=A3BJn}1tG#Pdb>b$TB@I+%<=u7fs_Uz&(+r7u zalCgtIDNJ}7`^}x8mOTo75GNGP+v&RQz(Ql)L)O371WtYO>=5riLAi7&R(;oj2 zbE&O5QAn6#Sas?W+=VWkPHn=W*wHoP#vBEaM{DLP{mRn8IBKiYFX+UI7bf7#J)*6H zl#N*G-lSQuXJiJrt+Io)#-oLj*{tC%GH8liX|!E9E4A(y*?+oOXwZ?#el^0;tbjBp zDUUnRvenw2k+mYxKzeiaw5D)2Bik ziyvI)+HWd%wMYs<`jB5-dn?Wy7Rm{>AHo|pn^&2W&Yn`YS&q3RIgh_6=1}8vM@&?F zSJ9<1U#@L8cs|i*h{^4QvI4!092x3}v%1nUZToqE+tbGV#62!LBXc#th{^ zj~&E%{kfG)(X_BmWl+7L&&cZ;CMuuLgAW3#@=XB*XXSu$TcM_@os)xGdzN06O$C6ZsU!Z|xL(F;%vgt(c1q;%~88=}>g=Wur!i)zQ7AKLMiEYb9u}iLk}% zVFQ)=sLdWIqV~F*edc%>L|XsphT-N%edF@ToO`+zBw@~;E&3rCQi2(z>%x|4e2F}o z`5CD{-_BB*rVfn;id%R202N{cV-~a5P<}&V>a$Pk=XN!H8eSh%F`XCEDx-`u-XCON zpQSJL?lQ-5bDH=_ml?h|$)*z%@Ca_TS2g`g8%eQ1pFtVqtBgD>Gom^8pjPTvtPvzV z%lG+Hmo|T3rY@zTSpf@#lqJ9-{z3F9IPT-kTd=dJ{)VhL@VfPb-{ltmt&#Wji_8St zK`Sjkd!{-4>$K14YyiXvVNAYehFEq3rr-a|f`gy2@pbQ=9OtDm@lT6V*DMTd9#3DC zdFMmgdQd{0CtRt>@k&sNP+M$#oesl#cq-rsLo-LY0Wq>`fAaL=CfspLo zvWb^o)kT|c49rS9Pw>Gd{7kY3WL9+wXSsLVsSVsLO-=n@;wxGHtAM`yMw#L?sY5AJ zw=Mi$$k+6aOUtmP#uP7)?(!eZ;%nR45++B?O!nRgZ*n-`WUwvdO4$TETO)z z#VR1qT{#`{N#g{XsEF9l_b~IgT@6qVgJf(mM3M+xaacC&_ya4ip=USA^?r*@`IWw0 zcmo-GS+p^pDuOs5$tIQK`@1hZ8!%{%V()#^qPRF*s}pz3IlBoHKbz|#S8CG2DM?^Y zbdMu>x<{FVpt>gDc@ek!@D?-gS^>6L2gOuCPJyExm-8?t1 z@oq6MrAIMn-InWomso4z);3LmE=dVhN8t8r)j`oLD=fE%a-W-@i!qa$9-TCibgwZ+ z27KY(MG5#EQCMSlI3ABS2n|5FZyJuSHo4-wMdz`q%fcJT{H9A`lD(#-u&|(9C3P}M zT7B925q{ryy2nmF|758cGQ5*u3+xU;xD>tkr-7GX{B<`)Dk&^p=tC}@_jR|7fW1dw zu^>BDWGu*2=NGsc<*{^4K5afjxaA(@mTz@QEjvSJx38~(W?;YnCV1){`&j z*dRE;VyGbW>HCm+oc9oIAek7Vl-I{Jir0H_;EEim!a2*l-nOwvEnq1`SCmZ}@+`2m zikRl()hM%Oya!JMcdS*2e58sIZCA73)yl;W8Zn7EZl3V5*C0{bCR?#PViOQj>FY+oG`dGB@1%rOIS=|W&DT%Rbje;l5hFWNsXC<>S>3m9aO}90 zl2z+TgAe6go=R8eADcn(ex-fl57UD@$f-&kp;V-bRakx%6obKZ?V{0WO?yDWk7WgZ zSXseKXJ>iFVle{)^AW;b_qDZSh-jg*Y5<&SdZ$&6bh_$g(+e|7s@$BM&YLve$?Oa- zIUH*^_@0W&yxW-Dp&2pMm+B?KA(5be7eT>gU@*}KI*5j8}t z68{%wq)-cSws`$)uwGG6Zqo}nbZbVxPk}N<6zaE+)OXx2^O~7;V{0FwJHuXB($9|4 zoJo*EXfeyKppXl?W}dZt)EYwXo#|zZJopq!f_Re<4-SuDugRM6 zKI>dOvmz@WB|0x(!)m3}c6?>pSi`@V%{gRB`q<8&2*Q3C=Wn-}lN1BGDc8xnhWrdw zv9e!D?%4U!jZ&;bqGQK$cPAO*Hy810?W!~0WP(|;OOTP}zwa-<`_{|65s;}f6VAa+ zL7kdT0rUepLc8|-;gx&JR*=5vc#C(MCs}*)xj&}`yS}>Jr61G8Kw%pry4=^U6t1Z$ z@FyPmzl%eh5+!>FS&FLfsn-L8VHp8;kna2U$7%-ujr16QFyr8s0?Fu6nhNS|=N5aG zaSN(0>-`VCb7~GZkFUOm%N<&(Q`A?=+&8Y^wvkEj&2}1Q8D70tGzU|DWmVw9cM9zK z`m?UNr9~BzYL{65F+wAgjx1f^k?3M-j`4F6hJ_~%O=yn&G=}|K>>&Tnc&CmwU)&#Y zuD=t(U-t6UC3250Qz_s4*$;nf=3l?dS0ty6X+{E|-y5sa$t-jsqwb~WRoTSvB_$Px ztqWF^r~|Izm~KaV(%oyZ}Z4}a!D z|KmW&=sYRi@SFC1_pOqSyx=)I%m9X1Rh-i@&}urm_vnpB$epI<4OO8zr(iebgUhna zsg;-951bau^<%SiH0Ak!Ko|dcFy*md@h|X8-w~?4Om>C+^DlyOlLILk&qFZPYk|y( zF~$9t$XrNd0H#m>z1;Pj9T^ke^?EXEAm*vifybyfKToIqa9X1be=uP1XGof0v>7>? zgg&6n{Ec_}6bnU(lAd{k(&o*6slJsj3;v_KX@$&kLVXNj-Z_j*RVD9yx(ZZpi}(vZU*l^#9;b7HDsXIxM6EjK5W-s?y;wOGwh1sCHUIy)mwIMH;`&9%m@3 zt{gnCI$#Jh(A#rZpUYSBG7Z?!iWu|AR7qDTzSV|f+*?e;3Fh{}Oo>}y| z-s*k}o0S1A>PP7NA2+v`caJp0hc{O$rhMzlN!sJe>OzwK!ERA+?`+T*E5;6t9#WQ+ zykQ+!owmtUPcJ>@(oUOAX@DA|zeSqqD)bMI~;&f$p8qacW(?;|d7xAiM2?pK8 znDpuKyHY}2_ASM8%vhxB3e}}8F?&LX<9erOic>}KE4FqN(p)Dm@NUwpcR?F2#gHCI zz_Am%9Fc8p+KD;AjAcI-FZnKn|6D0@ti#+_zRgnV^)tcxivnYl*o2&C!Iy$gOS^=O zecA0!%HJ_8vcTVGVQcrPZ$685Y96YMzukJ9ad_vT1Y=B$1?et~ZHP^YE)V5cdy4ab zZYic%UGfppv)iFR=MbKMaEU>zsQ8QIO!u>ta1kPWL@awnuihzyB7+YW_qu3D^BfP~ znpSYNl;M9LaQRFhvXff}zwP>c3F4^x5sJ3T1#D;dP`t~mZ~evE3IIth@{zFtLEQNY24;rkOAQGcfL z`@wml=4*WE_P|%-TM@F4HS*aPYMn^nCxw8@jj7eboA`1!lsLLdAer5(N=#|<>Zk|m zs&!sQZuT*`0f=)D#x)#fNI4D#74jYA4x|)X9mH4BB#RQynMn&_)Fg+q@J4@+&?v8#u+(Ib*a9m{cR&YmYd45AlUl#^B72v#E0rNyWo&FI;vkrlE3lJ(w0XW_jcWYpY}*P~ZHeas5qu z`-M`1OR-LyV7$Xdp5-O`V9_e@!(k8!5KFf{8*?l7@m34pO<>MiUZ*Y|n^!RS7BPK5 zw~=i>iQtjubGPO6)jyDBn|n$VLS%UGcjZd|GtR*)6o_rUo)xy4Yu{d)K+)V0n8|;# za7ZRKsp)}O=eE^}6YpBZI}pANdFl(O?Dv>CnA}+ZbpN@tGI=Q;48sdPOD{F`NS&O_ zk`9IEih1bLTk2qUMfVN@=xo%=kG}V1fa|Lb9nD(^c`}-g+J2K@A2TWGye6ofU?J5K z`%Lp^I@s>T*AsC0%QU~eW6zbNYE-E*(N6VP6sT5lj7i)q;( z76T`tPdsOPU=2gwDt%AxbfNUw=&J#i7O7`q)f$Z>1#vFuu&H)bjV6N*V5bQ9Fg_1u za_0Wem2%JNV2wZ%>>J*D-(eG{P6Suc9EJPa$st!t8UprIT2pjkt{M0-qb2c&sYNG5 z{aofu5mt&XQhan=M3RBD9ZvmquHxI6P_=`m7KWsnpFzOY4GuRECZ!$XN8@zf{+(AX(KQY7vV`x^`_jzo#_6%q| z{?(*Vc2f!KYMG73!rd!PqKjAn$=ywbIl1`W;Ris4Wqe-4Za_)NYW`S7U|exV)q=k| z$*)16<3gdEZL6I4Pq)(^#$e&Pn56peFPN_}Zb;~j)pf~RRqO5pqNnEyn}x}{sUJEF z2&NlkLZ@(jMyVOk!yaYG^}7OuSY9Pp*p<@?g(T?fm}lwHqp;4fu7P%7F4%=OJ?f_> z&nd~Tma?s~@^5b-AIK^*yclK6jES~qt7vYl zV|p^x9S!Evhg~u4F|=bY1K+jpUte*2COZOmQ{R5eD+o^Kc^%_fTLH2`UmgHYKZ)7B z>u0WuT$_ALj=x!VOXoIkb=?p2P9D?RYstgV0k`H$HqDB!{K01b?X|yeV)9g9QSlsH zcfXa*lQHISDWI@|Zt4Z}Wv7gkw=_eOn1NdmTohw(VgIU1cN@COS9QJLDqPgcA+Sg5 zvx7=JnT2~-10&D%+nV`dNqxJe79>M59KoZ+1(f6^5EF4^vNd64(5%v|O% z>Z*Ql{d*)E4dvv4Ph!brFbF4#w9}p8IN_xAa*@1%sn;E^?Nowq=S0mf)!dm<2g%<( zdgQ&^iM@4YM_tD}x1Thrq59P^>_BVee={i0UIR_|Vzz-J@75J)0zo;Cb8~m2&lptJ zsT^6gKH{4bj|l4HBRz*HGaRtJYEM3O<7#Sk!6Oi$)iV6n6XT^XZAZ{K1JDa`g1Z9! zKNI9KxvVE-E5pj~+m>qQI_JR8kDnBrs4=ab3%*j@%mch5f5T{}2zQz*z8DJE_35_A zwmIz6Y}3~uCE)r^>YY$+O3xjyqbptvz}i@xlpII(ON?O90G-rokz+Ly{c^ACp&HFa z2@bt|Q^pNmg<&x4(W3Z(KhLZ0GwkctsjpbH8xr5$V804^BWRoU*e|l( zr<{oK6d7?9!zs$hxk3iszAdbG3a@?}XOFt=x#54;YEa+pCuvMasP>QE3V1P_5%%5A z3BJf(7C5D1zki>6Am2G!9XOBeF23m4*ASqGGiqp+qwW}>Y|Sx(i;5ebPbG7PU{p+W zro|&2JcpP^W$SJUH~5qYz4$n13CtC9_E%16&t|6(m*!S7c4a|9rhy3kRmrK7>bfcq z$NM2PaPQqmB^Kvzz)jR&O^)8++eV!xa;a8wOH8Dv_Bba|}Bxwv#YTqQwpfUCR5 zM>M6%Yl+KREwQ)_tD(KQqRaWw1ZfTfm;2QXKuq7o$fSQ1*pCpLZpVKfm=}l2etKI` zdH_`sx1PdvoiF;Sa5c!nb(3QK=Hifri#k_A|8mPg(-XaBJ4>=>xE%$~d&a7sKHdZ7 zCn=aZ3})%6!l+6=+*GbaCS+Rkg*|0o{(!HkX*g2rSmie%6O%2k3E^F*yIy&Q>(OWq zh}#u=dvjdUmgYnk&pp7{2gSwFF|ifb?)VL3n?Td>CV#4$+UQT`QOPk=Lmq+Eti_wd zn@%E$94%Zs$56get3N$gcRrfP3@xGg=9dPW^Nin1$uy$7h>5ZHbq;CW2e9VX&w0iz zB8AuWjT_F%%IYwks0hs_@?WyK4CJzt84xop#w1`|`DHsINYk)ZL~p(3-fi6&Pc&t+ zg~TO8$oL*>Eqtr@g{bJ2?FU%AE~9`^35bsSxpIHCMb}6Dox__tg-0YALZjjtrI>22 zT0F)pcWGIk{4*QtH+A|iTTcQ(*5Cj{w;V$0-(LjddB@$ZN%G&WWmp3etA>oXS7ME! zUj)=UkIS+-u!KIljskfEuL*}dxA)Zsh|Eo2mVrJ>R!nveoeZ_%L3w`ix5=ZgaNa3B zc_D2M*5ux0xv@2DHFgh+>_kf5Yc!0D3Jpsa9;&2W>WihbwGTmPZ9YsR=(F3TyqOAn z8yf@mR2*VBMgXOH(Avx=aP#h=;Yf7lzHC~OA@0Cl*ZgwVHd{1!srOB6cuEree2Zzrv%=s#g-ISd9e z$NTRvw4ch9`UVDfppfg}7|qv9twvJxliVH^N_jvp&m6^-QTyw0zs#2oEtcW1W;+ybY>Axjna99iNXYlKDl8WF9{Nyc~M@ZFOO-LsdW98?nr_LUe%{N{4C}? z?scwA@x0ad5Z@0z;$Pl!*nBlueiyd+<7)WJXUIRfPTreqPW~{R{`>7iW3paLlnoHM z_;;?tUp4(iUIOy}<@dh?C!QnMo)GmgBk12>0On*5mu z^oPHoem2SccZ2$u3w}}iCX7>6{oz>d*{1NHL((j{2ze;o{aFn zcpY`Be_`?e!QuRehxhCjArKjQ@$t()Jo^91TloE3oqsQUpE3sm|4%;rM`TU)I^mp< z=iT}rZTVlnbpY~vJ|ztR_ygbk_3FR-i7%uGNB>c}+Qa{=wZM}RBGwS($X{kTop*!( zyW3qSAsm;Cck)sHC(rfAT1*jw6n}5lKbP$9&H6(u{ytfME(P=7C+kmr^S2uOQxpHc zv{_eCOB(;;0{Fku*?-%wKbPxo`}L=y`rCf}xfaf8&-C%2m;c@|jDMy8soZ0-($r6p zjwrtwmlDlGU7*;bBCW1NxJ8?Q7&VBjNcve{oV zdE)eNq8o<^DJyUt}ag86l2& zavdYeWO5;0Kbo|Klk&sfI@K9a@=VMu^u-H8C83d(hxm+awNP8VbVvO3Mr&ZQ*3%(L zpz`ELge(4qf-*FsIuWn}%2*FzRV$*s)Cy{9ctruj>TlNSme##WYFxIXXq#MtIeCAS z5ad4d)h+*CS9MG)yC-Zw?(Y;w(=^io1vd5-P_oait;{4cRv_w2h8c;0>f_w@^bJho zky3x12}vl2Hz(UoZXRFPDXO@14!sa~io(Ym&W~4{b5Pl%|lhL!>5ivI_NSE^R`3mA`brtCZkWy26A$Z^uj z0Q-@3zxV*v1f;7auO{^(&=boIV;162NR=pmbM|J|j$*<|xq|BRQ4GKqarV=HmUo8& z*ME=+Izgg_SjJTCl#TSR^4J#u!6tZ-O4agTh$jiwb?V{uJH9i6z}p%Df7+MdUtNEZ zp8vetw^XGHvl;NP%oSmA^6|hEZXHC^+Y{Q7Cs1tVTH>ohNiCQd{+3$2R6VD23u2-V-FzQhwEE}oVi>q8;Fa>s z-;rBT>qr-aC@z1qXh%-?QM*F6ZUM=A7<&oA6zfisIxk&6EF?*ow>($i;8rziD0pQsOW2?2PK=F4cY>K6kAppZ4i6Cs!lnbC)%z!&}7xK~~z;d8gpV_liMj zBWSfXyHnZkOPbnoBc*E)fD)m7^TwfMUyYBPX8tmp1kI(#WtTlqZ_=_wTB*%kPR)bO zp!O=CN+{sPt5o7DWqUt(5XHsCO*eU~Dk6%#(n+gCKR&$0aL=!Z9dEf!5+`D9uqj>s z#%yoYb&Bc+??-nYhm(evtVaA?Z=@EcpL@;87a8U@ZfOj#{0fyl2`XS>YC-wN)$`Sl zN~tZ&Mo;h*{!o>gXKvK{k-9OT5;LN(tdC2aE0;?O)CNuX;`+{PV0AIb;$&f|xy1RK zvX41<^gqHpo@h=?@d1<*G{3WpbD*5ptRqq8-dXqCz4ZILKM9K>mYS;&j}0XAnXN$dzVi-H4j1y5$O3l3#s(o1dz zUmH#wOp+(Ne3?)?9jC;fXI*GMF0p2Bl9}9Ni%oNqd!=|qQeGb%N)L??8E$8JSSOPY zE>IuS!{qP%aKJG<)iB}i23a-KKU= zO0Q7~ga9Ltm;On&grq`6HCx@VIn>N?VZ+ah=| zBWau+l!afiaobqeJ^Agubfa^e__@h3I~qs)#@Ex8|l)&R(!!=}>JhX#W)6GJ_2R1Fux&)2qTqtnr6*O`0_G(T6tA*m|H-(T5!}sf^C|w=!Kq&% zpr0XkTX>BPBB9$??*_8+ZS)WjNtLzOAIK6oXy{*mm!k`1Xk;6w8JspZok)oi>d-ub zUSq=2Ob@;FI=!@AeG>^?Ss-0L(8fq4AaTOu`f0Dxnsx^UhHJBph_^6IpP-yCmNCTc zXtWQ$Y%sj&GrB8?S!9+Q8G1W?13emrKe%HuYWWl&(aa&@fVNSZKFX_9rJm>pfl!C3 z9H#C$?Mq?1zBGw5-Bry;O?#p<3qu1uv|w*dt5REHU)mRS*YtXkkm~AnB9O0!-831J zFSjJf47z9Pnhx1KW$4uW;sv;g39ia6BR!|LpD`z2v^kTxpIoUba9;4AgW08MRXa2XEt zL6ePg*+-au&g9d<3qWe_)gwx3QS7ZAUo|7~Iw`04;_A42K%lR@I4tXB-qZ`q@m&YblVFGilDb&m?@O?(ZLN#hn&hW#QO8xoO8Gyc1esrCV$#J5b3Q z7gsdwE2&pxJZ118n6=+99okN+#4(q2`#DtlD;hL1k}^XWRjm?c9J*FH%(DG?U^B!;+bg z8p*^s7~G3J%HHo6Y2&Fl^-%?UBaokEc+)9?GeQRK>rjP;9iojQK40tyr>U(7wPL+` z^`f~)K>x-{rHw}w__0R;OxHEsP_OYtp@M(S+r*=TCg`o>?c43bA71(lnk2{HdARkZ z4gbExK9~$`Q^19Y_R_3G>8y2I!e1`m6$FWuOwb-S`)>5lJUc7JkfQYaY7_!J3?SS` z_>pSFwpXDoR=(HZODE{tdmYc^;*h5K6fjWNcV<%;SXyfopa(GcQ|Hyy}M+Nt6p_S7lCCKUem3E}bMV{XjIHsb<}E zf`a0Ukp2i3pI{a&O@E_~BxjwH-@Uad)nG(o&N?sr2M$)vWIClseYSvLjF{Q+X$- z;lt{8%tGwav=PoENPb(R+=-Ro%l_r^mXC(ZYOxgvdJPt1%K~>Dw>|@U-phURO@xH? zAl)$e9pGl06CEhwx0FB*Lc{djJg3d+(x49i}*Gdx*DDJxN z?KapmTDAqAn=ruCkxCPP)nO5gr3HW8#Dl?*Phi%9QG?8n3Dfz(&q==Og!7HINjn)n zy(b6D4n0v#MNXca%u3n!9eQW7dRkc@(ao(S1H{t2DNxUNRTd!&VT1R2Pu@dsC%Lb>R6?!!L9-%LOlf$UQYZ+eN zNTq+Y*}onbWk_UDe`)p_0CkmmEb}&64^wLCM&R#b?TDWH|f#-YNu{K zl|?~XX=P{N8(wkRUz4v$namv~9ts%xF*!jV8<(6He@IT4cJ}!rURxGivC%AFS@)=) zmf6%_IsVCR#h|=NcpbmAQKNJ0VWBD}z9Ekq44fi(q0{s}-*xNM(h7-J(s}{tyo-Fg zQ(<(1>#cL?h&6(^21% zQKp(cyIT!a*&?wF&rkOxX@yP?f@HSCmpn@ znVU(%#enbhZwlKK%B?0b?X2#xVq800%lC^SYD)T42h)RCku0!WAnMrM z*-c+aOKq&4AL_95&+qe)fzD_IlC>&TUcK?N!mA#0Sdk1m&DpD>c{J>*Hv+Q8MqEzf zxp(vs6JZQN{WhAPtCQ-X^$nDQmxCi!{>Y9@XvtRPFV#vi-m-+#_a% zX4Om3Fmnz9-kpErq+DMv5NoZn1h4g3`7tcMVjjDGTjbDNy@BI0x3H+u%%NvfSuc*s zJg3hGe4?%k+BoIRG=mFh8k)%9uAcU+ZqyfmF{FHr_+Zy;GeUJqUPCN%rv7-$mp_W* z{~4vY&hy)Z5#U9UBKa?7V3p(|+P%COpXE?^Ff>HPKBqF*nwH-pP7YPnR?wX07q@y0 zE$^Yeg5NlVcclrdvL^|%UN^6>;)4x4^>Kq2;+)3Zm%h1umWz&J?5nG1|qD#mr;c@C=Nk$?{~whl`9i+bSSxZ z7|Ra@9l8wY-Q)u4NzFZoLhICwj4?56z~PLfgXs&OzxgNW+BXke`sJqe^g!k$ zEXU89TT99BLQ?f9X)3&aU`)Pa0kFblJ4fQGy8fRXcCo{K_Dn1=L@w8|}-8l@J*Q&tB z-^B=HyX|aVWP}L>22BqpVQmaZQ+YyvimNp1NaCjh)NJQ2Qd95eX!LY)j4V7Da>=DL z7@&(l>SvsBHEOyo9poUc-dY=Qp-miXvVl=X*7sLdigNnK(*U8XSNVZIlJn~0O$%GY zxDw-*uI;T^`6{NmMppQb>8c4$PZShwlOzlJWEdLmp6%*y3Cr}B0#m(;Jvc&I_1f%C(@RY0su4^L;$${+Pi|*~7)v91qFi*(En{B{hRpd^CuA zeSDjb`q1|3MLH|31T@^)K;WK^UdpBwvg+fdrR+mpP^As{8GOmp+%A;2F%=B@E`V{j zPkw6a{_Jqi7Iuglv9dIurjC-p(=Cs=rg>C(7!Ip{^vRNj&6rN6#Ppnq^37l+ULkr| z@CDk|Q;C<(;DUY1d4mxZ1ky$lrt~^V?4QTsb<%_uvU=$xk#lCw zPn+gd(}1viv@dP4Mww$2+Nk1H=tUoN^N|lVlv_z$;pMg`)%>h((K{+a#{pYlEbpl< z&{1|8(A?Y~mbt8>>oQ4~v@m^**Cdn8)JuZm4B|QQt{|RV91~_=F8^boNYq=(zKK`e z`J|e)R`5>6k%`-x;`G_xGTC!AIv}Vb-33HUfo9|6ETu z7NSns4t;jJ|2Mq3LZQvIb`u8Ka0P~vR_JlSYy&9E=reb&NA=rFBIIAP@-1NEm-`Z) zn6Fox5~d7un?AnE`@H~VOn|o|!x8;`DRypOQUy_m;wsw(HlAX9ymP; z8v|jp?GE*|5?x9WbucdxRW{gDKEi$_KTI;A@W*B=@DrX>mA!sE?c>cEzD}K7&dYs< z=B`!!ZPhcCW_E5JX<)zXmsS&k)XqzK=o>NCFFROr9uJC9ic|{`0-5*quBX(;)+ieA zv5&mSgbzYw&F*k+hYhXaL)?&4)SoiihN=uH+;P6J9So4ylAIr z8EV2#(_^?>F{iFqTrHn$6jAd!wN~5fhs$`9+spU;jV`^C#uWZeQn{;Gt;8F~Qwokg zQG{RB$mdfbk+yl1OWoxd+~dkKs_D^vCRrvqoVTP7$p&k5LX#P`_=>GOvG_VWd2tKk>T)(iXnz5Dk&kCw*O z4U2c)rHKQ)cDGfPgJ>9jt{m=6WYiw-s4n+wRwi?6Zb~9op9K_s4NFmo+3a@eZ@TPl zraYXGf=xe6KhYS1$-m4NG1;*F-!CndQh31LY6;__U<1QhaKR8^nAre0Z z^O?GRZfYru4q~k)b9BlP2B$ZqaQfBWZ=TXHf`2UQ_56KmvHoePAr`hnzxh<%KTvmG zEJ4oLzecrggqDofolx0+({!Kuwpk|Mil~M|Du4oHxO##VUu|p!%t1$MWJs?>-$N;= zLw<|BS`WH2KOQpavU2AABXtjW&^qPMKTunUIuX`h!T#Vp_WU8Txt(ze-?qcELyE`f zX+}q@Rn^NB`Orp|{;vvqYpkQRhn%aDPKAE*4&b-030d3Pdo4*}-1Jn?4ilcwZlvm7u=WQeY!?)o{!$**qUqs5_q(uomoCm9w0gGZ|rcJ?c?gvuLXDj1S8TL{ACp zM9`VI5WjF(rs%sNfd9fhtqazrQc3q+rw*!ZzLj2=lB}B6_N&-4m+v?l&VyjQ+Vf&% z!ss`&JdXs9C+r`mf%oY^-p0k_bPOs6Uuv$O`@7?KJQ-ZG9BlaM0o(Vz6nJM91sFw$Eh)5!qiw!m_R- zN%!3Qez&PUI9;l(rJuTQemB~P@}{%FATM0EN>L&HiNon6&UD6EZ!fD#>f=UzTX9F= zu_m8&)fTBV@VD{&y2+#s550&=yN9A)ur1CihD{Atz0)7X0tBQ_SKW*FSh>e66wF)3 zX0hH&QUzw?#UV!zWwzF>LkL@onb_Nd*|`POPY`B?HIufrlk5sBuvdjmIUo)yIeTt- zSDAHkN1i;J^&B|;mR~}avh}26Tql>Uab^ALFF$Q)`n^xc*;&5L4%s>jolugN? zUcJFrUus`$80pHuN;}Nk8S%qKf_yapM63#L@VehAy6@$}WLj32t%QDwOQ@Lzx|P~T z%06Q83h+^4L1%5%a-whEjan97Oz@A&;OKMv@f)$og8{#@?`XyC#b4@3Kgjv*&vo&X zTWvbDKjOF8ETm3$SJ(VmOq`1ATP&q~!fT221}dRSmhHc?ImmTYkQBU!^WP-hyy~_H z{_1}*U-&qWSFVlE05E}VXWRcS7igUm zWAjsz?!;bVZyG>BSe)Y;dZeLV#D19JbkV*;`G=h2&k|Ih1c!`)XB6GdKHSAeUfslo z%N>OqK&=*&otF#x>U@!P+H{ZOQ7QyC_9O2M@H>UF&kI1T0%1?RlWY^C!^DL+2k@>>x zsyn%+bypeGLo^H6KVM4Dx^er~m68vybXZr;in_~TE~Z~-K7I0rjZ%y{9T?5@hnM=C zo*l;7J(SKi%xvBGhVFK#qs47y8Rhn2Z)56Plh~{j8cB2JCnMFDDj03;e&)AFv=C(! zIi^Y&HEGnYHO=t$m?B1|g;;FJn@Dr;A}P_=RR3s^{71ozJtO&6NiB9OYuNN$Ldn?M zkDuE2z7epAyW9)fR8rgpf1J40d|y4bd_XaL1uBYd9Ubk;#$*9FRjg z2XhCZJ$r--p|;IMkAhFrFPC$FQLSgqyizX(i%WN+T{)=YRf^QH*CW#Vil6-=I2}#? zK=y7e>8XO>7kpW7MTehSj#+Q^!I-~)IXnCnsjBawofYP?N2zI!1dbHFE(c@GA&_-%G-|YxEN?9$x&w`PsPLdlCEu#1J(iT5)+PyO)`gvph<+N)S=Mczj zw#L@!-j$}I{8K-kgO10Fjmz}F6?gmTOF*d7#biZcR`%`}rE|XNlyRVjo=K8Idmla6u73KcWV~|4 zT($!ac+^jfx>LPHd^4;`d{8w|@p_b9tpfsh!{2f~JYF%5Lp~j$;uSi69G6&L{#LbO zwzKn5Td~FDKeu#`MnuOfWRmDB-uTGvtok@cJ;-%GE{BQu`4>lWe#vQdI$f2?3HC0I z%VJC-d)~?plTuRsu_1W2m!Vt~BV&63@p=6Z@k|*noi9OI@%j|T2E4wX8+i_07wae+ zXQ6L~l!Q;^qksBjboO%_e-2w=) z80-g1dbh zn~&W=f&G%DP6#GR(D4Oa(pPnQd$y821Z=*I%zyGa5`Anj48WMPqFqv~q9dDH!hL}BD2<#T=` z^{HQR3>p=Y6NiOWAl8cQ_`%F?o{X192<4PHN8j?c0y1A{Bldy7=bkMxht2u8;NFvd zCTTypJu)|~YCafK4pYkmMED3`4wLurvctf z8Azy@Y;-<%(IT;%(Bad4Xxp?ai^|A^T{Bg5H#|Vn^LeqJs6flXfEV7)Gnn zQCGkOnR@qQ&`pmEd@*LcU8RNezARB#1~seuW}VYSNO6lxMmt;&;Nu`BAIfVLf_YVl zJH>BUiuN5n-ZRS@(Z%emsx--)(U{E)6_fVVM%t6+XP=T)?xW#vkIGlad3^)TgbK_y zeRB9pH}z1G1L+KVc`sfE$89)qHoRaz8EU!J5&-Yd=UZL2FN9zM%s#4mJ*h0c@2LwL zXeY%NusurSCJ>$yy07R=c%EQ1HdjR#nO3@qr8qm)bv290LqkFO`gsi-yYGo~qZ`@g z*Dx9>3l1nie)v&f8J$J%`X5x1`5c93p#N%G?iSNmF)F{84V-`P00Z_Qy^f zG9;g)o1VH~ zkR_Z$d*ZN=p^+*5!gZqr%PFJDA`Y6j7Q3m{&Z`N%hs6hF-rp4Ssx~{}k1J%!DPRR~ z(^2WJDaXS29`Vl!Mu&~@-mcj@8xr zT{{NRtmVcT5Hx8X;JfKMRbAFnT=cfr=uxg|aLL&NtDipghVZ-jqMZo5n%sp;l{@pZMg$uC1Vk7s>{17kYd}$1IN_W<7^+v6v7!RwNayh8WZ+obX95>EfQuJD|KnLs;(N8%?*ip; zmtJ$jlSf)xGXADDM1Id2`_EpGYCahNdFVKhEIo8=qP}eoq!NU__iC98@I}{`(P9F^~K#lSdET>UW|L<%Njz$fXBJ=vS8ELa`#EVl++aM5x zFo}IqUyH5IP;)^&e7%aRo;Ir+h2#+IJk44>`SRFl@8@q&hH$|6{*nBYu0x+fuYC24VncpaLHs{3;|4Ii6`;;crI2D(QFcXp!` z%lkL?F1@bRPevsddYFA&->4y4V4Z_~$dy#Lk66gB+A8-&6AWK6eo$-Vb>B({tx*Pk zS|Y=|c`7}k#Y8*Ks8)F2yWP8~7GOpyzqdYls(3=Iwi5X=i!|&wcbUGAXwj zr$qTgIGaW4YbAREmukpf;uipef>}O|@^EH{YGKa22yoFLYPZ(S)#_5o#ArC#q>u3^ zi*EIyu7S1BjM&~zc>(xJ7_BI^f#T`OEFpDeS?bG%wt;J*3Q(Y^?$M{}4o#1!&LP_m z4_lgB?&p_Mo_m)-hVUsHKq;OfOnI70ziBruJ2E)_*602M)A%BU`Nx3fPA+Ya1c3&R zNZ0E9j4aOyjp_$O2}ulx{nFBpD+28Gbv?1>8lzPmS~}JAuGZxo5~{}?T^z1gcubhK z;X*_^&m)Da)ybXleOJfpJgQHYO!YF!)~%dbcZ%K4wn(lfJrk-AX0@N4-UJ;&H?Qs` zoNo4BwX(a`FZ%Q?P&%2DMcqgY!fUy~9SscstfN1sfJ-_igfFggA*R$yY&e_t%p%qz z6X0az+V*={rSsigmg7rK^cw0@*MQ#EG#ok5wR6DfX_Wg}|TkMz3W~kSK@W{K{%^{Mb_i4<7-q*{gaVr4&Mb8=s ziw_OHUSnD6#vR3L%9iGpJQ;o2^|Ww0<$={$`NZ*yg4|a|2WoP&EkY{*B!K$Sw}T3@ zjY=eWSMRNf@TQeO;S#TP|0Akc5r7d`(0ZWjCN?ar_M3-t*s!27^@4tRj8BBgUbovo z0~30@RtaxiTuxLZzQXr>dIGu4a^w_IU^J@E8c!K2sZ!jl3y&Ko>z5*@ha!bEfU(Yb;)5|(TBy2T3*DEquc`C z_CbDjPg@GvkK+dkqI1*J*x@RZU|$q7}vzKngsW8^C2Ow-s@FwXDBh?C)dFk z%RQ(al$O@d-XZPz&GQAGoxighmH89K5B^n@`#k)fz^8R{lYyYeonQPT?{g;|m=;V| zs8>QYlcVOJ4`*5E;H1ZE9pF1eSmsMC^1bY;ah<-Q48$wqDQcx^0904N(4k<&NpXKO z{WjnCFy2KwhXOh}cI14QK~6AiGQYArG~=NHD1>g!W@WVb`rb>3u2dkS*IVdgdB^)J zy-(B-@xhe{o@o6H+;>E7Y^`QZs#|Bxqt;jE?7Fp;G7&lkC;7k^1FspX{lhaj*I4SR z%>q6imqs|}3!9FYV&xE{MU{9%ixOe2qLOr{9$cAJ_k$?Yc#Ek$S&)GhzBOsA8uo%t zC;RY?$_gn?uYQ=Tw|~!#3uM+(P-@z`A}UwDkM`RhPtBl4F{$-lqDoxE7^?L5;2Nz` z8Q}&@4zb@mYtjCXovpL--wd0KYZ~0hwoy~{h+`DUd`YwKPYt)Id3%_-`^E-<_cE2s zB2_Dh*RmOoiM0EY8yoJ;z;>$d@dF*BNiq_U$6Y-6`s6D-2@-neHw(9i7ujp;Q`sEywP_0-)dt zg<{OQqH!ZP+!%Z=qS#>iO`dj5dsSj(&mDETKy~1pQ;B?FBRjF8%+#Mt?R}fwAnWAw(eVUr zB5zSMZ=N@3o<0O>QlgbC%h{V^uz%TN#`XZK>oEZblV81Xnf+eK;dA-qt!0%nRa_ERvij@8Bp$ytvMG8YPYpY|%v$t~-p&u1 zY4k`ww;41vnztjYb?=k;t*XyHPT~!aA->1mED4E@J-0#z69`C2&(l{cS4nqt_(vkm z;*#aXgwm;okW|czo{a+5ZAN==-r3a|4>u^v8%Cp_oT_rh0EcFQ3T;k2Q8rGP#C-}B zqrSZ*TF6GBSQ3d{t*IqjHssYpM+q%74Nq813JXQs`On%k^)-> z_uHD+_rI!K2TULESBQ)ILBePr9ul+U~(6=ND|Ar7vN#=LYdaoxSA?KR-h} z3nZShILhIaY#v8q@mp)&+=-gX%fi=~zZsUpcnB3E z{rIuvt!sP^aE@gdg=$d|pN zZhFkH##;Vx5|?wD(?m1uM=qao5pLk0P2^!!-$a&ssO&_{8NI|8eTOUX<-c;jKYQ_; zmTsM6uQIQ~?}(y|Rr|So-41zlp&**xz_P2C-Kc+od}r0Z!b zEiqf1hMi_0<}(w$V_YgUck+vbjqR2a^zbN?^`eTyrB=ds-!PJ7!`|)C%6uzsa?=C< z-McCKZDHwC7{!xXFOF#mSf}%_V`}m>r455QYPsFXG`arHqKUZpZ2wMoA-2K17c-PP z%e4YzC2x|7xl=0wf~5goWknCPWgd0})- zgF;>6JV?H9ahT&7^W?6l`kh}d{G-BqrRm)fr7IKEZDjpfBmnyA$2{ZEX^5lSb^{vxC||JJNXs=PthuY8ln;{@Iem+{X0Ul~*Uuv*MdWi;k5#i+{6SH=*W z&G{RzIJic4M$9TBwDUzZ>JMB^mlN`e-2H-P7>5Hh8$Cb6rD1JN7dZrMH03tFF3jgA z^I!hP+2TSkNKN(&w7Z^og8=?}KMcQk@aq*JT4MdWM~In7)!LIns>75pURI5JAvc~e z-}BRlpXJ&N5do|=aWqeVP8FMBsp_(pV=bqGnl&s9byx3I-AH`%Tk$aarJVoLyPg9w zMm`*&v7`Gk^iqVTuD@1%y6$ZMg+!_)9QSHsaPMHa18WJCy>m`aUpf6B^t4mI#lHxU z{hZW{owcPgN!Nu&^|N!cA0oeZK6>yaxswpTBD;L$u6W*_mTzIE-7aczH81yltVdEE zKZchYQt^sp>KRoe-aog79zh+vmKQrzkQ5%Yd86QKZ zst7uqD~y`>dZLwIQNx=!C=(5FpBZ7r%MUgN{>xmid8+?(7Jf+s{O>=F+X+*Z1Wl9~ zuKr?Y|1*U_DWQvfLF7Mw=9jC9gTG7Q>i_R_3V+|{A6xr>O+)eby8XRwzazlE<=}5Q z_*)MCFkXM_(f=>)b{rw#`Y$ekbB_GE*wgPisF-fblRol%lju|UMK)RGweKdmI7bXS z1EUapL3(Jmewy{I;X_Wv1g`Gn>_R6q@LI6Br6Kv*8KMHl{P+)&z5ng-{CT5UF#<_3 zT1qjO(jN* zl5h&{Ph7lycC4qI)=8$Wx_%D4Ja$e!vZLwLx83APr-k7Cvl_l7)h`^%e*_JiLwsQ>E zuY%utcNi?QBrm7#8lN;8M{@4D|2QK5SXr~ImNDGbZ($F!2G6&`*0LLud_^z_W#8ai z%I^*%rnS4CqTWVz-F)X-1BbFs^idI65bAi43$bBY4TUB*%LDL5s(U(H+)IO}M2BCU zg0m-ggpF>#;2B!-l}jsE;bw2a*|1_oP&6R(i@ zD>%xipRL+!>S!j7S?2Z~^XF%h;y7imr({bEO=Yc!aA<(Mmt}*Tnmt|tdwZp5rE?@? znDswh{tvX*!~}OxJQ9+vPPkGVT4|>M1Y^YB`wE9MV;W{ zL-U^0I~BQhMyalXl$Z9hHCG4g#8MA<7A^wc{EHszzZ@dMampgh_aZtzx9JxynnPPU z2i2{3hBFK;Wc)*^k!B>#-w#8O+=YsSJ6|g-=Br*@E3p#Ddm^bWEG*pB)1&&mZc|rh z_O4qm+(-6GN-ms^R_VmT-uzl%@~@EfPsvOCaxv_`gDw1LCED0{`XK;^vL2*s7h7N`@5A{RO6SH_{U1(0s z7OCvKwsM5$n2Hq^VAE4bpzab5(tovH+WQ%%)VSL%(1>(UB+AT$EnppKQRAi@&IWRZ zH>F%{ad*Yik+eo5a}S7iVpDjPLqm%-$JmNNuU&Pu^;)l7QAYzfG>n%a3~1rJm1N(X zYlTHoH^Q^b@^;(%^BrHl?h%N%b{7%aj~V6$o+)`ACS(-7Ot4(*NC4}6Uh4Zb#`Wi+ zX&E65u@D3l$DXGS=y~PWMwHu|XZrO@CGo{EO~a@H)Q+0-#Bdv7 zq-VUjbBzUyj*GSnOa1PK-N4>dFZoAv?5%Nl#9r0q@8*wj+q!XC;p>5QA^=j;o{^kF z1pxksas@=z>3xIQ=qp01sd}O#qR&eo^gYVIXXD{?+#DBR1g+sDs}eYEGBssoXQOFg zmQ*!)Mopcoypgt!J|*&8^wX^jzGi4Hk3m~#Ihiy%b`?|{M39@305~So-)<1q%}x1g z*_fn9PCse1DkvOO_SMNQu`5t6)IOy{}I%iIkMRPiRraSY74r z#s1j*Q%ID=?~)&_6EuFOLio*f{USB>jT^l~yw8UZB5L3CP~1uW9`;0VdY(}zz20ty zQTTFSl1C}98PY8(pW@_Jgd5E4J?t`w3Ybkf@SLD`UTODCqO%N$R|(h)GPvPCdr?Ow z?4WutFrK#nGE*Z;xB+vn4$5}OKo9@8bC^-%;eXJi`vlfStiskWq;=_E-FhqXcrMNr z5oYKlS*j+SR1t22FW2t30yo-;43H){Ba-qMtI*ozq-? zSoK*5)n)VhlNqklIBX{!+F3G^iHB>}jrndRkyd7YdGP3a0dT^l z_d6HoE3$Ehs;Hf>ECoe91KPgOWZn9<2yP?2Yz08hmoGd^zV^019abVTd(R5uFPDGr zQVh_AfCDA?U0+D%fmLL!0tK|n{Bhjvm=OGNm^TW zK;9mHb*imcviIfd^6n0Sq;ppz8_YjqWwl7E6*OO^Ox%rlaJ_FP zN7e0ph_Z4+p=u0sv9Dqz<0wi>%3?aY5Lr@2v?u1J&w81w_~HpCUs_uS4pmylS6R?X zHyfX=?CVgQ>{czpE1%isx7CtP1Pk|uto0MRfsn3U&kf=pxZs0wEB5Y6?tA|9+eD9C*GfMx_lY>i$pxJ*FipEIOe9x77(p#-Jswa z5Pd+qj&*4s)v|k~^^B6SteE?6x0E{k#2X_@>t9Kh3s= za(7;ne`9myboo<|2L0VI^IZBtFRClIE6NYGJoONZVmzlO-sc_azdrfXz>6_kqj+{} zhL^u^hbk7fAcV_tyI@Q=y)vg-Vln`+M#6fX3jhFN8n8|1v+Ymlleu@f;j}gS@=2mv zmX`T2e%0KUfcy?c<`j@ttAg(5#|h?EZiOb=6;ZC6HQ1^jFPDuZ9+W!%&YOIXTo?p=R+gtsq4}UvzNH|MC*$9PE5+R5p@d;hX`+- z?suxh48R-w{rIWu>K>mJ{z6-P{oGiub5#t!<6S9Z@*!_rT3SZbtKgo&!NI`fJJXN< znxFf_>;GrY?o({b;>o6HzS*Z=x=?1VuO}w(>JgwCVtbk0!1&ErMqq(XW~LOv#u)n%Utya+)NB0 zLn-Di^s5QKXFfB!ee;sZ6{>6cw1N0ii_IA;R98c(=a}DLHN#(J>s^b+?{WILy4dsM zm3p4}LAlf5M0P32Zt>iuI%dzODb!nL;TLH!Z%Dq@0|8q2CU+;MBQX`+&D+#4%B2- zepAz%5)jAw(Zjj6FSX&AtdZ!t1PzZfWa1KRRg^C8M=Ye4y$ZH>3`tCt^VoAi&4H0(2Js3tD`lq?G1Jo_9KIh{4M z@hlzo;J5$4pZ}2M`k5e&97x*iyfphI-T&k16hMzr_jq2Exo8V0gwp4k7T23py!^cI|*+8Mw zuFcC8*wGQVfzec6bQ$GZwGCSZIx1LWJq*9YhV{-_jcl%v_p(a%-_xba5kS`|NOpE= zwOdqsLQa03^dIg1%%N_wRA`fh-Lag2>j^aFZBkBs_K&?a<&4tZ9IIlh+4!nyQ@_b# zY9=2LAZ0RkFvK}Z+vOC1efT9#ZHpbWELgzvh}ll%D!^_1=zyiJX}^u$>nPFC(k3An z%Ic2kJi3{fBM>4G{cn-s>@k7<7kw<$zkBl>cuf4%Le3{{MF9D~gYqbz?7ty@^e;;3voxeG@QafNYr?4Ie%e?HB6QD8^O z>7cS{Grd}=6-k&IRI}RFmQC%WiC_WV=z2P5-#8`Z=lWpN#Y)UQ8+j|2!)B1WOUq=- z!da8=QvL3p=<@LHJr;~|#p{D+uv;N_bf4|=2Z9Y3^w-q1Iq)}^N2I69*<@KF4y{h! zIzd#7itfwSO(3j&{NvH$htprF3wR-Ke`#U)i<4~3OTs!g5!`8g&GbBn)a4Y;i)niE zLv=%AU3EW1v-7D31O~rLG8BM`L=ehJlwNd3TDM2yyt&9=wyF4EUWNDnT%K2#yMuh#w6=x z6#zwDX4an@sdB+<%4Mj9=UAH2l=0ZR@><^+DhtL`@{b>F+b6wT8n3|0fl4l?-ULVl zY|*@TyWeP^z0NpQ+_m&_t`qXV*n7{grnYWvSP{E`NR_G}pdg}D>8J?OrT3x;0V2JZ z5J3eMX-e-cl+a7)Bp@nMLl3=54Iw}XB?0oqectD6@$Bb$uk-i2-ha80xmMJ_`Kx7;ng>&ysPKqP_~(9^cvHSf-ghrsC~P3#q>&C81+AwJSN+jsNS>Kn zask{Q=d?$e> z*nOO2%^~`IfE4bl35~3+qiGnzajJNR^j&IE{pw%kETAM3ixcY4R9*9>D`ga;PIZ3O zN2PPRew!oqP2^lj=vhs4Z6d&9!`xX}xO+SA;sB(x!gggfWz>W)Yn&7>TzAqXumWWY zp0&gW*jkVzi%ZUN+84d-Q5q7ZIRnvf=h@PF7T;fq>(Y9vecmz0#-_0O;A?{qx;so@ zU$HZyu=hQ$8|0>dgqAT~x3b@H+wr?{{B-%r&r*_#p7QZ3lM_LSx2;ZBgWSryBE_EK z6HP*fP;_h);v)yn4i@B+b@RZ2tG?>T~G@#!eU8_E_HNkaDH;S=NkM;?DGDf-)G zak)dLKli?qszEp&NTx7R*qfN8kezEOzwzP_^YOu%5juNVj>GE@*~o`1w=NR@DGT?1 zzOy=WPQ9B=OeAAol=k;0BbOB{UZ6KPc>95OZ&33Lt9dxU0NWsGwTH4;4@3tO*G{Bjz!e^L?>vAcYo}D5Z6EWkprXnqv@$ z-^!T&x3^x3CsXAwq4slYjtyy+yg8#|;#a-YHeR>pUKZ(b{`jLFbA6wm?~3q@vOac$ zW7Geq_nCqrr`%I9fOk88*qeVE{P%ZX>B%JhyfkP3!)rJ;x4+(Vj~Ce@^z4Wz{wGoR zpO-!V6@h;aef__BFhB!&K>@_8S^w9jRey^%`P02$acz6+gMYu-{-+cNRsL$eTX}e? zPwszpSps?Gzx|xOcFaB=o6Y}x;^xsizz{pm|IK9z>g1JwzV&}^c=h|!&AeoC zNySj*{HGP1VPe)t_RX`nHpC<)LAQ(4A&`x{Km>LAA5&i{JwcL^10!W+k2?%Kz@XV4 zfQP?EwodrN9;jozv&O71=3d}ZD88!+oc-P6v)ha4Y8klxH~aY|u5XHz2j97MlmD-9E zb%tMXs|?QPgMeHo&<|oOj>kHClzZ;{@f62jMYH5JDp%BYvpNIw{imhU)?^MlcGi{Q zH1lLn??X9b>u=>94G{x`MC0Fegf>|qG6#uu@|Lc=F zw7ECha3Rc91eYUd*lmrD1&{eH9wchGVvSVv^3F@)glfSRn-64tt zybtv4EzaMkM|KTVDG5?w$M|ITd8!C9r)~`ZQue$l4SH@$Vz#*Nq$fKeHDZU#uImN- zCMf6vqh;81aS*fDg-NxySZVffVH#f-M&?lky{rv+B zneXtU$TcE_>xP_5^46^v{VE9em$(GG1bS^w*IkI)K(QSTIF=Nav`+eEJOA)Uyb#oD zQ7$6#=%}=t(dAN|H%Bcts{fpN->XlQVp&RZpF;0|m~^II)rAHE)-zA06NA;l8?Ku3 z4KFO*me>-z{E|*gPCG6s>QM{NOBhP zO<7NE)lV&ppdztNrg~Wu+pbv%Hb)VmhO*s{pkm3XlZUAozix*bNt%myi215qX4$mk z$-Vw}Yj2yih!~m$jjT zo@K2EB@RvgAk-({T*Uxj~$YGA!m$=3>T?UZw>BBrAV`y{1GUte2#g<-Q4xkMY??4guQJQkTas|V zh9ReJ8kYJOadz|5(*^x&GvF;6??5ff$-Xre^Ky(Uh|mP^Tq?=Un24>@a`1d0qs@Gk zMKbUy70IMO`Yij&8b_1)= z>28le2*m0_o>~X7oBF_-1DfL75%k*->been%f?Zj{BqO(7WuMFd+8_n_XHs$?XGKT94vmBqT z>Fr%l?|g^H5A5(Eb-E-U({Q0{qPodlxXiDc0UNf=`nX3m1(~g5AL4i$mz&crbq_`D z41qN~1&`2zE_(Zg+Z(b#`mKb`8_etWC=t>QyI4)=glJm!;;N7d1mj(*U4o@$STinB zPJ-^p3EVs=i5h_| zl?rYxDR#)XBO~`pAd{>)RPbs0PrjClaa-#eO)iS5M3!wA^GdF!@9~DtD8g2hw@N>j zR+&WS*XA>x*44z(`Q})JO2^UnooDvHtGlzLQ4xI$LTwR*0@10sDBAr*T4})iN0?ZSa-sM;5 zhL#wee8&!*gmb%*$2Zn|s||RheCqbp!>)X}BOYr_my9$pf+!30xPiSQ7*6uxN_Rgn zXFGUbUD6=(oa_`i`Oi~}iISesOmT2fE){z#`g=`quCJ~4mzX-CN(et5xEwWVz>;A# zB6lm2y}@gw9c-yLIRyZ$nPjD>jPHfr3wTQ76v0RdfB~L%#CKqv1=-|`+#bzL8b`2@kM32cCUY7% z#yR6Q?QnM{;l743*X}!SMMv;DP7ywW*mKNGa=03D7}=N4gIxBgsX2#{YYB}{k(B9S zUSzQR`)%&pH$O~UQ|0>&*T)#d&h>N*jg>tjdEMPU$x$Z*bbg5PtU%ss1MLp*t>0_u zBp?3=XgHNj=7J8jd+gshQL)+e8fKIEkK4z`*TSwnbgJy^tA2vLOk|I8Ri~mkGIzP0 z(w+teX|p|WQE5CYTg96xhyKCnMQ`KOC+o26hfdx7p_{~D`UHxc5BM~3NncA88+HM!iz zG+ORYJ_6H)w{*&!595d87ql$1_uVI@wzjnNfZ&yp;xUB3YUH4=@;$eOJDZg4RmOEn z7CL$7PyFz@lq?#sXSHD`&MuVrs{8`O{UdTy)O`WzULHi(Cgi}Sfn%i4PGW+^c%@{V zUoRoGq%TMlgk=@uz27Z0Q8)&k$|Gn9a2>TGn!XK4eZFHE%R0Q=p*A5kCNHtNUE%dM zjbX`$+jbBNHPy4IH?qg3_Z|Td#b^At^Tk1!hO_#n`fMq(EyZ6ty2oO`W*}0XDV5`t zcurhVe|_@SLLzuTomFD#br|nc!n-pdb7PCT2Zbe;MbXo;rOt!mNtbV6`zOlUYHY(l zcooX&S3GSxn>Jh$VlaL5Kn#P{Sy63!B2aSLSUKf!z)(We}IBhP(bPQt#J02>(c@d0KTB;;nspW8w19s-} zGbBq|7OxO2_9OH38``#EcEtwLp*N~bXN|7P<~vJ<=vOGgpoACYiNPp!XB7TJpq&y; z)tzuyvSC}w)`0fh8$)Xg)OmuUlVS-!KW=EzU*X_qUTIP^y;|!hYIcW8q^HB^2Zi3b zC{IplC2MkS7yn%do%9&Dz#)~DepqTEwjZ*D77Mb$$IEA}_RrkwFPaJ6jFX(_d5809 zhk6n!SxW}Lt#kVkXa(MN#+MgspAC&a9d1xJQ_Q}d- zG9*SUIhsNiEv!7Gb z0^eX{8Dr?$-3NIzLu=*fn5jNS)XD(r_SQ%Eu0!X**rZ)rcOnBp-SwKCa8Arjw<1yQ zJ+dI$V%qo~H?r7t7f|mu`%MZ?vd5IAfBvuH2J+GQCHCG!*i^Jy6MMHPHotV4koDH~ z1Z-Za_W*>8*pz?p`WA2F(72Oj=YA*3p*#HXqU1O?iow^FUTp4($mZ@$OUPL^gk=Kl zL)0WBIi8Up6a35*UWTF+^G(MOA5QC7lx|JX7mt)_t9?IH)ll8yO=Xi5P;+xy=F-Hv zq)Eb@qfQp0;l9+1Qq5_RAwwH36mE3m;GM5v-6wy`5dim=YHzHa(k-}+V;!T=lY#fG zM0~0_)sA$2)6@YCJxY#V)Nk1XuKdMBK6Tg6XkqQQGazy?lsTD{t_LroPWn9`L;haB zIW`Y3rf>U6rA>GwGh_@?kBJM1lKKMiXQ^Zto=8lsHV1+lMmgP{Zlio&W_s~KnRu5uj zIZ`^}5>A|$cC*6PoC>yK63qlWqj)-6yrY z53H}y(=o}?nmVwT27C`y(=*Y5_Uqs(UnZ(vn7hPLuJ!}kdI2&j!@Cdc@!(i&8J>jP#tNyd#P>zC9eSC*ahe*%| z`&;%M;w{Oh-clvW>r0;Z(}08O&vtqj{iI*<<~{$sZbU0XR=}tbbk?N?L?>d6D!9_C zw#(C^-mPhs6J!)Q1#=5-0WK`omyx+!d2V%5zSo~NEDhxh)DuX>of{X^np2HIOKGRu z2?{hG4e)mLHGHg+%PTo|%EI#?eF?LM0&l7MA2xfR+ATkCKe*%Ur0?sR)<4i66Kh%| zW{N0qAYSEy4a&01RpgkD>6QG<_an}aRoT8>V(IA+>?545kydg>50yqxMw-RxZ19cg z8E39KM&^3~VR8F}*tj%Dip+I!-PSW{BSMWXU!4vxwmn+3)s?=Az``eNaYb#}xLSKZr|hS|j7Qa#rPL8=BtPhI1dTEv{> z1RgxZKA=0H2OtGTL!n8|4&lC}T;rgZC87)^`NP~1yS?7YMblT!Iz+JbpZZz1uM!!O zv*sXEyVDgmJX@mD1=Hq59FL92eRUj$QkL-n)iUx~=4$O^3!4&su0cn?{8^swq5(O) zL^Z1QcjI+3izA)yks4luSB5H!Gqb@sqzUSGCI_2^Ylo$W(1CmSm8ig>MXIvSTgK~G+rO#qb*L$?Z3|x) zLTrWm*go8-`l&O0SBRI{q15^74d9I$DzHAsv%!YjpvACGkPX%9Msiu)Ejm*TnNZAibq}_>)GAykFG!5)_y`*PA-5>`9`X; zXXB0S2jXvw%`zSF=(1uhOM6F(gMsqvT2{FTHun@utdpTv0aShC=~tf*6I#pP6;89f z?>mjnypCzg`_3`+jU^Q`1^uLOIBcSWQnKopD2Z=XHgQWlk-L3vNwW5&tw25}yPw!v zu6trMEgwrhvq5LAG?xWRWnZyC7;R0^mF~%VBoE@b;nArsJ33bzX-H2XIKd!dm)T6q zSA_H~FD7Sg;n5VR*(Zf*Vb%^~Hw86ZYCOR*`CGkV(<~r+-(}pe@mn>X!a_X@W|`nr ziMML$MxNQJJ|4GS`f})R54nLqrkuY`oy;+m#5e?(>8zTNe>kg`VybnK4jnpK*UK=_ zgB?LS0~OmeOc=M;$}4gf82C~5dadp5Js4Nn0ac{w7TBP-Hwas0R-?h_4vdvH+D6RD zCvio~`knR$k3Tw+fwU^E%1Li}!tIY~OB)%U}K1xFB53!q-ew$u& zDWFf@#|Q)unyg?HD&LK)I^@5bG|-E7z8zXIvmP5W=%YXT&WG@foS`+$a;;}vNow9L zp{-0~mv-O2E@E!KH|O?IJSJ=KoQl~+MoA^(tMbj(J{iiX7$QC!z_R^Sf@4_7?+~`L z+Pi5te*@xBa-PG!6s(|ew4JyxRwowVQT?KXD&G32Ybws9xMBA^UE>J%G-S}BUV%Sl~%P=?Y)hI>Re^dJ3aQ=BAuj$gX^5S?oO#<#NqjMWpXul zy#GviGj9AN#oj2I!dvYp+dMXP4bHcd@=2KSg%Y(}gHPI;08d(5v$S)YJw0YY1a9nd zL4;*I-ud0c&di&>nk;hpkbf`X@C!+S+%C&0`j+%hS*Izq?MK7YPL;DW_viDARgS#l z%d+w|6O$(J0lDd%^X6*rn}ojfC{su?TCg*Ri+&-=Mk~Dk9Az_x#jDtfJpMfe-2Cy% zyN#uZt3!b-VZUoha?19V^0i6z@tVbmEBe&C%Wnbc2IjiSk8(|6-hpZ~@~|n|qWSx5 zRjb3Mhe6j38DE^C=nVd3%nBDGG?hE@ZAa`9fg(jC&p}+u@DsTxkNmdc3A6Y!7T_U? zE9$v%W)>M?VH#OZIEWASq{!n%me>R+LE5r1ulaTz!HuX!=&K1`J6xU29(Nw48&2tE zFJ+Uu^O#wOZ>d5JMZkS+oKFFO-Pk~$*x*#q1 zfcRQG>#_X(E8#mpQe?XRyq3uzD}-Np^<5#t-tWuDDEf0rPrsk#K4Zh{?)X#xAQozI z$SPLl&nnqJ39VdxCc#4V`|Y+5%->1FCznwcu2h1QBBCN96b z#13ks-Aiy)>5`tmqIKgVcExQ@wxMywSY;y%FIIwZN2DDFW}8T{p`7hnb%S3&x#+xy zRdrn5F_TYQ(MBcQx+m)!ocFk?;J~dRr$DOa=)#qiQ2A1v`!=SA0*OsIoy6HDxX{IE z57HsXok!j}ysm@hg*(?`%Vc$uc-wD!3Ak0c1l66(d+~mU;}SV#<4VLhu$2aiED&9B zEZr=-Vk~A}4-pM-s%)gv3)(G^fSvt&N3fV5M6RD6H>wgVtjhUZrg5yJ@1G$_9{1KE zF;6u%0ZB*xvPad?$J3cqCzakvOZ0Fz{q7W#Gn*UdYx~_`&%4?ER@+mdw_P=7QBVN& z8+A+O$KCgy7_4FUglb2a@@EBaULKDs`J_WyR^s;Ce~RF1K?^m_X(SluK3*j5p8j?e zE#-&bdglB}eM#tWz6G*67Ad7fb%CA(G8FBl+#WL+lNG)C9_~C-{JD=NEn7K+S}xnO zW)dj5upU(F8w4d8=YQY)R*?w$zbU+hZ4 zlUAjC?;LTMA*l0tj?_-$5AL#%<|L>OnJ`NIMyB-&#!h=Oq8kcturTifpgn&+aSN<3kot(N!N-VATpjyoDTIDua?o@1RNtYl#A}I@XHyIps6ZF*Nkvxf1aNK;Mn#CFP zfdbgWuS~kUbj~tpjTV(+qojV>9&k-}3t8}Ffv+6NkdP~B=kx8xhmWa4l>Xmo2iO`u z?nOvVzwVh9n(K6=P&$<-Xv+u~y*E#neYAc>=fFcwcYe*<BM?U+*OVstqGy4rfb89rWGLGH16BkEJ(P%=l%x)^tf z&sn|!XX?b2)7&4U7-NtxKT3L;F{3Seu!!vWh^ij!Ad|U!EScQc)RoxD80?e2ZcowVD87~0_{b-`ua*tr(RDF?;TI~|W2G-jv|JmnF?_;ZuI$)h zWHe(p=}}ydS28R$AGHZ%8bL38>*zfxTbA8-kD!}4e7LX#tGbJl5J)t2A4`I@d21+% zpHDFa3LNW20_md7GZ zfpRupkOIL23fu;C8z&_qzvS-NSzAHSBmg#WCL6lm7zqwK|M~ixH#b6VWV9%LJI7M| zY9!cER}A*4zW0K;S`7_M$POYXC!pn8U|KG;{eW|hEagyrSXU-07TAw;vy8>a#90k?wJ_KG^ z$Jh1D;*Mz9^z8W8kzu_9{xE=GC)?ho9>Kv^8o?ulQ;DCSH`e%@$}at03e&tyUk1vS zlRC*P_Qx^*6UHZfHt|de3iq7{wO@jr-TA$f5~Dk7(iU1;yFY=H_*ml`7M!J6Mvw;e z#U}wrHvGb2X+uVB(>`i=4Hds`CQjj{Qv*!^hIXugGPTBzUZh?1oG5Xxf zO;5#}l6J;K0q-+M%WM1=P;7}YbM=8uUVX6ZLjcC`!Dwndpj{m^R`t+Swgfh|jnyBk z&b@J2eE_^0^2k?Y(Z0@iqF=8VI|)-cQZwF9*EiX2&MGTc#KO4+wQ!7gBqB}q4!`bJ z6-+R(44Y(j97;I6CpT`x?kc{ygHP9b(;-sY%h%PBH(t!-hpzO4I3{jcY<3^mRg{=s zHv2vxHSQaM@to+*x1~Fv^<6NoNuoaUg((Kqqlosv+V)5i1w|<>_dVAKA_WdXUiK^I z(?eg%c6&5yWcAwQGiKbUVmY-#$heV*AwW{CGKTl^I!qqEEHKj_Do@WcA$<*1&H0^{ zEn0%19~pAWK@?S178!ptr{}&r9f1hq4`^%}^}aDq;n)q#?$&#rtX3|ePdzKyCwKM{ z7|x9C(6)d(PJmWj?IxQJM|tAzY%ffO8GylqlDDViv#5GIB5}B^Jm_=TVZWK7nWe`7 zLzgRGuOF~oH7>;)l~OMJ?rB@yJ#EC|c^78?2fj69nzG1d(jXbai0-NA8i>wrc?z>@ zG0aRAZc$h|6Yo_Ncu=!-=Ov3t$C!WJTzrfC-K0eEinSoBh=6W^>bP1_(>Cd`!!~*U zVCff(8I9W%Bu;p-f-K!*YsX^ZcHWZ3c5HMBd@H~ijgh^OYyDq@Z7bQ=tGc};_86H9_6^J!&Eo~K1tnF28_1|7?qf`+!IdAjp6RzkrFSB zv0M+|T7Pc4Mo5&u(3j#pwSC+4Go6h;PvU4!a41DV9Aec5tZs76eTFWmL4Nm)In%GB ziNN6od5n&hRc2mZvHv!TaM)rjEK)KQyJIX|OgOU)Do4(5T)MWW^+diqAV@k8YpNt+ zbYorbNvbQDmr`kDX}6$Onqz?y(BmQU2nrszwuO8=y2`Xa&7C3&0SP!VY#INC{k_zoWpdh) z@djVT1Wos)BO3aiOd^{~Bg!v9qTyI|!y38djCCje9rYg=oi`UfhV@sHZTH&^DhKDx zEgMZ^iCkwti|S(s7x}SnIR@cRONO zVamDbq8282ZD`tAx59G${X4-iVd9K+A&-M_ajl|zfO4VtOML0Oq6uJY+-6lr0(?Tp z_TuhnMSEj}I(Gmh#l*$23v0cRRY=< zdQZKs^{y%~eHGbYlg_ap_{~^qc5n@?7GPt7e~q7}rW|Gj%|Wd$ha%!m!mC&D_Yc^& zWs?K%RG)r5{1Rapd$u0o4`a{oM>rOKAix>j^0o&?h&yr&;yv&+sXX-S$1N9%c56Yi zitB!uM(0Wl=$297hHev)ttt?H4jv$YPsL1v;Tsp)Q>jJy zMTOJoT(jg(H>o8Cy+}i_7-J@6uJW*$NQO{fs;JC6uYX zvn`4UFfSD45rrM^a{=8%S^iG;OKbK%I}&3J*OcRz0Dgems)+?)6O$agbP=Aqo4-Im z+0*pVd$`NInTDnpI#pK7-XF4?kf+e{T#%o-Ct$H%g!IDF?X7W-hnCCm{m=2z4fk(Y z^+@7%7=WRP+&{>L=EH9%3kSNA4WRkYo4o7vMb-tq1GK>wjSnS2%U3i+pV@bxwKQDl zX@j-hMw^s*G_4ym*MH0?ek;cZ2y=F zM=WDc=S6$>4ewy0&YGvqDkkm zm)$hh7*L(}Wom+Toxoo_T|Mp&E;*bN;Ws9nj_qoCYHVBFvsjacimZ7_;~_r3rPgrv z`soPO1~|CXt0t-3*`g3|p$ll!E>`c7gg1FP9Hw<-G)_^Oys-Lw{C3cxtJX+8t=K4Q zdVZ{@bW~pdx6@;~PBe2KEAu#R#F6d9wpKOD^|deibS{fEspTRc`e(HF_8?7lA+Has z{idTt$Y`FOl6D9`OF(JP7FB&Is7}rkPtP7`Q^O&&Ycb$7$@b=>LXF0W&8B2t;J2(o z(Z1Q?7K99>c%dcnz2Bk=!brq0z85m9DcPv&?4~bz?r!*f~h{)AWx0X@uV9% z4Jlb(HVmyUC4D+FsaKaU1C{%dH$Na(uZYmiM7I+4;Kk1q zC|i6RMv{>a64QkY#scB_#S=vfrCog=kkk2UZv8{PjmD*jrv+^q8v&ulR);0ZDEkl* zrJ+Cq+2#-Xfw7NEo8(v7sIi=V%69DUvAC?I`T%8VC~WjV#^^4%yrk*tZG>Z-YT)4t z$pC@@SfLJt>>m91m~`quvVQ-vggt8I;Q! z*ZikcWfP?Ir^jl!nSF*#3RUUe>0I5+c|N%x&U3xx#w|&vgpAvkGjVw3uMKdBH0NYd zB3%AH)1Agg0@S3!>h&F^JD!7j_F=vrR5GHl`=8NEk#gVwuig+)^RrGFYe?82UqUD5thlWe6yi>D5^-lZJpe@BL z#iqw~-SeXtzDKWJ5E|FErl529;x0?%DU`6Y7|HQcZ7@quo9`c%mAgC-chr4yF5`pR zB6<=!*@9*F$& zN=R~2qha(4LNh_tg5os2o|wiRV^XvuiukM zcxhjfIMhc>8#OV{3}2+(wY3eYZ)wzexrxv+VxTw6Q_D7wI)YapibZS$C0~Oypo9iU z76{qPZ06>5=0iK^Md>^UCPBrR%yhzE`}GYC(U!CL&UBG8Y`UPk)7DRI$88M+O0Ph6 zt)Sg@`P@&9Vq=zfbvFiMu7-Im&9OM%F5R>d&ksQJKHhtGF^xV=O6^hkqAS^xn;vi zFXgRwJAtxap0*!vqk8*foYI2aANidpPlCDxs0=P zU!uwSkx~q<64VED>+XEd)HD^-6g1j@C0=C01_^YYq;a$;2(a8p!Mv~()^#r}r+9N< z8vP+JIRg^a4SxgOZC;Zcc%(Yw&E|{SZqD8Xt{iarR|`54Y;3RAlN!>`pJ_?V?rFD; zyKH$YK5II*|@Lq9tqc( zG%k*tR36MH%^_K^S|jPHCbdqlAKpnNxu8ui)V(=6Xs&d;q|{3L_If)F#9lSh&k+=> zsiNza5%~Fu!0;riLBY+Z${QI1-Vp|qr2J|}BbFbR*q_^xDBiw2514&eoW!|3zWMcH zQ1<2&Ys5=;AxDwY%x^eZ5&UWX3LfDPomEG(mB=Te{`<0ic8C1k5+bpWr`+_b9bON6 zCPT`ccNJxJJsahs+h}lTRC_dbi2(iRA~ME$G3OB$VOJXYvuXw?%_F(}Sg&!- zshD5z!XUGHttepAL6(rM3>%dH$zfp{A2)SbW+-QI`mRK+y2}q*tR~oU{D?)_$58iz z8vgJsR>$6TLyS#_H~UHF`hnLE^v|uwBxWuQIrFX)7oEt7IqI*I)hRlx^-f~Yol11=+^@>zpPmBdhBT(tD z4P-UDO^;q(UD9Dt^zE#gIu%!3XPI6}F8#*jL5P;fQQPXrg(Vxs&hljH6{azR()FfZ z{(1~vIrTN}=vf1k-=1&Q_=)0=_vMAShMplbZGhr--OouBlbMh(Rm0`XvvH>_!>U_B zL^jFJJtRS-`utb)JaAH+{?qSX{wsxd%?VLWQ<$$N%32Ft>=%)h*mOaRJO{F}sVFw0 zic=72DHjJ2*t@G!_hf(8_vp|yml`d^$0S%=vi1Pi^6Js~XPR!vOx{X6+>Mf{uN9cH6UYWo^_kNwvkUG&90jz25P@Q3TQHz6@wW&`j@ z&OMi3Wb0y9TUx#J_@GC=F*`L~nQK4MB*noZ4A3A>c5}8z?h#vRL2G=s@0a_Nr2)Mru}}I=028&f>uo`wq>p%d>PV0U{CghuGzxU)$YML`!pg zdcyTA?rqQK7T(6PjmjS4_9W@H7zc}F z^AffNfU>X9d;hgZ2XaI6hqiLl)NK8-C zOPO%4m%R$T55#OH3>F$T_t0m0cjBrN%4m z`v-EH%zV;7)9LHom4_*5{j+UFAYdhd*%7^xaF}A$If^`=Gmm&gk$2`w`EZ|Z-IKO2 zxHTd`jTqU6dnp8jaATA^aq*E0qnurg7UDYOPU7Hnh1zC}@?TH_s~f-I|5r-+t^Zs5 zE0riL1@Ucv!^2OrU5s0u^=I6vRmA>?wR-W_c9A{5;oJ9_y6x{&N=O(P$VK^Obp5ga z(raZ9ClbbLwoi0(xTYfF^<238FJOnZH9TQx&PeqVyzKUfm+Vv{0x#&6+= z3i*$kpD@kfp66IT`+f|1`bfy7TK`rEJaqeiqR_}Q3VL>9#tAy?Y`NN7m&mu>!0D*# zSy<{`as^azzj`4kw9jSvFOIk76Xd73r%%Y17|NYh93-kXL@V|xh|B1!_|HY!2wxW&tcZK{bsFywG;pMfu zG22QQpFYYR9yVZ?_0!|iO509}PDv@+-uAG0@xrbn=2nuFCp1M9Km&6<_s=@)zqHUY zC%J})&=&niZ(>@(-oC!fnkhduXT)K3kyWkAXWm4r2lx4b^9udAC~QgX)z z003_NY*$rP{nR?!n{dxs)N9QM$vg!nfl?*im7HqEb$5P5b3+RYbwQw}4<9~wO!+QR z5zG4@{0B|`Q%Vc)p=Z-phd@VXC$8(rR?yQywn(AYb^g0=@XT6} zi^1KAm4Q6jAQw7b^``~~aV)H?_yZgg{q;TFFUqI0km+*qIVWdrUDG_a9(o%tP;os2NGR(&P^VtX$c8?#Xz|`7bfHvjOvkGVnc4_wzs#ZL7o<0yAKq< zkA5~?bal2Ul;ILUFxGGDr;3&@&|a_b4v_UuiwhA9Q(^VeJ6n(Czc~;BHqTlMB52fA zsx$;1#-#bK+lYx)L0hZV+-&R!kKH}yw~+bw-7{FIFO}20*VHGi#uL!z+Z!&Ibm3Do zJ|#2pBS%9WB>ydi&WWJFE6hF96}tnM3(g#uuWJ@3fs%tD;?Bw^wL5(=w{&AZec}>0 z*JDCGlyvfr%!xaVfdWr^_@x66e2}f+H2pj=-H9ln&tr93k)y#gMMvI)2Fojl5EF6t z6Sg71(LyrIV$s;${D=sHAw5I>n({8nFH{N*&oKBvRPiCA@TpB&BfARAYxc+KubAT~*xA{~5MV@)?5-K|Q~uGl`=r}04ja(*ug&tRil1ToTX;l@Q|06x1{6K-0q?T zoUXvzR#MCrjm{K7w2Q0smjp^1xeCLLD{b+)9Y>OUHdz%K8dT3(Rkz8PK@)0N%>$Pm+Ful-O{61YlDK2i>guXT-(x1qZFKSAFlAy3BJ-Vcxw-K}AKCmN=fPp2(FqFe*fb zBt@9ExEQ1wOrE9Th#86#do=OX@zy?W8|J@=?>(>=ow_4*){G0EZ&$YMwi4Q%Kg zrn+P*vBN=4$O1UG%a0po^*>ifJ7gxaPGLpSKg(O8k`D4t{mL}Ukx2PcQC)zcPW56> z@^vDc+fIL~pr>EneyxQxv&DN4Xr>8L-Z^-c7umaZb(pJor&!l>2s~V}WZBz7uonJ2 zbhT35+-1S6J6&GOcV}f@ia28EoxFoM$l7V#gW4B<8;DCOs4rXQ7ra?#vTrXtdFl>N za*OvccA!)MTpB-ZudN(<(h~yQfu;A)ofes+W~ma0`m^WH-)iw1M^GBPFCvelx;iW#YD734xr^`g!`^etz{+pxsG1lqKoFt9V+cAj(Hvs~83?n4-%( zx>Uz8RWui)DO=-N$p1*Qt2c?J%V0|y(MJf6rR@zcUKIYj*@>e@+udF%)M_)Q` zm=R5zu)xU@OuaYohV+1m28_S6#-XQQ+H?F%qBg~t1AgSScVT9|_v^B+I*}S}t;RJp}$qFXfzT zwY>xU64RNOA;+mM3;a-V(>S7}feZ~BH66#TjY~NqIS1YcxazKdEaH`i#nr1S3u z{ZdAA^P!FP32BX?KL(xRc3B;~#ly$vZ~K}xpKQ7Oip7hn{hI2~+nmWVOeQ5?rl@0Gg*a`Xpi(&_zj0$omBYIAUYa0&eio2b$Cf2&#iNQ>rDbat5gsv zpW7Wv6)iyB-aVA$eEVblb|EZgZM0gJ0jxtDz2M4r+$8-iW@Ni{*~)$}P$B2SaTg%x z+(%XkCxtjgs@?M7*t(l}xny5v%FK{lB<4cZ$UBz*KHn(Vq-M$ePLoR5fL6Xe^7EKE zYxS_b)B8Bk8e8i9dk>Zprn_kLjua;u-MSO9he!v;=1JU5+XKo~kbd#Bh)Kdc+{T2> z*;1OuZai*slHg!NA1`P)8mqhAQ;8VdfwIIFSrk6MD6+c9s;gAa2%dZ1L?p70(Q#O! zHd3bSE$NL2Vh;Lj{~b5{E@OW!OGYG_A_h+zHE;}h5EOl5#zSgobo5qyCqx9$v+e&h zML!Q;_DfG;hL0@FYeolDD`olD0r~ zxVU0nTypvVwW%1`Z-rgZn9=|qb-F*)}UVvdIry}Kj1a7U>o_}9CtQda%au4`RwPu*L?&t33u18*8O-Bcmt40ZN8jXE>&Ad>DA2Y%6|0}jqs$ps8ZNo4Wx5@N2aGIG2t z{saBJu2^qfqRz~WF->%HEZ$x@o4VeK7i?Hq5pi+E`=Oyp%MMyZnr)sdV?AR_qIF`D zKWXMuzEQ|Wlw#|4JO|XDPR|el>axPZ!Xl!h)6>#uWD^92*eRjF&c+ZZs|5Bm4M5cs z+7}H+ElYLFZ6ot>q)C-q6c*MTOFv;0ld=~Jji9}pTzHY8sl>vE0&CBp?>Z*=J?$3{ z*>V*_kM|QC-#T_wsvjITP8(Pyw$`w>(9 zfmwTd`yU(3_;?ceYfxY(c^(0bW5!)RY7fwEu5h2BIm+MqpJR5iEpMmZ9-|Jy$%{a%6L6b-dAs0M;^>s zrnVGb{Zv)0>Ajcbpkr#=bj3Mu$5B%ubkRcgc>uqYIU>!%veYv)Kc|>4PIEzOel^FfI#ViqPWO}YX54C?fq0)w6H@@CMl%@P z($c0X1(hwg^l>^aHY-|W$Wjq7c{}#qUYUW+L!^Km(dc%#+~t4F zJGRG<)=u73EO)ROZ_R{kUp?>@FZ$di^pWUL)1``FJ}OJV;egjsIT{u;TchycLAjYZ z0-Za{?08ottWgd94xvBk8N2nj{GCLO>1s=c>NoPc=+68Lu-gWl-%lMKa#aUQS+Shv zQM5!ISY`#jlMO+n#7grgTk1}`jDagQfRwEFIj(;rY|X6bjANH3jCGVYw}eFvC`Y!s zJsQE9V;pFSd>3I7O#V*wdy8{NSL3@>%l^q%nkrR|#34I_qLwt*An@LLUhLMwXC2ms zj=`293C8@&Ix65b=4F?eXw9|S8)%wA$+<=)EK2*sE0ca~hom$Z9b;Yr#e(V(hLx2^ zJntCKK!0k17g7EFL@d0kh;Rf`anG>q1r$Ge{dO!9G)UEY1F@NfMFeM7-1cXbk!%WX zhWj0Ed)&xs$2R(D7Hg0>Fbm(Pe@Iv>ZO)_T5%^nP|0>~%ZVrkn1n=WMip^^GX53ru^36pEGcm1#i}X!7mwS`sMyfSXxdy8FHl(&E zV!!Q#C#TEuzpr-cSjDfp{f`oizuA-GeitBlR}1fNpRGg31V$-BG^As=xVg=d$B2Y^ zrF6Mww>`h9?2p{Y`j$=dl0<|4Ztn*+x?Rm0;PuvG)#qv;{Gr(=s!lFl8s%a2bF$7e zkqZ^D=q!rLjNI8|bX+}&BLuJN>8tmb|}lwF0k@-uD>e(p3NwBnBQO7gntO0 zNFr*()}jTx`S)2IETKSU=0CDH-oU)qZvxDkHabIob20x3;`>~uUR&;!$60h)g24n= z=4WTGnhHNZ@#l!-PzKn*1Bc1mpQFTN<>d!~;7eqDJY#wyrwmLCwthvKtH(GwX8Nk~+Io23mfljsotRNtXF%EaKlroFWv>zjeGh zJ5sD6@YeJ`^RQbn7t&s!dUF1|8gJg9{ql$XjX{f;n3$5dbndp~Q31KQfFj10!)g-- zQd40~UHSdv@{S#g*sVfS7QzIR@!fRyO$*`lbca1w^YP(qA+ZrRK4W3p0lOW06w~Sf zoF^ea`GBu!^wIJ`-pBhV8+A=hO^2DL>+(;u-Q6p)FYuTbw4|h@Jnu=7Ovs#Lk*C6nt5?IlPPa9I6MqD`q%i)GQu%ATYVoHR1r zuTFj&I-t3ar=oj+)?VR~+0#a430kpqgRyij3D*O|O|*45Mq~Bkew7pYOPPp;0lpvQ zq8S^(8C|2VjFGbNCFaEUvMekbu3lueESY^IMZlt!8gr>6)-;8t`TR4pF8DmSM#PU{ z*@gP^8i6*(C~dBd@rznq5h)sDfrZfC(@RLV#;2$ z+I^_9- z&DL*&_diLr2n7y6hN;?5_x=|EYu8RfqkB4dZa>8zf7q*EU0n+RFmJ1`aPx1C_Fstw zB!>4+;@(FPOu@gC_2-iI)EBQs0nGD!zIXQMqko|KEZOcKoy0A@249k$@2dY|VPEfv zP-v5=-#kAw^NV#7d7%VN4pRREgCdM5AhS&CQ2wY-I*L+q2|e+7H`A{ChB{QS09M~P zeOowta@LC%Y9A4fqZLl!ng-iVI+^0+>KS!GuV5DDiqaMMkohOZx;r0B$Qf{dlXQ?U zJV}1+FLgVg;Y587Y#tIR?&)COYpV+iAdXpdAoQ8@*1agNXUWKaK%i3N8(s4LCswC3 zzdD11%VGd|d~8#H9t~Z0(+1!CTPBPHoF#3H-`Awo!+ZNJ5l5qocWFXz8)oBAN5Y*U z#-%&8&TE&Fq(DE3s2@X!ZbsR+Z+=1}BNI4b1E;v;AJTAw%WlJ_pF-~vBt9L?WwO@O zT04C?Q3rfgo}YD@ENT0UjKig+TD$SdKjbVI$dJxkS@-yqm^qf_wXU{Z?EVg?uM+I6IV|&rDs@(-{}tg) zdUooK+%-^|oO!caOl(h2C8_nWUXI$hDTk-DVh37*vi64+x;o*X)_LkSP}!$x{8MmW zHyiWMH0XmYWzudu==&`htOo+$ zSeuR#a%bL2QT8j&M!aRB6*WHzlZtfdR3wZa!-o}bm90*FGj}-rH1)Z*%n7j(r>F2Z z3p}8b7Et0PBR}d{LGPRAthlNucu@O=#}CzVll4;-=rFWqvlkn+!b#YqG`qQUyNELr#Q=!z!KYJIW zsPU3d*;5@ECfjXh7JBaDDXSW+7-twR3TGNxh35B=n0e2$xH;6Ty5htn;|uTmS(P_( zg=@|W2^zqwD#quS0^J@R_N}Ml5|5%Z!tXN|8z>Sx=d6%oH=F|#*(uRThJ{aTi!4utV9A^pO z6G~@OC{{BPnTn~SbG?CEvOtJKYbfDk!A;NQcUjwG@l<^C z64y(EW{Le;E7+XupB=zeOE;JpPDcMS2C!k9nC}aYI2b5ZTH75mQ|Wt zT#pMX1UvQe9xxE@`Ho{@SCGKjWR!>zREiGKMoXa_m-ow_E)JlujeYlNycaWxJ&S=Ct$LRww0gkw? z0J^jxhd$q)zqHm^x{tMzoBbq<1*BCzOn^Gy${`<9VBP1T7U>jM&ZovKr%gp+kCud(iwM&6FFmH z)&&a~=HPE9w4001u&~eHwec&4+QwxPddnG6+ebzd5R4LxN%SG)NQH==KT*>Y9F1^j zCB5N5%S$3BE5{um@W62;?NLg58_n)e@Ymu;7Ad{I@nI=^fW*A?#5?i9pF(XOD7UDG z*K(+(oD&0ysG^o}jW_)qxfb{Jgi|-hmBDpF260H26ch34>T<6bSBu?-{k@XOGG~ZTx(A)GRJu`AKekPzk>4m9bwdocpt5mn%RU z=-6e5+~H9a*G!mW(zYTi$vAieH&t>iD)BIXMZY_qLCyXwhM)rsHD%r(G$B1{UiHep z;~?l1?DrE5PTdC5`{X5?6<#4Y3xIwE_J@z`<4%@Cj!%J8(tO5b;#ZZ|KXGa*g~4() z)9-dH41fom%vTf-=bns})oo^aJxSs^eA;vB$v>$$ifiG3foEk^_{m4sPr)SNwG*(A zuv$6nr=-Wtw!iIFDi@rsn*(x()#JE0YiR~u-n3k|s!NsE+Ex|a<)PFcO+6*zG&fM&#^?)LpUSod2WYN^0vB_29x6Q(n8$xb3NM;elc^@6oby zmvV2{-mX;nEH1&1mHagyrNe`lSXipFq^&hGjv`_-%L*l6&V4Ra<_z5C`waGlQ=8mN zVet-*WYz;P7UiY&w}MMgLRZ}!K*l_HSfGw`cWxB-Cr)BfJgirL!tRyVFJidb$=!_m z$!!UWKKq+Qc&V1*$IZNVHdpyJ@IDiR0nc(U(yV<%6~+ry&`M<@&bmv$q*<<5fUvbO z2KSe7dnsz|F0V!A^zXcPM(g0`+e~grjQRBCmGkp{cVs4$O0`6#zu<7Wt^7yr`wGcO z*D6`|x0kNoSu-RG{y9Mq66!?C~Ih%opb$$=dJxFUzpqF=4eZ z{Z{T?PmjOfSpb@90VS*K3TIjpM_smSL|5!tp50wY+RoyrxmO=$EM@4tEgW1>yx&v}4mHxG26+?-7>i{&j6R1-$6az)BkOxW!eYkd% zvl)1H>v75rs%^SX)Qc8uk`QFmc!TBaV|$~W zS)j8TF26fBUE`YKh(@_bL>7th<$030kKJjILu3oZZ*0?~?ygTk{X3;tpyrfZsjw{}Jv7gZ>{uxc}RU6_jKVUwmCe-v%{J z*S2U5=)k3;2E(cbx3;9=ICAy%+4v;>Lug6-zgy=Il86Wck-tIt5M{qw8-miKdZPoE zIv06DdGfy3S6?#F?-yF>+9buVI<0u`U?K(~T`y|7r|!qYA0PZKNObDeS6*I_PR4mj z?|Aoq8)~_n^JO=OmQj3(;Eb0rv(W?GO%6_WB@LyH-T*7zv@%H7^&qT(t~&*1I&)4v z?W}BK1Y(_px4#(~hhPMv_!LTFfu`(y)6(Ai+Oj|Tw^rmjMx!jC1t>*7u5(toa_~g- zG97MN7}zA9nVIm@i%;LyHsND{L`-Lb!b9)8sC`Z7qNY{t8jZ!mCqCV-4jXe(qZK>) zaEsz!D!3QawShKJbU*t0mS(FTi=!ppKkX$~gQehnxKMiAcjDkuIN$l6?`cQa=jC;M zvC@}i%9P2;uDnBh4y8{GucRf4vu<&j#`2BQfjELYb{+PbnlUJ4RJ-x*IkIft(v*?d zQPIY2h!WmR- zwO>m|3rRJIcq-v)PJ5ZAC)`XYY)L$0u*Q3Z-)*P{M&H-oYSqUXoiMNA)R(y8eOw!l zR(_nSw-h(xv|r%c$xGe`f(cFX!u#Tc2i0l@Q&h(H^vW&XGN?DC2;DbHXc5}atoH82 zRWaC^Kd#R?(3VtCcQ4pV3d4oPG@0krYW}XhFPxd4w6_&ZSV`*K3nj&T!gVB5^*mCy*2UrqG2|@;R zEL-ro7rg?s%#qV&U49^AU*2$oSLyF-KFh>$y;sZRR%I%TcNDOXvO?H_k8L(C9n9c& z`_l9}uDsmy?~MP307D`y;#V_6y%kL>;y}a*7RuUyz#GT^cxs`TzM1$)5 zUHoZozKAMT07MGk&t(at8YJ>O{c^1^mPi)omF%3aQTevt(8Z4ioHYZU!UY60SFLDu z5iwqB{7U3<;9hyM!=z&ohRc{GbCD&{vjit3@1Dw`&ZveI6_0pTQrQg(! zpNTD9BXUG(!{AWP z1Y6Z`Gh%e;)uOgkL2twZ3h4_rLj0~k;sAqi)i@C8VzMh;GVM>mB7pOh0+k>SY{n?j zw*r>Z^ppBHW-sslW{Y;#-wAwn(fB3tH$v}w{z4sfOoZ*8YZm1)gdP3EoQY2|YuHJx z&0&JUS>I~Nd+qbnv$039#4i=7z!CrYP>R4>BXq0jH|gNh8wjzA3kuZ9 z2ppoB^+cx%^s;YXO|3p#^t^-g2Gpx#32oex%{fTI37AE9vNty0&f#7RqZ<<`67dqI z8J4%V-lC*c`Fk^rps|^2y9MI0eFy@9fb#mD#ohsn+CMjnXNBz#d**qah}$Y2C_9^R z;NyJy1@-+c_B57$TV@mtBA>i_6!$-%fI#V&3(8)anM+vqm@E#+6F%OFtrXeKJ(PKV z4GIRP^rp^lZ;}hAIqGR8(~d8{_FyjiVJR`0^wFv~xw*4)a%6!fBq|K%q+A-%PZ^g# z1Ifm|w)DEnyZIn}gn(W!#8Jw7zdv}Zsh87-grS?-*dF`hQ}n-(PIM7v zK*lB~CnrH;A{)uxJ!|JmHh@fOI@g)#;=X8(78DeO(^)U2HQs1!WW>s@J+98{9)W$I z5_+%Z;$`rae@h90*FYkSjlR(EKgcM0YR3y!nc& zor|iyi>d>Ps)LKFqm4u__>5r#&XhZU$MP!?SZK&%gPTHqlb}(TMNk#kdK}5_DlBBa zrT*B)sl--kZn1UH1U(cai6uwuD#OXbi55JhNY|lHr}d-L`vMRgT=SlM=6RhRD6mH* z!_@gT1%V2z+!x!3tZl*UqWzj^((0;I6O1d?fEo4Q-M(d8Wo~A5%hq5i_u|zkS@CIe zK7H8MWF|RG-Wyv6JN+HOF-v<9DK3qY3I_vIDNND`7BvFzY3#IrHA`E&x(GpCpwjt! zP?W!1?TVB8ZFxC4vU0gDwsX9i`&g()zDT8Zqb?CscHSe% zIeOkg&JkxT`81n$TQ67zOc`R1Rubv5dmGm0KyM=w4Xsp#MUAHfGpm1$6E0DVt3Gt} zETs@s*Z+_n)_`ULAx^Y^WmR^Z-<7H#;EG%%`wI?0Zh#*6b&f_08$Ym|)(?R=eRcmf z+Mm$9zZ`hW=NLG393p%ZOlj;R8bJJvnSU4yCtLRZ1q*Ge?+sFj<phh zH>8->$$A7lJW*&vf4t|IP|sPj$BkGR;x>vSVzwDx45C3SrsFPTArI%VUVeCm{>{

    K8$IEoIxtlLX<>?7cS%?=`p?jTD4?;mL|acPlP9b=`^I)V}K zb~oEW2CR-(+-rd+(Uv?+fy1rvJbaRmiYF5>VmCK7E@z-+p412U*Ia|HI!%lAAk}*WT@G< z8vZp+{zE$nbwCZA#K%qsh5W6-{_BhGp8!vLEh0z(JqnO1ioCyTmSJ%h6IpfYiTQC?r z`3-PaeY|cGjRZR?-~+6^jU1HI!@;`@)v*@#eiXPs6qA_X%nJZ${r# z90|}8Zc=*g_S{{fnueOg79$`yJ}fHbW$FHlmvAZp%f;~6O;kCXivf82RC0H4P%Xk= z$FXv8(d^MMQW`WzF=Bcjf?}MF+Xsf(-BY>|HHjvL1G+GsXhd9$K%Pqx_g`B3w;T}2 zbFr!-j;3yrbSzCaZcf%0Td$C&%``ixJC6??9R*`6@xmJ$gh^}Lzr|X;wg|a%X0RwJ zyK$gQfU>uj&oz;c-l!5UVdQGY|F+Bs@MwVY>`=f%iQ#iJ%gq&QTq5Bk zJOL~C1gz6Rcig!VgzSLZHXtXm5RNrKG5+`7J)f z%=Vz3{1$wuKwK4~O{b!wA~@w3ggD%N*CBaV8<*_%wS@uzOP(}_D5o^d0gX+DX>g$< zzL2=73aC$o7RiE$6W&jQuCGP)QC+ob^kXpOLR#0@D2oNU_&^{*6S+*(d(=v% z%q3aUd=C0HHwCzedaoa`$q@GCx~Y8C^IL+F{F)N?3sE^ta~SpzAB6@uJW3^PXcJtX zebU04;$U3`TKS8C{xfFt*Bwdri}An{o_P2qjQ33C)&-t^+BLc^8gnaTW;XxrTOr8? z)zJ0MlIGN-=G@Ioh4G^Dj~fKDeQNL02bzDa^Ha+uEXi|n@B?W>VdL37-* zVA_O;L0oixKCTClqd1r!`~hUtu{o|rgAzCQ7;G^Vx1TTj!4Yz_uHt<+noH&l33O{I zcSe{@kCM1}x&3X7y7D8gKw-QS}7xttSFL#cABL-^Qz{YCLy@jizz9vn=vG*wzJKYi7v~ zj625(ly;Mb2MU%NBxh@uDR|}0Y;Lp@Gh@sKk%c+#B5*ZkZ>1+YTWB*mqvp;s97{q> zGd7v4%@Lk&!IXQM9XgcC^X(p~Ca9ONf-1^s_82-{$1AjMKje1i-&&&7khH;@V+j?;2^$-8J5U(N)&<7xQgA?;3for+N*rOT zK=Ye@4?-Eh7ZTMDvFm%R6&Z6{vr#ZT;WSEMyfS_>Gwb0T`Wu^O^n|EZ-%WxR-}q+B zR5qgo>h+ZLG~e3S*E4vGik0Q=Mc+n{6)xk?W2Scxx;zg*l0J}ob2l1NDh{#Eko~In zdF`RbQ5^bg)^4tRJX{^Y(o;$m*)`CA*E&6X!npapvq!UQ;Nq)%i~X#Pb{1&lf!Ate zz<_0>zd01HSCC!pEbFFM5S1H}tJfPMptljwv*yRriXaY?^u$F@nAok4s$qd`OquCV z>w~`y+7(wHfB(y6Fi3^rTw9Ey+TiA!>#;<;wU=<4f=KyimJ2$`HU{f2UninNq5ASZ z!Z{4~!-oK>!bewnCUq4$#Dy>W=#biF)4);m+G}-5yNgR)$u3^62xU{u0|D_ zIl;1)pDV;!U@+6*;D|O|jw>~t<}epdf>Sx{J2w-^J0~M6+l@pih#sZFWh~gW%Yxd~ zgYvmM^DW_(2|-|i-3Esn@mJ&Q>aONLYbDSq(={1pkUw6MHC5;0FZ~9!&{(g+&)*=H zZ%sc=j5B<&U}ddXB_dQarMUm4#qkYz$I2|CnBsaz^=%z@u4XrPsO{{t{YvUO>QB3iu;9VK(3q;7?9%CA#3Iv&mze@Pr-Nn zM3TplLVfACZk?$gxWwF5yzSjexGF_6tIVu%2NxPreyIG6)^cMHx#yeRP5eZ*RH_;n zA-mBU>CJ-6z2nhlSg%sj@|J-RIEjG{*9(CoIfKrtRv<+`JFJCNh+=x(GHeBlPDZ@Fy!Npl1;-h@2rKuirqS!pv@t<#hpf1!}1^Kh_;1X}X=sbdiIn-8xnXCk%fCO<$f1coy`C}Xm!D)Cto@nFL#t`)CLr7A z?lUO-YcW}$VVs_>;*-SugpaGLJ~ltW8?I=~?Z&^SRJPI(S0!n~_h5Rd!mz(Qv58xF zkR6k`tX9Ew4^k5?>y}(X)_7+>xaLsX@J75pH$<1NBxfmx%6Xw*e8#>+oVsqY$uED^ zhku5kQ@zSMZq!3M1sZ3V&KjWUKqmOll)Xqdrb1^Q);l#PvsM-4{LJ{5HE_VZ7IO%fxmtcN#s%jM3o9Dl6bjkasECS==+ zOziLIAi#fsTI8WeSJE(emwQmjSo0i0GP)lKb%rld^fS6c{2nCq)Q)G*Uc3(sHYY!f6 zjX5$HA;Qp&fJ1DA#c?=WxK}m9W59OR(6w){nRq2ysllX@8A7B}ZuU?Zv@pnXt$w%Z zDs}-}f{>Ojld<4onrgL3wOeCO{$>?uqqYh+{l*dvsymWBCNj1#D@!I#ogew5+|zw3ZV0bK9t*<-29V3Ai0I(Ru%4O5 znhLamh$}RP7>vy6wJIsZgFa;M<2|K ztVC|?>RE)cK;vYTr88vI8J#^g3)uL#Uw4;F3p13E4^uirB#p~gT;@{SD5)H4lVmq) z`3{kCk#-2sc%-z6`i^>JTgBStGFl173f`iI-~o#;>clFFyuLJYMAqi>9{brK_!w0| zuP9*?XNjj8&6x8$fl}6(oWf%t^46YLi7Wf1&0j5EAu~W#B4$QG%Rn=vKzwPXorcZW zLk)fRdmd6^SFZb*V$Z!z5h{R@Yp@J5R_2T64<+a!B)D!?jvMa>d&>&FRS!tuvbGzz z5|h$OK`FNh_ee}#;_Fs;6Rh2OeLl!eL&^4R0oK{gFDshF%XgTMzpvfwdc+krH_jf2={JpY^xqm9zmHWp)a%NVI4qN=d{~=p7x(+yKVw?(CCG)8b2to2X883^U+Bl=+$yGk85Hp9cluz9CTI|>2j4qE(pMUn2{&xc zDx!!OF0~3@xV%4>Y(ZU_s}i=kB!~k}d6R(HSXROdU9JJtvkpLy!A3-Ad$YnH)KX|@)6q| z()`1fDlT;bCO*y#XGxps(d^{HLTxz+X6y}26U#T+1rSebqt(l5WjrodvJPKJnz*cC z^kxyrha4(N+FQe+wD}Z=-Pv*+DyU&7{Z}`l)!M`Ol{%M4D<&V5@w#j;K0ki%0rPpk zNY_!d-pfYqDfT-pb6J{Va~yKn96kHYq{&P7CdPK!G>Sc9_O66&63Xpu`bVpw~G&3kXA-Se9_A0@q|bvvBJ8ED>oSQJ!)u?`*T0WcF9w4pfq4jbPR6DM z-eNcA(I#+zr2ifJ5kkuAyB-nY->%(>P+g+!T4XHR+bn6_XvVkWII>Wt6_YYIbP74@ zB>*|jh|w+u;F7F%i(Nv=L(5!BnfxN1dPm@+WyMj)>PwzUN;@+Z9+A$988zWjS+cRP z#fsw&mR1343mP$BbYyielo~s2>nPoMqqEwd9nH z(F2jJ4G`Qz+_*h9vWQyt#bHl$HB|r(X;>tKS_|NShxd14o#G!JNeqq^lwqB ze$=Ls$6oa);&xTb$&1`(GZJ&m&SDZa`ZUHaeS>m$HT=?pXWEgwdUQHHTg65UZ~8!N z%q=<8!sSal>pgjO937 zn~!pd%Sy%W;EP7S+xreTWnC}V5^X*sF*mNP%Izg-)yUY`gse1v@LBEkmn&Brg7<;l zL~hE;hTnNrhN{x@aAZ8H%zmC`r_6GMYrjRUI!wa1m%pJsj@!CvCGqN=`-*6CL}0=O z$1_%01T&%~0#{a!rTAEHuqkOvV^zv@Te&$9dN0`8Yy+34P|^$fLDD(SYw4?u_C8Nc zln@MK^-;7?vvRCn2XvdE_qGBo#P1a}lld7zY=ECgxWGP~xRP}P3a5w_)-DT| z(}Z_9*?>Gu`qL(!h9xBDNRC0LQ<@_?!-aJc%8ZU04%J$$W4}k~SfG*cst zO>&K_d0dcc$D@AvMdDnV;%lY7)=h4$x$%H`=$_vOs$w#|L)-^Vzr>D}Jb4s8VWk-+ zY?|Y-vu|hh?9rM5+-yI3+kKf)VO0|;I;0O%We&UCnXFWB+2$ORCMx$aElsTYa1`DW zAbj+c*KX!@wtC*(^`7$LQRi{bx?T!1e`xj4H55vX7xS0i&p1b$pN?~VM9m~AU?zUWI0WokN?jB>m0D@g1429F$L}gj_jm2)W?QK?BtKj- z5{MmK{7wlw-gR?TyV?+h?`j<${=HOf;OLOvlo5|N4WU9Fg@m=dYUZyfvW$%6&W&jJ zShj9o6E2;LWG`E5Q%Z5{jN5o8P8RV#b~=g=asXohb*e;j2yNiz+ku3}I1p>Px4b9R zHvA6Bq7zKR8fU)mZO$UACm({P+PEJx*Hk~?g7ic_<`UXd*?%yPrQv1#z7jDtZHKIH zc3ROoB9u;5WjHUsMujHFGl|-*W@3^a5s6mw+94=2lU}3D&gkyN)j#5c&u_#Ro9gX# z@mf^dkk12=eCLZdCJ(HduKXqIKwROb9z`i#Z1~}9O+{>l*&dCVmEMz1=!(qU`L}q} za>TlJJNYcF^WzB3Io2?D?v_i=Mr8akw<;bdcp^gD%q3C_B0Hm4ykTPd!)oLqhPpip z8pt(7z7RWwjur_t6ehK6Wdlw?lV%~*K^WlC{<5KLJQ>Wl=6Ce_xg^JlIt&~cE?dXZ5=OEGf9$sD`8ncJuQSjEf z<#bqcOXuU#O-w7hD88kX2BftycF0CJZ5r7a5ws?g{%Dsul`W5X|G9Wog^Fn@Kc{re ztER$ASaXV*ExBAw{3mVZ(n+iR0m9r!jm4_;k`Ecma#kTQH4o|Iwy+7s(OD(th>9BT zbZtkgIacth1gY~h>3Ddjm_Ss1q177?_zvt_$?}YSZkTsDcPa58FS(R#O!X6IOuED> znbk^LN!2(|WtDxR3LU!WM~+f#ZK#1=WQy=q|5dvYVQ*=O4hf>Hd~#>P-f|k>jJ@0^N(owl3GIF7>F!iv%aQujBxcL;~a68K+IdkHWOtkem1D=t5gs zq)e@>f}n{s)t)QndeW7j!X_L(p9&1KKK{VAN>VoE;W^2*E1)-efK4~5o^#ldJj^I0 z2W}3i=FxL6ZSUCjzP{~rGhJt|d3G0-8b}qK7&uC)iI_w|){Madlg@;%2TEx#Z%|%# zRAUSR+Q6Tik5L}tkVLkur`GAUyphSyjUHYSxaC=A2(RNXl7U@7I|c1dW#P`ea__0tXe zNDsXV9GFS6(4O|yHl1TWoR!cO>nhr5IdI5;bHz*qZ$YZnO|LYu)sj~Xr?ltao)pm{ z3J?s10IsZ>oG@Z*BDlTR`>j@&k+Y*?vw3b64Up!FQj+6MZTz0J!9SFg9}J8kZ3adx z0W-a56zD%B5W2l=1UxY)|EsYa`QYK<;W)pk_Y?gDmF0fXIC~T-W~0tz{ZUz?b1iC@ zrD+BnroULOmi4I1-Vo0RNe;U2uE^k~pS2fzXqjf|P0O0z zdXRyh4YL?l{_XF5+CJ6QJ(&~N)37}R*GCTWy^d5~lj!>hc|s0K2RD}J@;}~*)Rql$ zBmJT{ zr9TwGpI7%iPE_#Auatbjr2J7XkIaB!B5fdkZn%Nswr$2pv1Rzcz`$2Zg>BZv|DU<5 zM<27ZWz5XY@ze^ukzPZUt_8|!YP72xX{kG~ZeUv7iSv009fwNFljLNG8=e6Z&GjZ+ z)NA+3f&9;7TBC=j9G2gNk1hc7@9qPoFkcO1Id4HeeR>GYN4eqb=05$N`$n_%Nz$v8 ziH=T^>Kn*q>dl)O?ka3;AnEihyQ2;WRU>Fc?rI}}7MSz1SZ6R-i*dcHG1lxxJWYiu zI9O<9VL>8-++*#*6g{UH97Ro6H{o7m5NVu5ASI^NOAn0Fz&lJLccCF(?%zrN?Hit4 z?|R84Yt?Ib0;!QvDj)*_>pg;IHPsw<u#lu6KRB?RQqnslVIUlwLru zPxX(EG96T?-I#QvY$ifw0~peX@n=^c1@$CV0E`(A@k7U@37gc^(t50Sj>iiq<8Sy~ zR6GdJ>|@=NZJ8Sjpn2dcM5lrtz)}LA`-Fwvh~cq)Hq~JRjJtmGk_XZH2uS#!BbC*d z3vfNoi;&7n0b5$0u!)1Y=I5x&FCOs-q5njYP?Uq#=*-N_uMBLj&wbt>G8+QyiD$Mo z$W9~XocI6u7at+a5R5@uq07GxM4aY`Q-ED523X4vf%94DJXZYG4}sxjKtTo`x!<{T z{1v#DwSawaofo=W$?^KwRfB)q$^if{bDe^l7PsrkGR<|Xd({lfm_L^N->)c6mY4^| z_`6#2SFrv38PQvS;jVsjekJ&8@AFq!0Y3XmKyjU!3*YAyhJPI>IsJQ41pqHedyvnx z=W*Gu7U}aHfbd(|-D|*?5?I<>>yO#sg}gTZ{dNC=4Txy>+{wo+KFJne!S<0ybd$G? zd+g-SG<7F$ED9s7d-_KO8F$)g{vC;ZLK$mGC&e}@T8r+y5K%y5#FV2&M3W0Wx}mM$ z81r*`{jb&oe6~;pP^zkg7>hC(_m%8tbW}0F7jdY&qvS^gM_VQ}L|3ERInVFS{^4!i zk40_zcB=u-^~R!%{k7DZh#Hw zy~n<7w|*t0XTiI8QrRT=uPd9bP+r_{@!yMkd>7i~>bIAF1+?o6q6oQ0ff)>`OLjm@}?$kkIBPwfKx*WVri6MAwJgxthAhZHN0!sqk^KaD+yBM-#vQg@478MExjHxQ4SC z4MYp?SV$MpG;jfG65*T_JlX9Ej!Ro?e+p&^^FBIQ*%(Kz$_=d#KqfzX?);eQaSfvo z?xjnndH8lg*rTz%Mh0^Hz3+i7FSJm841SXr4AE_fHY$D+fpA=nYVB+V>JJ23+|~7N#{D{M2SC@v0BC>RyC{H*REQOF zgG#`lQja179w}!t*S8SCFOGP@y^hJMXew$ZgWh$?& zXY;>a~qQvf)zfA!VE6Pg#w5&9D?>n>bOlsC)IUGsWm@&-;^60F^cHJ7lx&G*10}o{>2CSL|DpAflB7_o@x?C@iLqy zkGz!`XgWi(bwE7D!cn(g@&=?i6Fq~^(az>Il40x0xA0R^qqBO1o|AwOA-T8_ICT&I z+Vt_H%f)((tC;FE3m=}iv&uVO1&t5T&q3Q>T|jX~WxOR<&wQ|VcfXO}DjIgYVc*j) z$P@g%r9h|rhlcWhUE8;r8^Vybf=#A3%!JRWl*$F1A&ss-ZKO2&R-Pr=lL=JL5W`pG zbb)~M1GRsO^Foib6oW58`wNE1J0XO(hDpL)PsT|<_oRnBQ-S(Yt zQL8wz6=i-PzC~}pU9kJ4OlIdCy>y95uy)~gw-=@MGe6R zX)rH$iMlvVcO6SF+quWb)fQXXdBg%mXk zxpO#3ZLT^SBI`syjoi}GUl;aB_9?|7 zFk8kR;PAi3)xN`|#5cBhG|hpFj$zxjSVG*^@PjH60XG?k#n|TbmU!@WctwGKC%^qD z^!17z^Wck{JyxL3jj4{=+%^@GClBH5l!-2~?nljk+gY>lok9RH&#pb7Kdc0)Y#xu`WHj|yfpIFp7 zVB*D0T)YBjl_RDB_yln$H7*y$uN>?QNvlRHR81l$v9 zw%F}fF>_n-%yk#?I_3zunxOL9K>r%}jpn_q(V`DETYRQG1NVXjx22};Bx(4hhR=k1 z=gXv_J7e85Y~NObMic!<@ehgWQIOK0LR#o@O5H6ePMX4)t(skxWjx2Tw`yq!P-Ro{@F zSs!@Rj`f@G#z;NXR*$ehL$<&6!2X{^ATKMEO2;3J_G0>#AFOFs)(EK^zQ)PzyjXFn zXjy*?Bf{|5$!tU^nC3>9holWwzWJu+elt8)fXi~@C>i%LP*ME?k((V&E?iL1_aI;$Xj!gnEdsCSCG4lR#KDjPs*$px$pV##Tl)%+Zp7n^)bPiDjWl3OXDG;lx{vK_Em458Om4)> z7OgBc&K40=B*qkpMZ`QdYGon28pbFHRQnW~*9a8t+}a;yZ|;n@E`}i9Z+vBO_cH#f zQ9ln_wer?c72XP=#bhqhY$b5r0_O-uJ2Cfj*WfIy%8WHWfhn*KN5h}HAG*3empy^N zYMe!0_`qOjEK0C<)gIUA2I@%4Z(=~ISY|30deUxlQO!?C!booq~H4N#@}|1tLEaY?3K|FbmD zEX|BlPMWzjZB~|!nY&;!El!I$R_+Uprj{r!geZtkt5a^3nYlqTRGPBnf{F`HrI-sa zrhp4bhJu1Xiim*d?{1!F-gkO_(_8<6pO4)4b)9p*=X<{AS{l-8H^D9&#YQcQAl*y@ zNC|>l*MQSpxeL3qX5Y>3#r=g(hv4OhIzE-vpr!Edl7ePo5aI}@r)T9-RcWB~pb?aB z+kC9yS!Ge*dNJ0{s|mOc=Y~Pq>a6woy+)h1e5CVOUi|3MlJ(u4Ez8E|^(@9{Mrp=w zbg~%~l6qmdzQ@qpO7XNXn|RIMX!%?znCSS!`IuWykl{@w$lK|oL#Q=>u#3+d$ielK zkCmqm#NFE+j}F5Qw^%zs3R@PR!CdseOatpQD#oketi|X~mOhpkdw8L)yT2{XE6TpXiZ74Vs``I(`W@Oh*YZ5<=QmXB&sVLjq&n|KKr9|V zf?id+=^u^opObi&UGbQ*TC(tC|C{XpP5l6mTf6dRH(@{SMwU1EFl28>Z|(T3bjwq` z0jK~K8&%aZhsy7Dw_anoDM=qw61k?J+ofmu1hcaC2=22Yl(r+u>)HxngJjpKM&pd; zP-e@iFw{~)B>lIEq$@-S#8S?Lhi!)&+vaD9K00{n0e-t+X=lQW4K@Nb`WVEbb83*(H8zA}Ojh2e++ovFljsq5ykjGqS!r*y~ZjlzmWE0?>T0aPLOc z)a`aW6!GGNLy^NgmtB0?YUa8T9NJO8mi_FYxC>|FW{*)nshK%G@mm%7I;?hhIe2Jv zm$yijvQM%jFwO7Uio)9s`{-QO^(-=Wn`PXY!s7L-ZoWZ95C3$|;e8@mh$)%QbHPYI zZ+5iEk!18-)tu|2PESB~BB6Hb8|clbzuWG;bDx|`gxdlOrVnTO9x2Sj+1X)G&)Kr6 zaU%b_$9WUGrxuQ^4si{kvosaGKj^RcahF8vf8GA#c1muB{+E{twIs!sNB4B_X%BPz zwFON6>~Hz291(r@iZ(e%d-x*yPnE*MremfC_RpTN`jjam&4ERu3hMGQM}f*sVutSz9;>`&F)nH#Vh$M3fCP! zX1g+KF8W45!`bGBt#ff6DGH}@fo>gj>q|wEwNsdpP3iUFv(3{P9xAV%V)DUcRZ48! z!D}o0OWSGA-R=Karhof*1*+spMC#sFb4A1sQ3y?w8EFdaSJBn0RC~GH89f@&`+Cb` z#RnAo@+h-sE{DpJUf$t~ST2=r2oO=J%p4-CdKDUg{It z?6066mKkn8&N}ghgx`5gP+Ieun3+bb8SC?%Rf-|`qI%}J=J)*m@nGYVr&i866qb{P zp7k(GvW*_MRqM?9VJ*qB&hA3WH}l`+HGiCC*}k)O_iF|8S9fd5+yi6o+G!fQ=fCW` zS#ov#lq?~5%U(%=mE{fp*`Fv*UFV+D{e=Bj5*<&L9&x6MLDhr?a%9RfDxPQ&JSpAq zQEq%jd|UqKh#RHjtfB1*oUn`0qmRQrXWDHFA1ywUzd!6Ccwu+aS)Zik4US(?Cx1tr ztvBy-m(){yEVPC6mMq1Pq5FM5TuJaTt3n%)=Mp!+E`J4xsAKx zQA7`_onrfnfA)cx6`yr=^$mXiLib_fL#`qZDq)xMR+0Q;FS!0x{XV&Ry=V08Rsl1flR3o9 zstwiuPH+7%c;ISl*%R81M6N>S>Z7}F{+}ZH-!ypZ)jew6$D8$E4J4|L6>U&I(;gp`$!kO0NZj5G(|9noqR<@jEd0O!(jHQ2ZlX>G_ z4|v0~A~(0%%6c%AyIEM?y}yT4kk{bQGr!ZeOR)}liiRh;jdywWO!RXxy{XwToCIL^ z$6x&H@1NEO|DKdq6ReM37yLtu+u``mHRvyqw4Zz}iET%@21j`->O$gA$BU{B;=Yk) za;sgEWI5p1NFQ)t^Op)iS?bLQf22ed7kTWs`C@*pL#kWtjcSKu!Tt{Gb=%IaT3)&k zc>dzuMW+ZUTh=@atG+wke&h%=p|JIN>_ba#@z5WWL_{<0mtO=GHy7feYfXuSn3l4~hBJgE=%@8Frw2yc9Q3b{SlDZ6bzklO^oQ5R_WO$S7tNOY zw%F>Ekxfuu=KXs&k`JAF+*RahFHcE^Zz!@hcmAWGQHMLDGjI&>`-VM*N$#>!9}VTC zrxdp2)6B6B^Q}AFTpH~|QN{dO$x*LMN=r??YEgK4XULtH7UdP^q+Yj2Zt*R`IK|N5 zW}*z_abmNT*R?sPa!g4qb7oXsYF~NpNEEj1$gSQ(tR10z5I1+7m)mQh{+CnYe^`Ii z<|wZPJ6h8*v^QRC(I-3*ydlI6yfy8%=bZoftk=5YxINN(LHE1}n-`s?8=}+@`3G)x z7g-j?SRE52!2G^v4M*3P%Z&=ZJm@zZd@%D?{~5g|o@SF6>eM4i%xC zyGgft(Gtj3-StOq9FJ{Q8MEJO{up!|qT&PZAw|%|6{B@z@E+>WGlhHcH6*WX;dvfW2=IA zn(G#{dhF`IjNlt|FWV2odK@B2sRUbYiuUh7_TIVQKC7EX?Z=Kx6eaOOsEapJL9P;hz57{iIpVu5`6?$Wt2dR&HJS$wH7 zUy;HY$DbDCHzeQnSVsx4q%~Up3tW+S@0Y*h$+@34e+%QJ)-?Wf8Obsf+uZPmpsx?4 zpnFb@c5u7b;7(sFUf{P5|j6tod*4a8f zS=`(c*=53v@Gg6wIuAJpzn`Y9fuFxf!FD!2hRr^a_60d!o5D69 zwWi;fheww!?P9iUW*+gi(hby#!HLs@gv&hWI1N@(0c5Dl0jUY%C)4a4sqz|4nwz%T9YO~76+SrfWF zUxeN0AEKL?L)(d8R890B2G-(T@kth*T6zH-EhG6dDX{-dTOmH&`QvG9-yZRIU(|Wx zxKgQk&)E+X#ZT9Wr7Ih#VvU5B4pkPwN`f1t{f=nOFwf4Y{N9#zO=4F^UX=dU%}J?2 z!&LkWm6NhOG^h)(Y=bbwFf`&2-V}iYb^z#=QnY1yCS>q$S-sNCCIr)MyCaO zf_q)^;c!!-C~ONi?CVW85cLP)@T@v>>8%^pzQ>LSTl;?G&aBfX)Yi2KwneV7`T_ki ztt&s~=$ETUKf8U0yp{iGNdVfxkFbZx#Ty7|1XWQ<>>fJ2!s| z^K35tWgZ+OJ&(LsPuh}#{LJ~h;@I(*p`Xz{zY;9*EjwY@JAqMNSbDBE2)_`2^yTgT z=UK;-Z}8CMIU}J(M){SI?B*8R1e$&67+6tNSD(QD1Ur_Q1=Sm0{YGpC0z>pS|!0 zmGTi|6x`q;aCgl#jX*$}DS;2FX-yPOcUl}Qqfzdb7t@~%ip}e)ox~*yTkKPo9EVJu zc?dLuHj@qUKl)mqvT5@AY=6$i6c)oG8DVo(0Xt!c}j3lyY+3}Z| z`v$bbh)2k6oKj+Rr5=c!b*B-G>CuVK|M3 zKUU6*Q)wWzcv$b)z`sNg7_Gf*XsiU5%6!M9_>sU7IffC{Z}_#pSj*p^_0`U=tc3-M{vUss(N&EX`iw|6<{Vm*c?&E^jXxF)O#=tVogSNv3B%i-;Fnp~_ z*aKW9brF6xE!F*B*Q;3JsNl=$RSw>~#)_FIwAQnOkneU~i9O;0$xQ!Ii_S?ad@g?k zZuAqG9`<g*?(=W9=fGWSgX zw4O8R-*z2XLlL|lqtq7jqAY)=G4b4MP#ZxQh3K}HZ4~m+^8P~~{RdM1U0mFm{9ujm zA{;+iapZrynSWW~KbHP>dY<4J{W#YF?oFg|7&#X+x9@ARifu- z|7FYQs(!*?cd?@|W4mHn$b7m;(tkTmpD3AHRWV{!@Lm^~gyvv#j_+|EiYVR-J`n;nB4P0->q@4+P>^ zNvF21m#d%~1?3IL|El~v1(4}4O20T!4Svr@xd2{4bL68>pck6b5{K_+j$WNPeHgBE zyJDaIkA?DoP4d6L?jf&uwQq>=$YmYycu#|#zJplRPC=*0`V=#PAjAJr6QKOMN&a(Z z-u-=zOYJ+yKQzo`D-(iK$YX0k-0A*}Xk?rIMRo2xaqRs!G%Wuq2=}C)19mkar4n`( z*TSMa`~v`KIoUZ{4bac zR`r#~abu+xS@0fbJ6IYGWhb(xn`!1#t^?TL&A&&jH# z<+7jTQ0hr{S}8KUx*-nGqYZYRcGf+=`@k~j+xnL9u<#Wgud(g_JKlEwF)&bvP)XS2 zC7EDJ=6zvPBj!6#=*wqjW{l42($YJ{&S`NRV;f90P4ied=Wb@q7MVihXfA~K_CmbF zs-uFlP7iu9c)T`E<`d_NW037o?I7<1x{L$Fa!C7=M^$bqMv%cN*FN#Z zKxJiO`ORCm*dJB-Ys&Aw;S`boe-P)cTPFbL!PEoU-09U7pbD^>2XD4aUl{R~;yo6P@(Z6I~6RymuURo6xl;j7ovHADXdLVMk zM_>qRnd`KBg<7N`evF^ytBgF;wHBMZdxHBe8bo+wdK z@5#TjjEspJgG0Hu<2|~c+bUYyt@Y(f&FZnbRwwewfV{*pN4JJ& zoNA?WBx$Xzf3KMGga{2@aIa+0gQrZRF)$M+rup92MYn&# zv}NoG*C@A7nU<*0V&I|}>-jV)XM1D(R;$NrlI*VX3NPOMo~{eW#3jyKB1`F0W6cLj zo`l!c*;d|irK@K*cYO3~U{?}~6oHqoj@HW;|6}Tgvr^Xv;GW^q$na`dOUHgnV4E9f zn70ekW@<`ON5-buFyATpolN;G0OlE=jEj#Yr9*HdM!sB35adLWE#2Ihq+`MqkC5pq}IqZDSO}@tLx&Lr~;lY5y^ZPiFc91mmSOk0c~|Nl}CA zH*rFq!;xXzlF;93^-vtcudHQ@6`ezOMbB zJD~sN+rR%|^t7<#8dQBLTh(!ScBrbjom2xDW#izVrUWXnb(HfNoR~oP11ZdBX82cwz4$Zw51b^-SEKvEc!Ti_PJv_4uA&HmfsMWAg!KZEp zg;ecqN0Z*KK{E&Vy_PUwhiaVZx;5^dr7WN&I_7m4uNLNhp?JjHNp~W5e0{7*Ipho} zQRQ5{v9ED4zBc`bq|I)Zbh5qoB6z5!f*Y&kX?`pRZ|4KiT@=VjCgY6QkvWVPJxtHy z?&3v>9W$|+AfBVN_L#Wt$uOZV*_xl+0MpZs3FjVSV)(;#q|A}R*sjI15%7-413&&< z6Zk)J#Ak+EH(5P09w~7|W|LuYev&nTeE-WMJ4WNUO=Zn`SV?_zrW;)Xo1!0JKbb+v z-F6(+@zyrT0nc2?GPJM+t8)w*66K$KuHNUYf3NZ2LFT>9l~gTUbxB;Ue0e)f>}XT@ zd0yVt+T<)iA$)RgzBzMUUWvA^nJEy3JYs@SjLnXJLln#f}E zpAn&@7Q<0MzHMNH69>Q`-k&yuB85cQJyT0kv+xOq%8Uykt!=P1!zp;?roINOG5Un0 zzcEcO57|os^9;vOA6CZSwzccx%AvImPcUQfEcjt3x+F;QE4s!Jgp{ISnUoP{s6k#s z^3J&IV1qm^xa5f>s3|i(dqk0I&>*GCp6uMoq+dQPQ&WbVrQByCO4BXsmu1}CmjHIJ zGn6+ZzGJ<%$Lw6V$YQ*>hbxBmgPLW)P)S`(;@Qr?VA?Z!W8zp7RyQ1L2{Tz^3Tv=) zB$oxBkvTFC$r`{}lxJ}br~zkbLp5!enIRb2xu8vpbKO$OFN@#D&OEfC^igJh9x98d zTm!Wkx!_hF%w2j(%}G3*)psoKombHgUkwt^utr)^Ul3S1YBn%DHy(uu(Ge_ch%9>ZJ-0&2NDwDl=5x<98etuG zWFcgP$T*Zm*;UCmm(qjy^YP-uL!mv$xhNZfd)+%_g!ex1`9L#k_?ZkT&D%pcqZihPEtSFfLT>sEHu3iI}u-Wd{ zGtbHaPvjTos9zB@?la_IinL$poM|fNAYvEXnQ1L-ewDp_SCMBNLb;1J|UsiP@5a^R;cC&!KnpTs50OAhOJ8wJJdpt{;7w^7NMgN%15 ztAAIemo80r<*K?vV&;>{6k1%$MRp$h+j^w`s?A^fdbJ3B4N9s2Lz+iK92LI?ZY``2 zY`F#Fn=7g^g?xBU!;@EF4lS{OO>r=ovDC*iS3X70Y?QkjiRDD&p2#l}#dW!jaYi1~ z-PeKb%p*>F1gD*5xEY8QQyw6d%DK|2Z%EVq-G220<(jg5SWiu zyf|gqh+7;FKdWMlKXFYIgN#W$xv z+|jqKQeN^XoS1!eh7w$|Ipxw}Da-eA=U^(yZa5kk%da}MxluU=1~P+U{F-K9B_nd? zs@yAAYwc($QuyJV{*4kXaER19ZuIz<-+GwM-co1C5n6phTUqdfbpWDsqX2hCWPQ=l zY&X`_5%d5)dEJ)zg5UUkrz$x%`~uc;OymB|l`UEQ`4y;vL7x%u>eXX&jQaL1>YO=B zXB=F-txg{NMD!4BO>7;!$FQ`46Yc1o#)HvD(b6e*BWR-EW$q_9-3uTE*i`bh zb#<82Ai=N7A)lwtrh8xCjr2z4V{ZQ)C(UO*5x)&1GGMr17CSo8uc?}2R9?3h*}^7z zA4Z?Sp#z$OoAk6Mx@472KCxrjB-%V{8%ILZyr`eDNUQD`9{IV~-VC6(LtZ~hY;T81 z8!@m@^$VcbdI=`7x)a*aBs-tX%^87HK)swWl|^yPK^O=uV!;6F0gQ#8)7PX5G?80o zD}SvkeAJlEd8dg#_sIC=1hjpq003ZyC`rd_nr{}6v`Re?wBy3YlbbJ6BsUP(YDe)izX zp;l0u5EFNasbG$A_^EZF27)D`t;ux_@cLoFoqsa*m4!xzFs}0Ds)DtmdSI|Af>gjM zL9h}Mz8bYa+BlT2nJRJWKm6u{ie3wdweIJfsRlIu{v7MO{BU9&Vw!g-uA!-b<|yrd zMhqs580F5WG~onBPJ@P0w-M6TamFMU0gSu3J{TE6fa#Ovx9YL=RD{$0YfbTALV_XU z9j6w5ZvXPshSKvvkB?=9g+RDdj1<1R zN9(v{poAgL<9Q}z4Ve1rr2MVbuO)h+*QV!zgAU80Sa?W{pI*0CxYYY zjc^sB#m%3j-9y~?VbLdr((@nJcqcyTI#N#?5p#s|x)(ghDsiwt|K4drvoVlr;q-0% z)F(4?Y+7P{-b-f?Qumw0ktu7a|Nd~2jrW+pW){HsWr8>^YT=X4CcT^o zYv#PliixipO@F#s?=Rawc3s8%sJ{KT(VvIiHKIQnL+pI?ux5aV>6=t9moONbV@anu z8O>M&<%Zh@86^#-m>N3dbzBlR=!XE#0GPXDVYrU3Zd?G*^e`No*z3T_sip$;bOwpaPn~583HkB4=XJoKNYB48O8of)==lQ0FA^ms zbA$(PfJygJ&SVUv*LinRGfT{(R3XAR0?`udt>dRAsYXKNw-f%o^*>anMGYcU2n+f< zZaUoi-S36mF?p4fe$38C3~MH1s~B1!WlJ3Ldr4FUKWi>oj-gu#L0dt|MQ z`-GoQ32b)DtRJG9mdj89$8Njf>POaAL8mOn5{=@aje1U$kZ3F64QGCThRe1@&rs(& zU3=dxf#AK@mll5Izh2PzYEHSY#2~+hU1ssJOE+PJ;07v%5!V3JZ8<=(glbHb2SAY# zt3N#ELGW;=$FYnpy^rDx>P8c+QS>9Uf|4y#7*8Brn zX|A(B7FtTSJz4@ZO|22$R0*fBYEDIs8*5SkQb$}vYfOpao?bqd`$ebt>Z`{_@aGds zh>%aG3lhWAgtHVCw?)HY40)U|97`*4{5@f|(AM#23Df^+h*5lG$`=iM zVsHo`*vLLnZ1l(FChFq8-1$U#y2P|f4^%iX9Q^E0fF1Vt9?a3$y!Qd#?R< zsw*t}^oLJZezNmS+c%ZE;}M83=`L&pgD8rYLMU7bs9_vD1sss@VW+WE)4=XZ{*{Sz zOsOhYvda&RimlI0aaY+#PFpKg4Wf;^D!a4KR(gh#y;U5M#2F=LP) z0a}(f$FQc|o6!ty$ERPd-}Cc(?|-o3lQ(=g$1)zIMm*GQL(T`Wvy(u#q-Mg{Xi=bP zL>U(ovv?Yd!L&8W9o(p4hUoVE8Q1hk+ESD|6b+-uLDl|w`uIu6UUHAB?W#opuT}7& zr;9*4Wjv;}%C+5SYcA_WFUN+eyNj)>C$E%a?YNk>FrBw>d8sRep41l7>0CKgO~_j+ zmP!ddGh1;87c7xQ8@e5xgAraqFqc1F*!zFJ`=7%~u|LSGIN&f2mP*2d8G?-3*EBzo?jO-gCM!eqpxPA0a-%A~ z`4i;)%3o|9ZO_O;?yh4;6WZJ|c2ncYQt2cMdz(uO;99{=sxT%!#l@x_F&J_vu~rdE zcZR0ZoR4E$>9d4ZN8y1t&-Gts_`P<;tj!a1ou$m7C+`KQK8VZfb5|k=%7hB3D=Q?Y zBP}{qDgh6TM6`luXHK=`LW(lPyKY5byxS$N>h59J`KHlesLmY?pVoL(FG-EZo^S^c=i=h>*TU zfhNXGyvaHKdSSqULZA2YFZjx~awDn`40DEk+d3$TDO7eg(U}BjhP*EV(dJ_luAc&3 z$92TSgZ9p!qKS9G+_$~N<*V?sRmK-{wNV-ik6IE1pGr@r`*`Jaw2=GBq758w?I5A8 zQ`u_5(o0+;DD^nJs0OTl8j=gbA>Ls!-#gd2f*Eovh@G4C*G#$zEAG?7N4eXA;H!EN zxwQcbvI>uVD$b|i1IQ1grJaN5yzsz`-OTx^nSkX=vD2dQ*oz)wVYOBp2pOCNIT$HZ zW@A06L8i)4%j%R}7Rn|hL2BQf6HXEPD(jxwjiQrB(Fdg29k+2HktTbGJ-0I`7>>t4 zM@#>&So`;MKD7R}8J94SG2qmrUWf^Wdn$dv5%`&{Mw+00aV?@TGM+ORAlhi*O@iwE zMQ(-CAYAn{S?WsGaZ-t08vD*AIW1k(jH^*wgXvM=T9Dh=3@h~BYtF@TF0d&+y&<{2v-U+IY0-Tq*F8FiozjaYiK87>DZFXqPAO`RR_tW;){&&Q z{mwMM5PGSs)z1OX^C5`xL`5t+us_pNSQJ3aNrjaEBtn6?tt4`-htQ4_!1UBbdNbIm z@&W{=ci0S_P9cF1G|3Qy-zt!!8ziHe4Lr`rgIllKK4)hAfM_cf2jm%6r_Ayx_gT=; zUQbO>KH75!Hl=f`5j8t6W&@7pvlJW3=?w7j&QUO-8D)xj$b)7jsJbxo?e3D0`MVr+ z(U%WI9wJmcVx*XbezJA2Y%Vt@Ih+#Zm}ehN%Buqo|%ez zu@sCCkq38%ak-TF=|yJi(N#h zZpG1rJXP;?siujx%ThJ&W^XJ_2@j3HfU2&fxVO^e1sYd+3pfz>Dzkg4JT*LYgN-y= z7^>@?_sCwJ+u!))!=s;lAn*O?Zkg7{iyjqWVXLfWre@EyyV%cr-yuM~#wATmsEtT7 zlozwh5~S*~k0g+!?n>5LVXVd_N~ahY81=!dGi$Aq!=tb|G777bv)$f~;OEq_EOOapA?-^u z%F?>~540OfUGoNG`0!Sm2yWq^m~Ah5b_aK+AZr-QVmXbF6&}H2a8bsoHk!Zu%%-;@ z(I?ZNgiz5BB_8z_5e@=4 zo<``hgC>F2A!80;ERA&+vl3<^r^CK?J>IN~G?xu2skwGoXEZ&$Olf9s6p_vhw0WT! zr&W_MMdS!F1C#s$f%9ib#R^5pL`c{ku5dLh4%S zadpKESU{9w{JE-HE+}fIVv5@5Dml=0y&@;Npk!kCjQc{qj@*4f5ytLW^%kD{{s(Q| zC&jss*xr~r^>&YL)nE*$hn>RIt;IQo1EauQVGpxgM9;wCCV2rkv9U|OvmXgx5q zt|xVV-1Sv>1oZa;1& z{>99I#(!KIACxVL@h;;od5Tp$4A}?vbq@{e7UT&bceN(dCyCxcoI!S~Kqmof-+_jG zlQ3`dn2`Rp;uRW|D9xl)`{XDywgq5=-7Nc@q*bOJwN~_$n)|HfgY}Es9VM(*opU9l zReBraP+yEALS(90UIq*$#1~#p)`V(`F}F5zhs6SEuz@XTfy-Q zJ_NbHxUW!Z1|=}(yu*x@#Wj#e-^B77qZDzO~g$;})xM41Z87 znY+~9l%9K7(I##C#Q0;A(BGQQR#-WOf~0s1#Y1dIH%vCkQk}+9uCuHd(bR0N-u@Q4 z{k8_Mhrl6EIWC?=&xb%f^;_#-J!J^Jk&8=VO}f-}RzR}fo`tV{P^lX0~R@9^`|jD5lZ!ina*?GW23q z3NG1h>w0A`0tSeXufk!IJus)2V1nz*UD3vpBm%amJB=sS1ImNE5rgd&^ma;_0TPlD0 zaIG7Jz263Vb|DC(H}BZh)*b0(&f;*x-i12Yf5Z%1AH>c}@~p;^rS?h!=9HN#>agaERvQdSQB;l-13gvyv^hgv*9Tdc1RAJ1`L*AcQ+}0H#5c`%$80@U#C zoeRA#ti-RgLPSWqFJr5J1Ogjv(vh2VUEwY77gjSva)IgLifgs&6lgVqsISnxDw`Io zjI&VjkUUyTS@C2~lNp>GVwr;n)kNQvR;NL-#|&FlT>-fcTybE21k0m_gq!LH<+0kKs;6(1R!aE ztC|RfqL+XP7`*3G`}z5;x!v2Z$xK&5xxk5l_Y6?m&l< zJXQFB0Z(@=ExOxv1^{8 z$h8y5*FatEPoM5Wj-z(xhW8n>b`;K#E6Hu3!EwQK zWb-G+mfO$Cn?k-(0*DfL?O78@PSxe2qoz%SOst#3 z4C6p3N90ORi?Io(GH15>DKQpOd^t^AF~2oK9OkJQX10w0ZUI`5d~lqT6GNiM2-unm z+X!TiW+*-MK&zZQbGkt5Uz5jt?%QdxWlDn(m zXy`yT)n=IP{>eiF+r4i-j(%DsV0VKF}(O2x_%XMMRZ1t$@8N>_QJRWgl>R-+7EwnjXHtxAx;fKL% zeRik=CTh0OD}V(qv}u!^z=(Gs`5|l8@((^8ECR9S_Q|GACV*o5K2r!nfQoTBk@~&> zK`2z3<_Gsoqj-)V@W$>;LX1NFsbRV*E;1}h@?)O7wZEOKBqD2EZVJanqYTXaA;*@}0ydxv7Y`P0(Y&ncC-i30#lWpP!f!B;OK z=+VNX41zjvrp%sQ>?v;b;LMDRb${jSvIkwmeT~XoAHlA9CII}-3(SeTfsC!-t19)} zt3(I5?2b;YJR1bfMw)~4d{0%L$Wre^)^2R*R@X*jazXK4Z}9%V@!b^Tb1i~vLOC^8 zve5&5fNJ3eD1|O8JLb2=5}4Tat`TTZl?>X}RNh=}e4ZtRU=($u%K zNPrYt?y`+$fL{XaUe+GtRi;)B@J?f^&7c|dq>zc4`+4^HKCis9A{wCJKJwhbNz0zC z^*YjI>oXk2MNacJe+PVoooi{kCLGcOEne7M_6q>tjsvAoPKQepzZ2m0QcSbd8ol2e ztOuFoCEo0LRSXcM$I&q@Oiu@VfGm(*nVI5qU zTKhJ)+<6yC4t6NN)xqOxS(m4*Amcqe)@QnRsSmfDMXU9mpP!iqOWq-Z?a!s#e#5QR z`nsbdc&Skc{K8fv#pFyANf`!+m71XdjyOP2SdE_{Vf(4Iyd2G?Y=zL12r!eLYETMSG7Q1i&Dqpd=|HIln6`| zxRMc&J2M0y?R-_ZqS%Jiv>I^dFJkx3bv-mQs2AN&6=*N*Ny}|U1TJUKoQX~meN=ui zWzO79UsW6k1F9up&5f`6*Z6llHZn zKMn+nF6S!C* z@TfS1?C21|Q(~ZG$=LCN|2- zADRSk|Bvw+pcXNc0#aMz&8z@$U|~c%ph(C1ji}`aylO}CW($yJ03JQvyKs7$(;RY9 z7k@wf5tK68#V*EcwzgfBiDr*;v_7#nSxo{k-J?<4CRj?vfZvKn&*2|Q%rmVEvy)L# z4$U)R4MB?W7)D>(O-Khx^uqXNs|zOp*XfE4l=;u=;)%N4;x9f5OP!hOL(1mJF;P^X zmQFuuSqRakIBzyO09esZ%a#;lCZZ<-p_m1OsZEWg+fWatZ73^Z8X3v^TI)GKqm>n^p5DK zDY?jw+gSn+QxPyZ1$3TenItNY@_(^myfDID5OtUxSpogZGl)gzBNR z6K+@8OM&2+7cSiyzGch7z!|6*lF)p~j&2gQ?4~|*;_ZQv@!iQ}*7G9u`eZe@vIMA@ z%L0fGlndqqDz?T)P_XFZ4MG!%Gi>)HPj7*`Vku_HS3yP5GE{mxAa1?yUs(O8fq~YN zEH;egT4mDVSzU|Xk8$?|+EpkUj;A+9;3F&3EMyifH^T+&Fmk#;vLos35X1VcMQiaL z&6ds=S0vvO0FeObDCpfD-I<=8@qh?K`b`}BKnvJSw+}-c2?VrzBX5D!TB|}9R3R+w zGbZlcZ62jGvnpY)sQfFs0fwg|6lJspJC+1!KzRfKy1zE}6_fX{X4Be6u~=m zgc(9Pkek%UtNq0ToX~n!k1WuU zf)qZ|Tv4;aZ8*AUxqTTx>N{mwN2-eLsep(>{);0m98vRu2x66T zNsb={s4+&6t;qIY`gI?xfL5lg_~z~6<>KAn+2~bn$ZfCMC_wQ~;5ayyWMV`~gz8%5 zkOcx4=#|6Of(3z|UK!dY(DGAl5!xe(bVgxq`?%v93+l@g9_?~)RTF^7fAU={qq&P9 zk7=1#AsOyaKqJ%kg;foYLf?Tetbb{CuFCj!vi8-humfaO%CD-VwaVd)dUY&jIXPOd zIx=(|>R;WfN*usBRE2POF0^S3nFeUp$dKE^d|?IbmSqQ;K=#BD0a`^SQvI1oR-ZH+ zGdIAx4n(^s`>oCdP}IDYA+0HO{SB2x0Hv}(p9)1w-8V_K<)SM1TnNDv@N$u7^;5=+ zAvJK(O0?!mR>GCk&U+PQpxl@!4S--;S1`~CNd>A1pb5io8ips+P@b&Nz&sS-cp#~N zJLK(9@ZPzq5BzN{Wpn22-dD;#s3uIs;e{#}V9E0mgx-yaK1J%(qA21DM|G&GjQBu> z*Yvp@02y>HJ>K8A$F(Wep*t@e-+JJcD(zAMTS;#{?niG=GIl zx2OK7mw;RRc541I_#+GI#kzj<3HxqHj*7-^5=q=?VxZfHl*TMqmW#Dbihd;?(H_M* z-4=uflqE}-<+-ecb2RuWNk!Bx4?_i|N>vMI4Q8C;3+yY4IyTTy2he26+xr{u23rb4-<8WBiHLal%b%q&8J0g^!>1VWO5+$Lm5 zvM-+J>2tp9Ci~@lI{B15Fz@?+|JUFDGBX^it%P((X-hVdHhv86t7eTvaJ+GmdN7^B zGrpGoT|`FB6(e(4A_tW{|^B`-Uuzr%g&Xi%&ZfzosQ3nm_@gE7%7zPDQ`@U87qYTl&+`8hO|zv#=E@9W1!)q z=(WDjn)xz$>Si2-%ZZ!dMoe|V;%L=zN&Id=EH_q*Lp9~e%g{JImSJs*cc#K28 zjH-#Th$bK+J;YIAO$a;?ul|X>ILrBYK_M+xHu&qpD=$&md;n|!E`%$@tB2LOGxv)R zKVR5q{6WttG4GuQ*tr>W?WCO&|8K3$O^+*$^T) zx41ujh`%6 zKsOX{lKy|9qPB0V=2P^xA?{PI(PyDfbsKca?K%}`@=BdfQcVa594N$tlh=hJAd!ZZ zks8+{MeT4y-qG@;o&M3noHiOMRc7wGBIIOQvsAcB6S2r>cs!fO3_bulu(3^JkZrbl2@26Z4!v8dt{zN6F9X5`;DCKwqbIi{dfiiMR7(^e=22lN zBC0wmQ0g74gug&Q`-4!gbKLWoF;ebcWl+c2!g%4;i~>2HEIfuBvewv2JO}v^M8(1$ zhV#5z*t&NLL>F=v*XRJngVwP8(Q6NrtWQNk`;^E5jG(?rjFqU)w8{noDWp0WRQO1Q zjVoX+D9*QvnlOv-Nl+4=IT3z>o0?t0Xcz^r_jLi34)o8#2l-r7EUhpCh@2w9Y$FFy z$Uv?$^kh@^`3?!dZM5)ZWl0uo8bmNP;kK-A*Up*Uk(C{h-h+IdK_|N?t*4pUybKsh z&+ZICKg6#eHqEb)0ZGGl8LN?>%WNxf_g5Zs*2w&4+6kL}p^buPOZ&6Fw65%eo0+}5 zG)smbG`>&Rn*VmRKq~R3a4!*;L~3kz3$pHDHn~ouK#XIXbp!2$dgVsI559Y82!kF) z-?T`=E3s@FS9PvP`DA!Y>QB0V=#+ru<$09`ytI{R@Sei?8a=8%Wg`Ses=c$snaGfHUf; z0NVhx6)2e|mOU>M)9H$494VJX4lt})k*Y$r)RDw0V&QTt>}ll04@r-Xyu zswQeU4Xw(LjN>(c=*_~6*9f?^Hv+_3Cg`PG+U{TpRxn2edfa4jcj~+UwjL7aiH`j| zZZRlav%qNCqF;a}&ZfHfakx zVr0XqlJf-a5TX zA_pdfSF&dQgMo5i7pNyShSVN_**SclEqT~-simPOZ;$K&oGW7$jmrjg zo-8TwQrw0&&GyTjQ=Q`F=-@;rTN6Uf*RT_~_XGwu>wi;?(vbsT`mjhW8+==3zK^t5 zVF^36qm8m0tqlzBG&msZ6{cUky=&3b`QikcdvH7sT!cb;CA}!Jnj|fv6LF(THdR78 zz%CA+m(BBk(-Iygge*6b6;(|{CdQo_NVPUyL~75 zU181r60K&sRMbzoLirT#W=FS^$8W+isIUOhdnIzJz1mRS$h1#?u(^NjMDFG^ zF-I~5DYxUrp@9IQ;3{ORPvh;u{>as^W5d)JQqWyw9-WGRmQli(kf8^Qv??k}Dh>Q4 zklj181#TWXT7c@kvvjTxi>{to&+Jt^!uuVj$1U3XL$9hAMYvwzL-TO2;M~05qz^JhOII4GO@UfbD_hkn&u3gKRt$HWW zq7$rcOK-U5RpW5atD4PM5^qR2H>2BSE*-6%=>8GE9Kue|)y%gwzL;OYe<~%%YR5T} zedT+FG_7XpzZ0#2SVyArBWGtGU56*YyR6}{Ov_@5Q^>JL-o4y!iNj0!nfY8-84ao6 zhmc2PouJYGD)*`g9AZ7Qckl+a2qEGl=#vEN8j_CvUzu@mw<7U5t z!_me&RR1sKXv7x=~n z=S0W+)ok(jK&`%Vpz>^_eV2aMP9OEiZGW9X4(q1ZFvxgxD9u3H3p2fMR8kG>+sq1X z#hWUDP%gZpSK1qXQZTe8R(%Xk8zx|xO6>7qPUDNtPWW&>D&r_z#C*^e`bAeuOuI^X zK)@MrRx?|zQH+G*Vpa2Ps#o*(s|h4U{p>oc>(c~PjEP8VhhM{TB~+8>m+)R-1TQ!( z3qIsqc;(pgdiW8+3R{GvIP$}*?DXY4vyui?g*LI6m~p6ZM9E;oy{OivZDl%Z`u#rf zu|(A8nm}@YKlX}v*i3}n!b;xhelH)qd_4>Kf{Srw*qKl?(DM8s=s=md46ot)4M*WJ z33nw8H%Cw%wC3Tl7Z(|M1KT7Fo`~wE4B(w!hK=OiD&imq`T9puNIm<_{1DI~b1BPc zte_(Kw*v{j%l)1RaR!|i#XwTn%u(3DGric*J9BE5UsLDeck@-lUj!7I3QU)`A34~Z zC++s{3%bXZ=LgQ%tRL5zdq6m zb4uE1O4Hb@4Mv{BD5i046AgL!d>Uq4gqqnh-Uf0uqQiqX{Q;1SSi;httfFGd{(xCk z#dGQN`NHXHgDmgTKo%EomC(2p%|BkLsLUSqkTg@dH`DS~fYc%Ka z*O)0nF+8g$-VLu-R&nx;C0RC;w8#sQ<~fYhq6)Q{|{r+WIR3Tg3G_JeXJ$c*x*9 zo5T2Lm62cdh_`hJPEYp+=%X_7L!OjL!5A<0MMIxYQ@oktYC^fF5Z5>L znNNGGuo`F;HE3r0@~X+A6SR!GatDcNd*u$mPI#@am(7HkY&~7|tutBdOGPykgu-`p79;u z@vekYuu?u(A-(jl#*8WYQfk0LGH(Is$mV0=#qNLmy#c~(%B z$r%O8>8@3G#2ql1V7!qbFa)S`!yVfV5;!DYAl6tljtJx@6dzJ6Z% zt10}8FYq!}dvONJD)&SfTH*^<+I#`U~-dA#yv;R zWq&lY;}lT%)$XgmZBmVg4S)16&h+$8Lw}x^&Mkxlt{rvYI#W^FB8z!0{2NxSEBPIY zYw!Z{O>j`SWr)9^$h_@lA_+_L4W@r34@`KD_9RjQKIF&nQ0&BzG}IIGSB`fYF>ieQ zu*ab)K@Q*pWZ0oPMhpdh48Aov5PjZv8_qK^ImcoISEGBr+~}V_te#xnpgw#u7!PJW zS;c(tZrPu}1&O>VW^7~2QNf6T%+uj9Jhzg5dVO%?Y(bZLI>K7~b_Q=;sx$r|$PFMJ zje*G4$A8hR{(5c*3&hV$H}^9R)aT^KUytA3@So&cXVIb6s)|@VQ?xz%)1WI1ZJi^i ziI`XPp#nAT_=z|mx5y+;`3j_cqDFZ(1SuYGk<5)&lh(76)y1@pTgiV~iF7U5{pF4j z9Q~Hry%DhYc16ctc(;<5i1hf%3qqL8d3n`OQA75Dy52>arcYcSOfFJsG54zP3WA-*+tsd0gS~_#Cw@Dx9|}Owtz3d&kV2>{A^P3xZ2# zQZX-`g=Zk9@x6Dvqyd&C#eOAn@f?0Yq{A_ zLU2BLj?EDaa{WY)XGvqTbBg|0>UoBHTKy#3xjN{PPf*?-dUqhwZ$}J+<#G3F;?bo*ONk4*w^2$OgSSc;oc{R}RU*La~VdoB#2&&J8S@^U;HJ6}063 RGvKph+pa& Date: Mon, 23 May 2016 06:36:25 +0200 Subject: [PATCH 431/449] Serialize application setting as Array by default --- CHANGELOG | 3 +++ app/models/application_setting.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e5cdcc2ecc..3e459f8b2b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. +v 8.8.2 (unreleased) + - Fix Error 500 when accessing application settings due to nil disabled OAuth sign-in sources + v 8.8.1 - Add documentation for the "Health Check" feature - Allow anonymous users to access a public project's pipelines diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index f5079f9244..9a14954b4a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -7,7 +7,7 @@ class ApplicationSetting < ActiveRecord::Base serialize :restricted_visibility_levels serialize :import_sources - serialize :disabled_oauth_sign_in_sources + serialize :disabled_oauth_sign_in_sources, Array serialize :restricted_signup_domains, Array attr_accessor :restricted_signup_domains_raw From 9bb0d0b4079bc88134399f175ce9631005044060 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 22 May 2016 23:35:18 -0700 Subject: [PATCH 432/449] Fix Error 500 in CI charts by gracefully handling commits with no durations Closes #17730 --- CHANGELOG | 1 + lib/ci/charts.rb | 3 ++- spec/lib/ci/charts_spec.rb | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 3e459f8b2b..9fa1e1cada 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.2 (unreleased) - Fix Error 500 when accessing application settings due to nil disabled OAuth sign-in sources + - Fix Error 500 in CI charts by gracefully handling commits with no durations v 8.8.1 - Add documentation for the "Health Check" feature diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index d53bdcbd0f..e163663693 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -64,7 +64,8 @@ module Ci commits.each do |commit| @labels << commit.short_sha - @build_times << (commit.duration / 60) + duration = commit.duration || 0 + @build_times << (duration / 60) end end end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index 50a77308cd..9d1215a576 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -12,5 +12,12 @@ describe Ci::Charts, lib: true do chart = Ci::Charts::BuildTime.new(@commit.project) expect(chart.build_times).to eq([2]) end + + it 'should handle nil build times' do + create(:ci_commit, duration: nil, project: @commit.project) + + chart = Ci::Charts::BuildTime.new(@commit.project) + expect(chart.build_times).to eq([2, 0]) + end end end From a69ab104de873f11dc9004edbf39210000d4e50a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 19 May 2016 11:34:40 +0100 Subject: [PATCH 433/449] Added remove due date button In the sidebar when there is a due date, a link to remove due date becomes visible Closes #17392 --- .../javascripts/due_date_select.js.coffee | 21 ++++++++++++++++--- app/assets/stylesheets/pages/issuable.scss | 4 ++++ app/views/shared/issuable/_sidebar.html.haml | 14 +++++++++---- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee index a4304786cb..172b4ebe7b 100644 --- a/app/assets/javascripts/due_date_select.js.coffee +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -11,6 +11,7 @@ class @DueDateSelect $block = $dropdown.closest('.block') $selectbox = $dropdown.closest('.selectbox') $value = $block.find('.value') + $valueContent = $block.find('.value-content') $sidebarValue = $('.js-due-date-sidebar-value', $block) fieldName = $dropdown.data('field-name') @@ -26,8 +27,12 @@ class @DueDateSelect addDueDate = -> # Create the post date value = $("input[name='#{fieldName}']").val() - date = new Date value.replace(new RegExp('-', 'g'), ',') - mediumDate = $.datepicker.formatDate 'M d, yy', date + + if value isnt '' + date = new Date value.replace(new RegExp('-', 'g'), ',') + mediumDate = $.datepicker.formatDate 'M d, yy', date + else + mediumDate = 'None' data = {} data[abilityName] = {} @@ -43,13 +48,23 @@ class @DueDateSelect $selectbox.hide() $value.removeAttr('style') - $value.html(mediumDate) + $valueContent.html(mediumDate) $sidebarValue.html(mediumDate) + + if value isnt '' + $('.js-remove-due-date-holder').removeClass 'hidden' + else + $('.js-remove-due-date-holder').addClass 'hidden' ).done (data) -> $dropdown.trigger('loaded.gl.dropdown') $dropdown.dropdown('toggle') $loading.fadeOut() + $block.on 'click', '.js-remove-due-date', (e) -> + e.preventDefault() + $("input[name='#{fieldName}']").val '' + addDueDate() + $datePicker.datepicker( dateFormat: 'yy-mm-dd', defaultDate: $("input[name='#{fieldName}']").val() diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index d06086a581..787c387379 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -150,6 +150,10 @@ font-weight: 600; } + .light { + font-weight: normal; + } + .sidebar-collapsed-icon { display: none; } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ed1b8a8da2..c1eec45019 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -87,10 +87,16 @@ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed - - if issuable.due_date - = issuable.due_date.to_s(:medium) - - else - .light None + %span.value-content + - if issuable.due_date + = issuable.due_date.to_s(:medium) + - else + None + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + %span.light.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) } + \- + %a.js-remove-due-date{ href: "#", role: "button" } + remove due date - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .selectbox.hide-collapsed = f.hidden_field :due_date, value: issuable.due_date From 9052ec8d37537aac58d6ede8faf1b021b7ea60e9 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 19 May 2016 12:19:19 +0100 Subject: [PATCH 434/449] Added due date tests --- spec/features/issues_spec.rb | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index dd114db7e8..749ee01890 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -489,6 +489,43 @@ describe 'Issues', feature: true do end end + describe 'due date' do + context 'update due on issue#show', js: true do + let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } + + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should add due date to issue' do + page.within '.due_date' do + click_link 'Edit' + + page.within '.ui-datepicker-calendar' do + first('.ui-state-default').click + end + + expect(page).to have_no_content 'None' + end + end + + it 'should remove due date from issue' do + page.within '.due_date' do + click_link 'Edit' + + page.within '.ui-datepicker-calendar' do + first('.ui-state-default').click + end + + expect(page).to have_no_content 'None' + + click_link 'remove due date' + expect(page).to have_content 'None' + end + end + end + end + def first_issue page.all('ul.issues-list > li').first.text end From d3bdbf55c7505f7d25d9a105d894b7289dfc6bb1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 23 May 2016 09:33:48 +0100 Subject: [PATCH 435/449] Fixed issue with dropdown not opening after removing due date --- app/assets/javascripts/due_date_select.js.coffee | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee index 172b4ebe7b..3cc7018517 100644 --- a/app/assets/javascripts/due_date_select.js.coffee +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -24,7 +24,7 @@ class @DueDateSelect $value.removeAttr('style') ) - addDueDate = -> + addDueDate = (isDropdown) -> # Create the post date value = $("input[name='#{fieldName}']").val() @@ -44,8 +44,9 @@ class @DueDateSelect data: data beforeSend: -> $loading.fadeIn() - $dropdown.trigger('loading.gl.dropdown') - $selectbox.hide() + if isDropdown + $dropdown.trigger('loading.gl.dropdown') + $selectbox.hide() $value.removeAttr('style') $valueContent.html(mediumDate) @@ -56,21 +57,22 @@ class @DueDateSelect else $('.js-remove-due-date-holder').addClass 'hidden' ).done (data) -> - $dropdown.trigger('loaded.gl.dropdown') - $dropdown.dropdown('toggle') + if isDropdown + $dropdown.trigger('loaded.gl.dropdown') + $dropdown.dropdown('toggle') $loading.fadeOut() $block.on 'click', '.js-remove-due-date', (e) -> e.preventDefault() $("input[name='#{fieldName}']").val '' - addDueDate() + addDueDate(false) $datePicker.datepicker( dateFormat: 'yy-mm-dd', defaultDate: $("input[name='#{fieldName}']").val() altField: "input[name='#{fieldName}']" onSelect: -> - addDueDate() + addDueDate(true) ) $(document) From 85031223cd89db86a9ae2347daa1a6f9f5b26ff2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 23 May 2016 08:47:25 -0500 Subject: [PATCH 436/449] Move tags into column --- app/views/projects/ci/builds/_build.html.haml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 962b9fb259..e23a3782c6 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -42,18 +42,18 @@ %td = build.name - .pull-right - .label-container - - if build.tags.any? - - build.tags.each do |tag| - %span.label.label-primary - = tag - - if build.try(:trigger_request) - %span.label.label-info triggered - - if build.try(:allow_failure) - %span.label.label-danger allowed to fail - - if defined?(retried) && retried - %span.label.label-warning retried + %td + .label-container + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail + - if defined?(retried) && retried + %span.label.label-warning retried %td.duration - if build.duration From 9c1b3d88db1482bb1fb394275c8119a77fdf9b88 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 23 May 2016 09:30:54 -0500 Subject: [PATCH 437/449] Move build info out of pipeline conditional --- .../projects/commit/_ci_commit.html.haml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 8228c067be..290e19c69e 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -12,16 +12,16 @@ 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 - = 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" - - if ci_commit.duration - in - = time_interval_in_words ci_commit.duration + = pluralize ci_commit.statuses.count(:id), "build" + - if ci_commit.ref + for + = link_to ci_commit.ref, namespace_project_commits_path(@project.namespace, @project, ci_commit.ref), class: "monospace label label-info" + - 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 + in + = time_interval_in_words ci_commit.duration - if ci_commit.yaml_errors.present? .bs-callout.bs-callout-danger From 3fa32fa2d5c2674bea0768fd4e70132c71e19051 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 23 May 2016 16:36:00 +0200 Subject: [PATCH 438/449] Use gitlab-workhorse 0.7.3 --- GITLAB_WORKHORSE_VERSION | 2 +- doc/install/installation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 39e898a4f9..f38fc5393f 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.7.1 +0.7.3 diff --git a/doc/install/installation.md b/doc/install/installation.md index fa11eb9ba6..526f127178 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -394,7 +394,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout v0.7.1 + sudo -u git -H git checkout v0.7.3 sudo -u git -H make ### Initialize Database and Activate Advanced Features From 27a5a4e4306be921a2b6e09f2c306408b828c0cd Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 23 May 2016 09:46:06 -0500 Subject: [PATCH 439/449] Put column headers back --- app/views/projects/commit/_ci_commit.html.haml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 290e19c69e..1abd4e6f3d 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -37,5 +37,16 @@ .table-holder %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Name + %th Tags + %th Duration + %th Finished at + - if @project.build_coverage_enabled? + %th Coverage + %th - ci_commit.statuses.stages.each do |stage| = render 'projects/commit/ci_stage', stage: stage, statuses: ci_commit.statuses.where(stage: stage) From 526da93052dd794c82f49c1b908d9a4c33d91dcf Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 23 May 2016 11:47:19 -0500 Subject: [PATCH 440/449] Fix link to container Registry user docs --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index 8a6adee2fb..d1345ab249 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,7 +13,7 @@ - [Profile Settings](profile/README.md) - [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. -- [Container Registry](container-registry/README.md) Learn how to use GitLab Container Registry. +- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. From e8f869f96b81d2621b5914c9b7ac95073fcfde05 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 23 May 2016 12:04:55 -0500 Subject: [PATCH 441/449] Revert commit info change --- .../projects/commit/_ci_commit.html.haml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 1abd4e6f3d..ce5c550b44 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -12,16 +12,16 @@ 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 - = link_to ci_commit.ref, namespace_project_commits_path(@project.namespace, @project, ci_commit.ref), class: "monospace label label-info" - - 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 - in - = time_interval_in_words ci_commit.duration + = pluralize ci_commit.statuses.count(:id), "build" + - if ci_commit.ref + for + = 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" + - if ci_commit.duration + in + = time_interval_in_words ci_commit.duration - if ci_commit.yaml_errors.present? .bs-callout.bs-callout-danger From 013bccbd93c4d44994126a52bf997ea59ffe594e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 23 May 2016 13:06:36 -0400 Subject: [PATCH 442/449] Enable Style/DefWithParentheses cop and fix single offense --- .rubocop.yml | 2 +- lib/api/helpers.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0946ef5d84..d09e493ef3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -152,7 +152,7 @@ Style/ConstantName: # Use def with parentheses when there are arguments. Style/DefWithParentheses: - Enabled: false + Enabled: true # Checks for use of deprecated Hash methods. Style/DeprecatedHashMethods: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index cadf9f98fe..2aaa0557ea 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -29,7 +29,7 @@ module API @current_user end - def sudo_identifier() + def sudo_identifier identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] # Regex for integers From 51c167eeb75f429098b7128f9d9f33a6eec4145f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 23 May 2016 13:10:42 -0400 Subject: [PATCH 443/449] Enable Performance/RangeInclude cop and fix single offense --- .rubocop.yml | 3 +-- lib/gitlab/bitbucket_import/client.rb | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0946ef5d84..26929e0345 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -957,10 +957,9 @@ Performance/EndWith: Performance/LstripRstrip: Enabled: true -# TODO: Enable RangeInclude Cop. # Use `Range#cover?` instead of `Range#include?`. Performance/RangeInclude: - Enabled: false + Enabled: true # TODO: Enable RedundantBlockCall Cop. # Use `yield` instead of `block.call`. diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 9b83292ef3..8d1ad62fae 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -121,7 +121,7 @@ module Gitlab def get(url) response = api.get(url) - raise Unauthorized if (400..499).include?(response.code.to_i) + raise Unauthorized if (400..499).cover?(response.code.to_i) response end From 2de1109da39a46200e741b5a6643ce7fb1418041 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 23 May 2016 13:21:02 -0400 Subject: [PATCH 444/449] Enable Performance/StartWith cop and fix offenses --- .rubocop.yml | 3 +-- app/helpers/gitlab_markdown_helper.rb | 2 +- app/helpers/tab_helper.rb | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0946ef5d84..861dde4149 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -985,11 +985,10 @@ Performance/RedundantMerge: Performance/RedundantSortBy: Enabled: false -# TODO: Enable StartWith Cop. # Use `start_with?` instead of a regex match anchored to the beginning of a # string. Performance/StartWith: - Enabled: false + Enabled: true # Use `tr` instead of `gsub` when you are replacing the same number of # characters. Use `delete` instead of `gsub` when you are deleting diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 3a45205563..0a1b48af21 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -13,7 +13,7 @@ module GitlabMarkdownHelper def link_to_gfm(body, url, html_options = {}) return "" if body.blank? - escaped_body = if body =~ /\A\ Date: Mon, 23 May 2016 12:31:52 -0500 Subject: [PATCH 445/449] Copyedit Registry user docs --- doc/container_registry/README.md | 83 ++++++++++++------ .../{ => img}/container_registry.png | Bin .../{ => img}/project_feature.png | Bin 3 files changed, 56 insertions(+), 27 deletions(-) rename doc/container_registry/{ => img}/container_registry.png (100%) rename doc/container_registry/{ => img}/project_feature.png (100%) diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md index 5af47f7120..4df24ef13c 100644 --- a/doc/container_registry/README.md +++ b/doc/container_registry/README.md @@ -3,33 +3,48 @@ > **Note:** This feature was [introduced][ce-4040] in GitLab 8.8. +> **Note:** +This document is about the user guide. To learn how to enable GitLab Container +Registry across your GitLab instance, visit the +[administrator documentation](../administration/container_registry.md). + With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. -You can read more about administering GitLab Container Registry on [GitLab Container Registry Administration](../administration/container_registry.md) - --- -## Start using Container Registry +## Enable the Container Registry for your project -1. First ask your system administrator to enable GitLab Container Registry following the [administration documentation](../administration/container_registry.md). +1. First, ask your system administrator to enable GitLab Container Registry + following the [administration documentation](../administration/container_registry.md). + If you are using GitLab.com, this is enabled by default so you can start using + the Registry immediately. -2. Go to project settings and enable `Container Registry` feature on your project: +1. Go to your project's settings and enable the **Container Registry** feature + on your project. For new projects this might be enabled by default. For + existing projects you will have to explicitly enable it. -![](project_feature.png) + ![Enable Container Registry](img/project_feature.png) -3. Login to Container Registry with your credentials: +## Build and push images + +After you save your project's settings, you should see a new link in the +sidebar called **Container Registry**. Following this link will get you to +your project's Registry panel where you can see how to login to the Container +Registry using your GitLab credentials. + +For example if the Registry's URL is `registry.example.com`, the you should be +able to login with: ``` docker login registry.example.com ``` -## Build and push images - -Your registry is accessible under address configured via `registry_external_url`. -To start using it you need to first build and publish images: +Building and publishing images should be a straightforward process. Just make +sure that you are using the Registry URL with the namespace and project name +that is hosted on GitLab: ``` docker build -t registry.example.com/group/project . @@ -38,18 +53,25 @@ docker push registry.example.com/group/project ## Use images from GitLab Container Registry -To download and run container from images hosted in GitLab Container Registry use `docker run`: +To download and run a container from images hosted in GitLab Container Registry, +use `docker run`: ``` docker run [options] registry.example.com/group/project [arguments] ``` -## Control Container Registry from GitLab +For more information on running Docker containers, visit the +[Docker documentation][docker-docs]. -GitLab offers simple Container Registry management. Go to your project and click **Container Registry**. -This view will show you all tags in your repository and will easily allow you to delete them. +## Control Container Registry from within GitLab -![](container_registry.png) +GitLab offers a simple Container Registry management panel. Go to your project +and click **Container Registry** in the left sidebar. + +This view will show you all tags in your project and will easily allow you to +delete them. + +![Container Registry panel](img/container_registry.png) ## Build and push images using GitLab CI @@ -57,10 +79,10 @@ This view will show you all tags in your repository and will easily allow you to This feature requires GitLab 8.8 and GitLab Runner 1.2. Make sure that your GitLab Runner is configured to allow building docker images. -You have to check the [Using Docker Build](../../ci/docker/using_docker_build.md). +You have to check the [Using Docker Build documentation](../../ci/docker/using_docker_build.md). -You can use [docker:dind](https://hub.docker.com/_/docker/) to build your images. -This is how the `.gitlab-ci.yml` looks like: +You can use [docker:dind](https://hub.docker.com/_/docker/) to build your images, +and this is how `.gitlab-ci.yml` should look like: ``` build_image: @@ -69,16 +91,23 @@ This is how the `.gitlab-ci.yml` looks like: - docker:dind stage: build script: - - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com - - docker build -t registry.gitlab.com/group/project:latest . - - docker push registry.gitlab.com/group/project:latest + - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com + - docker build -t registry.example.com/group/project:latest . + - docker push registry.example.com/group/project:latest ``` -You have to use special credentials `gitlab-ci-token` with password stored in `$CI_BUILD_TOKEN` in order to push to registry connected to your project. -This allows you to automated building and deployment of your images. +You have to use the credentials of the special `gitlab-ci-token` user with its +password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected +to your project. This allows you to automated building and deployment of your +Docker images. ## Limitations -In order to use container image from private project as an `image:` in your `.gitlab-ci.yml` you have to follow -[Using a private Docker Registry](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry). -This workflow will be simplified in the future. +In order to use a container image from your private project as an `image:` in +your `.gitlab-ci.yml`, you have to follow the +[Using a private Docker Registry][private-docker] +documentation. This workflow will be simplified in the future. + +[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 +[docker-docs]: https://docs.docker.com/engine/userguide/intro/ +[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry diff --git a/doc/container_registry/container_registry.png b/doc/container_registry/img/container_registry.png similarity index 100% rename from doc/container_registry/container_registry.png rename to doc/container_registry/img/container_registry.png diff --git a/doc/container_registry/project_feature.png b/doc/container_registry/img/project_feature.png similarity index 100% rename from doc/container_registry/project_feature.png rename to doc/container_registry/img/project_feature.png From 17eaecd6c7cb33d0f171ae10da190fa120a817f3 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 23 May 2016 19:43:58 +0200 Subject: [PATCH 446/449] Enable Style/InfiniteLoop Rubocop cop --- .rubocop.yml | 2 +- app/models/network/graph.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0946ef5d84..3f5a705269 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -299,7 +299,7 @@ Style/IndentHash: # Use Kernel#loop for infinite loops. Style/InfiniteLoop: - Enabled: false + Enabled: true # Use the new lambda literal syntax for single-line blocks. Style/Lambda: diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index f4e9012537..9259cb1a0f 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -253,7 +253,7 @@ module Network leaves = [] leaves.push(commit) if commit.space.zero? - while true + loop do return leaves if commit.parents(@map).count.zero? commit = commit.parents(@map).first From 6898087b97234516b1d31ac731653bebbd9a171e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 23 May 2016 14:03:04 -0400 Subject: [PATCH 447/449] Update rubocop to 0.40.0 --- .rubocop.yml | 18 ++++++++++++++++++ Gemfile | 4 ++-- Gemfile.lock | 14 +++++++------- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0946ef5d84..81ab2e8143 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -333,6 +333,12 @@ Style/MethodName: Style/ModuleFunction: Enabled: false +# Checks that the closing brace in an array literal is either on the same line +# as the last array element, or a new line. +Style/MultilineArrayBraceLayout: + Enabled: false + EnforcedStyle: symmetrical + # Avoid multi-line chains of blocks. Style/MultilineBlockChain: Enabled: false @@ -341,10 +347,22 @@ Style/MultilineBlockChain: Style/MultilineBlockLayout: Enabled: true +# Checks that the closing brace in a hash literal is either on the same line as +# the last hash element, or a new line. +Style/MultilineHashBraceLayout: + Enabled: false + EnforcedStyle: symmetrical + # Do not use then for multi-line if/unless. Style/MultilineIfThen: Enabled: false +# Checks that the closing brace in a method call is either on the same line as +# the last method argument, or a new line. +Style/MultilineMethodCallBraceLayout: + Enabled: false + EnforcedStyle: symmetrical + # Checks indentation of method calls with the dot operator that span more than # one line. Style/MultilineMethodCallIndentation: diff --git a/Gemfile b/Gemfile index 832c5a3ca8..790c457e1d 100644 --- a/Gemfile +++ b/Gemfile @@ -293,9 +293,9 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.38.0', require: false + gem 'rubocop', '~> 0.40.0', require: false gem 'scss_lint', '~> 0.47.0', require: false - gem 'coveralls', '~> 0.8.2', require: false + gem 'coveralls', '~> 0.8.2', require: false gem 'simplecov', '~> 0.11.0', require: false gem 'flog', require: false gem 'flay', require: false diff --git a/Gemfile.lock b/Gemfile.lock index e1c5b9630d..52aee37d73 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -551,7 +551,7 @@ GEM orm_adapter (0.5.0) paranoia (2.1.4) activerecord (~> 4.0) - parser (2.3.0.6) + parser (2.3.1.0) ast (~> 2.2) pg (0.18.4) poltergeist (1.9.0) @@ -686,15 +686,15 @@ GEM rspec-retry (0.4.5) rspec-core rspec-support (3.4.1) - rubocop (0.38.0) - parser (>= 2.3.0.6, < 3.0) + rubocop (0.40.0) + parser (>= 2.3.1.0, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) ruby-fogbugz (0.2.1) crack (~> 0.4) - ruby-progressbar (1.7.5) + ruby-progressbar (1.8.1) ruby-saml (1.1.2) nokogiri (>= 1.5.10) uuid (~> 2.3) @@ -841,7 +841,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.2) - unicode-display_width (1.0.2) + unicode-display_width (1.0.5) unicorn (4.9.0) kgio (~> 2.6) rack @@ -1015,7 +1015,7 @@ DEPENDENCIES rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.4.0) rspec-retry - rubocop (~> 0.38.0) + rubocop (~> 0.40.0) ruby-fogbugz (~> 0.2.1) sanitize (~> 2.0) sass-rails (~> 5.0.0) @@ -1060,4 +1060,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.12.3 + 1.12.4 From 3ba72f69af69d9fb2c46cb6c25d571f92ffd2ee1 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 23 May 2016 14:16:35 -0400 Subject: [PATCH 448/449] Enable Style/SpaceAroundKeyword cop and fix offenses --- .rubocop.yml | 3 +-- app/helpers/application_helper.rb | 3 +-- lib/ci/ansi2html.rb | 2 +- spec/controllers/projects/group_links_controller_spec.rb | 2 +- spec/factories_spec.rb | 2 +- spec/models/concerns/token_authenticatable_spec.rb | 2 +- spec/services/issues/create_service_spec.rb | 2 +- 7 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0946ef5d84..69a65b7a18 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -525,10 +525,9 @@ Style/SpaceAfterSemicolon: Style/SpaceAroundEqualsInParameterDefault: Enabled: false -# TODO: Enable SpaceAroundKeyword Cop. # Use a space around keywords if appropriate. Style/SpaceAroundKeyword: - Enabled: false + Enabled: true # Use a single space around operators. Style/SpaceAroundOperators: diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3e0074da39..e6e2546b92 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -110,8 +110,7 @@ module ApplicationHelper ] # If reference is commit id - we should add it to branch/tag selectbox - if(@ref && !options.flatten.include?(@ref) && - @ref =~ /\A[0-9a-zA-Z]{6,52}\z/) + if @ref && !options.flatten.include?(@ref) && @ref =~ /\A[0-9a-zA-Z]{6,52}\z/ options << ['Commit', [@ref]] end diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index 5fed43aaeb..c628257e3f 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -98,7 +98,7 @@ module Ci open_new_tag s = StringScanner.new(ansi) - while(!s.eos?) + until s.eos? if s.scan(/\e([@-_])(.*?)([@-~])/) handle_sequence(s) elsif s.scan(/\e(([@-_])(.*?)?)?$/) diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index 40bd83af86..1bd1fc5189 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -28,7 +28,7 @@ describe Projects::GroupLinksController do expect(group.shared_projects).to include project end - it 'redirects to project group links page'do + it 'redirects to project group links page' do expect(response).to redirect_to( namespace_project_group_links_path(project.namespace, project) ) diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 62de081661..a073a1f9d7 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -5,7 +5,7 @@ describe 'factories' do describe "#{factory.name} factory" do let(:entity) { build(factory.name) } - it 'does not raise error when created 'do + it 'does not raise error when created' do expect { entity }.to_not raise_error end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 30c0a04b84..b6adc2bf24 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -49,7 +49,7 @@ describe ApplicationSetting, 'TokenAuthenticatable' do context 'token is generated' do before { subject.send("reset_#{token_field}!") } - it 'persists a new token 'do + it 'persists a new token' do expect(subject.send(:read_attribute, token_field)).to be_a String end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index ac28b6f71f..6aa0a89f89 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -54,7 +54,7 @@ describe Issues::CreateService, services: true do label_ids: [label.id] } end - it 'does not assign label'do + it 'does not assign label' do expect(issue.labels).to_not include label end end From da5943fc9585d9728da1a9c1c43296f838faad37 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 23 May 2016 20:32:19 +0200 Subject: [PATCH 449/449] Enabled multiple Rubocop cops that can be enabled This enables multiple Rubocop cops that already conform to our codebase and do not require fixes. --- .rubocop.yml | 65 +++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 832c31be16..e7eac17db3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -755,19 +755,19 @@ Lint/BlockAlignment: # Default values in optional keyword arguments and optional ordinal arguments # should not refer back to the name of the argument. Lint/CircularArgumentReference: - Enabled: false + Enabled: true # Checks for condition placed in a confusing position relative to the keyword. Lint/ConditionPosition: - Enabled: false + Enabled: true # Check for debugger calls. Lint/Debugger: - Enabled: false + Enabled: true # Align ends corresponding to defs correctly. Lint/DefEndAlignment: - Enabled: false + Enabled: true # Check for deprecated class method calls. Lint/DeprecatedClassMethods: @@ -783,15 +783,15 @@ Lint/DuplicatedKey: # Check for immutable argument given to each_with_object. Lint/EachWithObjectArgument: - Enabled: false + Enabled: true # Check for odd code arrangement in an else block. Lint/ElseLayout: - Enabled: false + Enabled: true # Checks for empty ensure block. Lint/EmptyEnsure: - Enabled: false + Enabled: true # Checks for empty string interpolation. Lint/EmptyInterpolation: @@ -799,37 +799,36 @@ Lint/EmptyInterpolation: # Align ends correctly. Lint/EndAlignment: - Enabled: false + Enabled: true # END blocks should not be placed inside method definitions. Lint/EndInMethod: - Enabled: false + Enabled: true # Do not use return in an ensure block. Lint/EnsureReturn: - Enabled: false + Enabled: true # The use of eval represents a serious security risk. Lint/Eval: - Enabled: false + Enabled: true # Catches floating-point literals too large or small for Ruby to represent. Lint/FloatOutOfRange: - Enabled: false + Enabled: true # The number of parameters to format/sprint must match the fields. Lint/FormatParameterMismatch: - Enabled: false + Enabled: true # Don't suppress exception. Lint/HandleExceptions: Enabled: false -# TODO: Enable ImplicitStringConcatenation Cop. # Checks for adjacent string literals on the same line, which could better be # represented as a single string literal. Lint/ImplicitStringConcatenation: - Enabled: false + Enabled: true # TODO: Enable IneffectiveAccessModifier Cop. # Checks for attempts to use `private` or `protected` to set the visibility @@ -840,7 +839,7 @@ Lint/IneffectiveAccessModifier: # Checks for invalid character literals with a non-escaped whitespace # character. Lint/InvalidCharacterLiteral: - Enabled: false + Enabled: true # Checks of literals used in conditions. Lint/LiteralInCondition: @@ -848,7 +847,7 @@ Lint/LiteralInCondition: # Checks for literals used in interpolation. Lint/LiteralInInterpolation: - Enabled: false + Enabled: true # Use Kernel#loop with break rather than begin/end/until or begin/end/while # for post-loop tests. @@ -857,11 +856,11 @@ Lint/Loop: # Do not use nested method definitions. Lint/NestedMethodDefinition: - Enabled: false + Enabled: true # Do not omit the accumulator when calling `next` in a `reduce`/`inject` block. Lint/NextWithoutAccumulator: - Enabled: false + Enabled: true # Checks for method calls with a space before the opening parenthesis. Lint/ParenthesesAsGroupedExpression: @@ -870,11 +869,11 @@ Lint/ParenthesesAsGroupedExpression: # Checks for `rand(1)` calls. Such calls always return `0` and most likely # a mistake. Lint/RandOne: - Enabled: false + Enabled: true # Use parentheses in the method call to avoid confusion about precedence. Lint/RequireParentheses: - Enabled: false + Enabled: true # Avoid rescuing the Exception class. Lint/RescueException: @@ -909,7 +908,7 @@ Lint/UnusedMethodArgument: # Unreachable code. Lint/UnreachableCode: - Enabled: false + Enabled: true # Checks for useless access modifiers. Lint/UselessAccessModifier: @@ -921,19 +920,19 @@ Lint/UselessAssignment: # Checks for comparison of something with itself. Lint/UselessComparison: - Enabled: false + Enabled: true # Checks for useless `else` in `begin..end` without `rescue`. Lint/UselessElseWithoutRescue: - Enabled: false + Enabled: true # Checks for useless setter call to a local variable. Lint/UselessSetterCall: - Enabled: false + Enabled: true # Possible use of operator/literal/variable in void context. Lint/Void: - Enabled: false + Enabled: true ##################### Performance ############################ @@ -942,11 +941,10 @@ Lint/Void: Performance/Casecmp: Enabled: true -# TODO: Enable DoubleStartEndWith Cop. # Use `str.{start,end}_with?(x, ..., y, ...)` instead of # `str.{start,end}_with?(x, ...) || str.{start,end}_with?(y, ...)`. Performance/DoubleStartEndWith: - Enabled: false + Enabled: true # TODO: Enable EndWith Cop. # Use `end_with?` instead of a regex match anchored to the end of a string. @@ -980,10 +978,9 @@ Performance/RedundantMerge: MaxKeyValuePairs: 2 Enabled: false -# TODO: Enable RedundantSortBy Cop. # Use `sort` instead of `sort_by { |x| x }`. Performance/RedundantSortBy: - Enabled: false + Enabled: true # Use `start_with?` instead of a regex match anchored to the beginning of a # string. @@ -1024,11 +1021,11 @@ Rails/Delegate: # Prefer `find_by` over `where.first`. Rails/FindBy: - Enabled: false + Enabled: true # Prefer `all.find_each` over `all.find`. Rails/FindEach: - Enabled: false + Enabled: true # Prefer has_many :through to has_and_belongs_to_many. Rails/HasAndBelongsToMany: @@ -1040,7 +1037,7 @@ Rails/Output: # Checks for incorrect grammar when using methods like `3.day.ago`. Rails/PluralizationGrammar: - Enabled: false + Enabled: true # Checks for `read_attribute(:attr)` and `write_attribute(:attr, val)`. Rails/ReadWriteAttribute: @@ -1048,7 +1045,7 @@ Rails/ReadWriteAttribute: # Checks the arguments of ActiveRecord scopes. Rails/ScopeArgs: - Enabled: false + Enabled: true # Checks the correct usage of time zone aware methods. # http://danilenko.org/2012/7/6/rails_timezones