diff options
32 files changed, 384 insertions, 156 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b167fc74996..3f3873e57c1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ variables: RSPEC_RETRY_RETRY_COUNT: "3" RAILS_ENV: "test" SIMPLECOV: "true" - USE_DB: "true" + SETUP_DB: "true" USE_BUNDLE_INSTALL: "true" GIT_DEPTH: "20" PHANTOMJS_VERSION: "2.1.1" @@ -23,7 +23,7 @@ before_script: - bundle --version - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' - retry gem install knapsack - - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' + - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' stages: - prepare @@ -35,7 +35,7 @@ stages: .knapsack-state: &knapsack-state services: [] variables: - USE_DB: "false" + SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" cache: key: "knapsack" @@ -196,7 +196,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 .ruby-static-analysis: &ruby-static-analysis variables: SIMPLECOV: "false" - USE_DB: "false" + SETUP_DB: "false" USE_BUNDLE_INSTALL: "true" .exec: &exec @@ -224,6 +224,23 @@ rake db:migrate:reset: script: - rake db:migrate:reset +rake db:seed_fu: + stage: test + <<: *use-db + variables: + SIZE: "1" + SETUP_DB: "false" + RAILS_ENV: "development" + script: + - git clone https://gitlab.com/gitlab-org/gitlab-test.git + /home/git/repositories/gitlab-org/gitlab-test.git + - bundle exec rake db:setup db:seed_fu + artifacts: + when: on_failure + expire_in: 1d + paths: + - log/development.log + teaspoon: stage: test <<: *use-db @@ -272,7 +289,7 @@ coverage: stage: post-test services: [] variables: - USE_DB: "false" + SETUP_DB: "false" USE_BUNDLE_INSTALL: "true" script: - bundle exec scripts/merge-simplecov @@ -288,7 +305,7 @@ coverage: notify:slack: stage: post-test variables: - USE_DB: "false" + SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" script: - ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>" diff --git a/CHANGELOG b/CHANGELOG index 5fbc8830d7b..592a3fec134 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.12.0 (unreleased) +v 8.13.0 (unreleased) + - Speed-up group milestones show page + +v 8.12.2 (unreleased) + +v 8.12.1 + - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST + - Fix issue with search filter labels not displaying + +v 8.12.0 - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251 - Only check :can_resolve permission if the note is resolvable - Bump fog-aws to v0.11.0 to support ap-south-1 region @@ -81,6 +90,7 @@ v 8.12.0 (unreleased) - Fix markdown anchor icon interaction (ClemMakesApps) - Test migration paths from 8.5 until current release !4874 - Replace animateEmoji timeout with eventListener (ClemMakesApps) + - Show badges in Milestone tabs. !5946 (Dan Rowden) - Optimistic locking for Issues and Merge Requests (title and description overriding prevention) - Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto) - Add `wiki_page_events` to project hook APIs (Ben Boeckel) @@ -322,10 +322,6 @@ group :test do gem 'timecop', '~> 0.8.0' end -group :production do - gem 'gitlab_meta', '7.0' -end - gem 'newrelic_rpm', '~> 3.16' gem 'octokit', '~> 4.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index a5a2e5785ac..1db8c9dd8c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -284,7 +284,6 @@ GEM charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) rugged (~> 0.24.0) - gitlab_meta (7.0) gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) omniauth (~> 1.0) @@ -864,7 +863,6 @@ DEPENDENCIES github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) gitlab_git (~> 10.6.6) - gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) @@ -991,4 +989,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.13.0 + 1.13.1 diff --git a/README.md b/README.md index 9661a554b9f..8236f986b56 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## Canonical source -The cannonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). +The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). ## Open source software to collaborate on code diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle-analytics.js.es6 index afaed7c4f60..cd9886ba58d 100644 --- a/app/assets/javascripts/cycle-analytics.js.es6 +++ b/app/assets/javascripts/cycle-analytics.js.es6 @@ -1,17 +1,22 @@ ((global) => { const COOKIE_NAME = 'cycle_analytics_help_dismissed'; + const store = gl.cycleAnalyticsStore = { + isLoading: true, + hasError: false, + isHelpDismissed: $.cookie(COOKIE_NAME), + analytics: {} + }; gl.CycleAnalytics = class CycleAnalytics { constructor() { const that = this; - this.isHelpDismissed = $.cookie(COOKIE_NAME); this.vue = new Vue({ el: '#cycle-analytics', name: 'CycleAnalytics', created: this.fetchData(), - data: this.decorateData({ isLoading: true }), + data: store, methods: { dismissLanding() { that.dismissLanding(); @@ -21,6 +26,7 @@ } fetchData(options) { + store.isLoading = true; options = options || { startDate: 30 }; $.ajax({ @@ -30,22 +36,20 @@ contentType: 'application/json', data: { start_date: options.startDate } }).done((data) => { - this.vue.$data = this.decorateData(data); + this.decorateData(data); this.initDropdown(); }) .error((data) => { this.handleError(data); }) .always(() => { - this.vue.isLoading = false; + store.isLoading = false; }) } decorateData(data) { data.summary = data.summary || []; data.stats = data.stats || []; - data.isHelpDismissed = this.isHelpDismissed; - data.isLoading = data.isLoading || false; data.summary.forEach((item) => { item.value = item.value || '-'; @@ -53,23 +57,21 @@ data.stats.forEach((item) => { item.value = item.value || '- - -'; - }) + }); - return data; + store.analytics = data; } handleError(data) { - this.vue.$data = { - hasError: true, - isHelpDismissed: this.isHelpDismissed - }; - + store.hasError = true; new Flash('There was an error while fetching cycle analytics data.', 'alert'); } dismissLanding() { - this.vue.isHelpDismissed = true; - $.cookie(COOKIE_NAME, true); + store.isHelpDismissed = true; + $.cookie(COOKIE_NAME, true, { + path: gon.relative_url_root || '/' + }); } initDropdown() { @@ -82,7 +84,6 @@ const value = $target.data('value'); $label.text($target.text().trim()); - this.vue.isLoading = true; this.fetchData({ startDate: value }); }) } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 0c21d0240b3..7ae309ba103 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -3,7 +3,6 @@ margin: 0; margin-bottom: $gl-padding; font-size: 14px; - z-index: 100; .flash-notice { @extend .alert; @@ -41,4 +40,3 @@ } } } - diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 2f5e3ec8e44..14ec310de2d 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -270,6 +270,12 @@ $calendar-border-color: rgba(#000, .1); $calendar-unselectable-bg: $gray-light; /* + * Cycle Analytics + */ +$cycle-analytics-box-padding: 30px; +$cycle-analytics-box-text-color: #8c8c8c; + +/* * Personal Access Tokens */ $personal-access-tokens-disabled-label-color: #bbb; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 21e19c97632..778471a34d7 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -1,6 +1,6 @@ #cycle-analytics { margin: 24px auto 0; - width: 800px; + max-width: 800px; position: relative; .panel { @@ -9,10 +9,18 @@ padding: 24px 0; border-bottom: none; position: relative; + + @media (max-width: $screen-sm-min) { + padding: 6px 0 24px; + } } .column { text-align: center; + + @media (max-width: $screen-sm-min) { + padding: 15px 0; + } .header { font-size: 30px; @@ -28,11 +36,14 @@ &:last-child { text-align: right; + + @media (max-width: $screen-sm-min) { + text-align: center; + } } } .dropdown { - position: relative; top: 13px; } } @@ -40,7 +51,7 @@ .bordered-box { border: 1px solid $border-color; @include border-radius($border-radius-default); - position: relative; + } .content-list { @@ -60,9 +71,15 @@ line-height: 19px; font-size: 15px; font-weight: 600; + color: $gl-title-color; } - &:text { - color: #8c8c8c; + + &.text { + color: $layout-link-gray; + + &.value-col { + color: $gl-title-color; + } } } } @@ -71,7 +88,9 @@ text-align: right; span { - line-height: 42px; + position: relative; + vertical-align: middle; + top: 3px; } } } @@ -82,21 +101,25 @@ .dismiss-icon { position: absolute; - right: $gl-padding; + right: $cycle-analytics-box-padding; cursor: pointer; color: #b2b2b2; } - svg { - margin: 0 20px; - float: left; - width: 136px; - height: 136px; + .svg-container { + text-align: center; + + svg { + width: 136px; + height: 136px; + } } - + .inner-content { - width: 480px; - float: left; + @media (max-width: $screen-sm-min) { + padding: 0 28px; + text-align: center; + } h4 { color: $gl-text-color; @@ -104,7 +127,7 @@ } p { - color: #8c8c8c; + color: $cycle-analytics-box-text-color; margin-bottom: $gl-padding; } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index b94f524b513..6b865730487 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -2,13 +2,17 @@ max-width: 90%; } -li.milestone { - h4 { - font-weight: bold; - } +.milestones { + .milestone { + padding: 10px 16px; + + h4 { + font-weight: bold; + } - .progress { - height: 6px; + .progress { + height: 6px; + } } } @@ -64,3 +68,14 @@ li.milestone { border-bottom: 1px solid $border-color; padding: 20px 0; } + +@media (max-width: $screen-sm-min) { + .milestone-actions { + @include clearfix(); + padding-top: $gl-vert-padding; + + .btn:first-child { + margin-left: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 1b4d12d3053..b035bfc9f3c 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -177,6 +177,10 @@ border-bottom: 2px solid $border-color; } } + + a { + display: block; + } } } diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index b3e6e468ecd..a11c313a6b8 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -35,6 +35,30 @@ module MilestonesHelper milestone.issues.with_label(label.title).send(state).size end + # Returns count of milestones for different states + # Uses explicit hash keys as the 'opened' state URL params differs from the db value + # and we need to add the total + def milestone_counts(milestones) + counts = milestones.reorder(nil).group(:state).count + + { + opened: counts['active'] || 0, + closed: counts['closed'] || 0, + all: counts.values.sum || 0 + } + end + + # Show 'active' class if provided GET param matches check + # `or_blank` allows the function to return 'active' when given an empty param + # Could be refactored to be simpler but that may make it harder to read + def milestone_class_for_state(param, check, match_blank_param = false) + if match_blank_param + 'active' if param.blank? || param == check + else + 'active' if param == check + end + end + def milestone_progress_bar(milestone) options = { class: 'progress-bar progress-bar-success', diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cb87b43f6be..522e2264bb8 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -91,7 +91,7 @@ module Ci sha: build.sha, ref: build.ref, tag: build.tag, - options: build.options[:environment], + options: build.options.to_h[:environment], variables: build.variables) service.execute(build) end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index da7c265a371..bda2b5c5d5d 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -8,7 +8,8 @@ class GlobalMilestone milestones = milestones.group_by(&:title) milestones.map do |title, milestones| - new(title, milestones) + milestones_relation = Milestone.where(id: milestones.map(&:id)) + new(title, milestones_relation) end end @@ -31,7 +32,7 @@ class GlobalMilestone end def projects - @projects ||= Project.for_milestones(milestones.map(&:id)) + @projects ||= Project.for_milestones(milestones.select(:id)) end def state @@ -53,19 +54,19 @@ class GlobalMilestone end def issues - @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project) + @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels) end def merge_requests - @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project) + @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels) end def participants - @participants ||= milestones.map(&:participants).flatten.compact.uniq + @participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq end def labels - @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten) + @labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten) .sort_by!(&:title) end diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 5dcb2a17873..7f346df8797 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -2,18 +2,20 @@ - page_title "Cycle Analytics" = render "projects/pipelines/head" -#cycle-analytics{"v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}} +#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}} .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()") - = custom_icon('icon_cycle_analytics_splash') - .inner-content - %h4 - Introducing Cycle Analytics - %p - Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. + .row + .col-sm-3.col-xs-12.svg-container + = custom_icon('icon_cycle_analytics_splash') + .col-sm-8.col-xs-12.inner-content + %h4 + Introducing Cycle Analytics + %p + Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. - = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' + = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' = icon("spinner spin", "v-show" => "isLoading") @@ -25,11 +27,11 @@ .content-block .container-fluid .row - .col-xs-3.column{"v-for" => "item in summary"} + .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"} %h3.header {{item.value}} %p.text {{item.title}} - .col-xs-3.column + .col-sm-3.col-xs-12.column .dropdown.inline.js-ca-dropdown %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"} %span.dropdown-label Last 30 days @@ -44,14 +46,14 @@ .bordered-box %ul.content-list - %li{"v-for" => "item in stats"} + %li{"v-for" => "item in analytics.stats"} .container-fluid .row - .col-xs-10.title-col + .col-xs-8.title-col %p.title {{item.title}} %p.text {{item.description}} - .col-xs-2.value-col + .col-xs-4.value-col %span {{item.value}} diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index cf16c203f9c..73d288e2236 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,10 +1,19 @@ +- if @project + - counts = milestone_counts(@project.milestones) + %ul.nav-links - %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')} + %li{class: milestone_class_for_state(params[:state], 'opened', true)} = link_to milestones_filter_path(state: 'opened') do Open - %li{class: ("active" if params[:state] == 'closed')} + - if @project + %span.badge #{counts[:opened]} + %li{class: milestone_class_for_state(params[:state], 'closed')} = link_to milestones_filter_path(state: 'closed') do Closed - %li{class: ("active" if params[:state] == 'all')} + - if @project + %span.badge #{counts[:closed]} + %li{class: milestone_class_for_state(params[:state], 'all')} = link_to milestones_filter_path(state: 'all') do All + - if @project + %span.badge #{counts[:all]} diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index acc3ccf4dcf..3dccfb147bf 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -33,7 +33,7 @@ - if @project .row .col-sm-6= render('shared/milestone_expired', milestone: milestone) - .col-sm-6 + .col-sm-6.milestone-actions - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do Edit diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index e3316ecdb6c..a984eda5ab5 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -3,11 +3,11 @@ require 'sidekiq/testing' Sidekiq::Testing.inline! do Gitlab::Seeder.quiet do project_urls = [ - 'https://github.com/documentcloud/underscore.git', + 'https://gitlab.com/gitlab-org/gitlab-test.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', 'https://gitlab.com/gitlab-org/gitlab-ci.git', 'https://gitlab.com/gitlab-org/gitlab-shell.git', - 'https://gitlab.com/gitlab-org/gitlab-test.git', + 'https://github.com/documentcloud/underscore.git', 'https://github.com/twitter/flight.git', 'https://github.com/twitter/typeahead.js.git', 'https://github.com/h5bp/html5-boilerplate.git', @@ -38,12 +38,7 @@ Sidekiq::Testing.inline! do ] # You can specify how many projects you need during seed execution - size = if ENV['SIZE'].present? - ENV['SIZE'].to_i - else - 8 - end - + size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8 project_urls.first(size).each_with_index do |url, i| group_path, project_path = url.split('/')[-2..-1] diff --git a/db/migrate/20160920160832_add_index_to_labels_title.rb b/db/migrate/20160920160832_add_index_to_labels_title.rb new file mode 100644 index 00000000000..b5de552b98c --- /dev/null +++ b/db/migrate/20160920160832_add_index_to_labels_title.rb @@ -0,0 +1,11 @@ +class AddIndexToLabelsTitle < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index :labels, :title + end +end diff --git a/db/schema.rb b/db/schema.rb index 59b3e237707..425fc33b7b3 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: 20160915042921) do +ActiveRecord::Schema.define(version: 20160920160832) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -521,6 +521,7 @@ ActiveRecord::Schema.define(version: 20160915042921) do add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree + add_index "labels", ["title"], name: "index_labels_on_title", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false diff --git a/doc/api/README.md b/doc/api/README.md index 6e3295e0e0c..8e4f7f12b4b 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -10,6 +10,7 @@ following locations: - [Award Emoji](award_emoji.md) - [Branches](branches.md) +- [Broadcast Messages](broadcast_messages.md) - [Builds](builds.md) - [Build Triggers](build_triggers.md) - [Build Variables](build_variables.md) diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 71670e6247c..40f0165deef 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -13,7 +13,7 @@ Apart from those, here is an collection of tutorials and guides on setting up yo - [Test a Scala application](test-scala-application.md) - [Using `dpl` as deployment tool](deployment/README.md) - [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) -- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples) +- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples) - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) [gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 6a971c3ae87..22d67bd9964 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -44,7 +44,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`. | **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run | | **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | -| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returnes the address of the registry tied to the specific project | +| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project | | **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | | **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab | | **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags | diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 6e13282d5f4..2470362e019 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -7,7 +7,7 @@ module Banzai UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze def whitelist - whitelist = super.dup + whitelist = super customize_whitelist(whitelist) @@ -42,58 +42,58 @@ module Banzai # Allow any protocol in `a` elements... whitelist[:protocols].delete('a') - whitelist[:transformers] = whitelist[:transformers].dup - # ...but then remove links with unsafe protocols - whitelist[:transformers].push(remove_unsafe_links) + whitelist[:transformers].push(self.class.remove_unsafe_links) # Remove `rel` attribute from `a` elements - whitelist[:transformers].push(remove_rel) + whitelist[:transformers].push(self.class.remove_rel) # Remove `class` attribute from non-highlight spans - whitelist[:transformers].push(clean_spans) + whitelist[:transformers].push(self.class.clean_spans) whitelist end - def remove_unsafe_links - lambda do |env| - node = env[:node] + class << self + def remove_unsafe_links + lambda do |env| + node = env[:node] - return unless node.name == 'a' - return unless node.has_attribute?('href') + return unless node.name == 'a' + return unless node.has_attribute?('href') - begin - uri = Addressable::URI.parse(node['href']) - uri.scheme = uri.scheme.strip.downcase if uri.scheme + begin + uri = Addressable::URI.parse(node['href']) + uri.scheme = uri.scheme.strip.downcase if uri.scheme - node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme) - rescue Addressable::URI::InvalidURIError - node.remove_attribute('href') + node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme) + rescue Addressable::URI::InvalidURIError + node.remove_attribute('href') + end end end - end - def remove_rel - lambda do |env| - if env[:node_name] == 'a' - env[:node].remove_attribute('rel') + def remove_rel + lambda do |env| + if env[:node_name] == 'a' + env[:node].remove_attribute('rel') + end end end - end - def clean_spans - lambda do |env| - node = env[:node] + def clean_spans + lambda do |env| + node = env[:node] - return unless node.name == 'span' - return unless node.has_attribute?('class') + return unless node.name == 'span' + return unless node.has_attribute?('class') - unless has_ancestor?(node, 'pre') - node.remove_attribute('class') - end + unless node.ancestors.any? { |n| n.name.casecmp('pre').zero? } + node.remove_attribute('class') + end - { node_whitelist: [node] } + { node_whitelist: [node] } + end end end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 12fbb78c53e..ef9160d6437 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -59,10 +59,8 @@ module Gitlab # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised active_db_connection = ActiveRecord::Base.connection.active? rescue false - ENV['USE_DB'] != 'false' && active_db_connection && - ActiveRecord::Base.connection.table_exists?('application_settings') - + ActiveRecord::Base.connection.table_exists?('application_settings') rescue ActiveRecord::NoDatabaseError false end diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 9376b54f43b..69c4ef721d5 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -9,19 +9,22 @@ module Gitlab SIDEKIQ_NAMESPACE = 'resque:gitlab' MAILROOM_NAMESPACE = 'mail_room:gitlab' DEFAULT_REDIS_URL = 'redis://localhost:6379' + CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__) # To be thread-safe we must be careful when writing the class instance - # variables @url and @pool. Because @pool depends on @url we need two + # variables @_raw_config and @pool. Because @pool depends on @_raw_config we need two # mutexes to prevent deadlock. - PARAMS_MUTEX = Mutex.new + RAW_CONFIG_MUTEX = Mutex.new POOL_MUTEX = Mutex.new - private_constant :PARAMS_MUTEX, :POOL_MUTEX + private_constant :RAW_CONFIG_MUTEX, :POOL_MUTEX class << self + # Do NOT cache in an instance variable. Result may be mutated by caller. def params - @params || PARAMS_MUTEX.synchronize { @params = new.params } + new.params end + # Do NOT cache in an instance variable. Result may be mutated by caller. # @deprecated Use .params instead to get sentinel support def url new.url @@ -36,8 +39,17 @@ module Gitlab @pool.with { |redis| yield redis } end - def reset_params! - @params = nil + def _raw_config + return @_raw_config if defined?(@_raw_config) + + RAW_CONFIG_MUTEX.synchronize do + begin + @_raw_config = File.read(CONFIG_FILE).freeze + rescue Errno::ENOENT + @_raw_config = false + end + end + @_raw_config end end @@ -83,12 +95,7 @@ module Gitlab end def fetch_config - file = config_file - File.exist?(file) ? YAML.load_file(file)[@rails_env] : false - end - - def config_file - File.expand_path('../../../config/resque.yml', __FILE__) + self.class._raw_config ? YAML.load(self.class._raw_config)[@rails_env] : false end end end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 60aae541d46..5d33f98e89e 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -60,7 +60,7 @@ module Gitlab def send_git_diff(repository, diff_refs) params = { 'RepoPath' => repository.path_to_repo, - 'ShaFrom' => diff_refs.start_sha, + 'ShaFrom' => diff_refs.base_sha, 'ShaTo' => diff_refs.head_sha } @@ -73,7 +73,7 @@ module Gitlab def send_git_patch(repository, diff_refs) params = { 'RepoPath' => repository.path_to_repo, - 'ShaFrom' => diff_refs.start_sha, + 'ShaFrom' => diff_refs.base_sha, 'ShaTo' => diff_refs.head_sha } @@ -107,15 +107,15 @@ module Gitlab bytes end end - + def write_secret bytes = SecureRandom.random_bytes(SECRET_LENGTH) - File.open(secret_path, 'w:BINARY', 0600) do |f| + File.open(secret_path, 'w:BINARY', 0600) do |f| f.chmod(0600) f.write(Base64.strict_encode64(bytes)) end end - + def verify_api_request!(request_headers) JWT.decode( request_headers[INTERNAL_API_REQUEST_HEADER], @@ -128,7 +128,7 @@ module Gitlab def secret_path Rails.root.join('.gitlab_workhorse_secret') end - + protected def encode(hash) diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb index e9e85962fe4..84da71ed6dc 100644 --- a/spec/factories/milestones.rb +++ b/spec/factories/milestones.rb @@ -3,10 +3,15 @@ FactoryGirl.define do title project + trait :active do + state "active" + end + trait :closed do - state :closed + state "closed" end + factory :active_milestone, traits: [:active] factory :closed_milestone, traits: [:closed] end end diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb new file mode 100644 index 00000000000..28c2268f8d0 --- /dev/null +++ b/spec/helpers/milestones_helper_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe MilestonesHelper do + describe '#milestone_counts' do + let(:project) { FactoryGirl.create(:project) } + let(:counts) { helper.milestone_counts(project.milestones) } + + context 'when there are milestones' do + let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) } + let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) } + let!(:milestone_3) { FactoryGirl.create(:closed_milestone, project: project) } + + it 'returns the correct counts' do + expect(counts).to eq(opened: 2, closed: 1, all: 3) + end + end + + context 'when there are only milestones of one type' do + let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) } + let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) } + + it 'returns the correct counts' do + expect(counts).to eq(opened: 2, closed: 0, all: 2) + end + end + + context 'when there are no milestones' do + it 'returns the correct counts' do + expect(counts).to eq(opened: 0, closed: 0, all: 0) + end + end + end +end diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index e54f5ffb312..cb54c020b31 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -3,19 +3,27 @@ require 'spec_helper' describe Gitlab::Redis do let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s } - before(:each) { described_class.reset_params! } - after(:each) { described_class.reset_params! } + before(:each) { clear_raw_config } + after(:each) { clear_raw_config } describe '.params' do subject { described_class.params } + it 'withstands mutation' do + params1 = described_class.params + params2 = described_class.params + params1[:foo] = :bar + + expect(params2).not_to have_key(:foo) + end + context 'when url contains unix socket reference' do let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s } let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s } context 'with old format' do it 'returns path key instead' do - expect_any_instance_of(described_class).to receive(:config_file) { config_old } + stub_const("#{described_class}::CONFIG_FILE", config_old) is_expected.to include(path: '/path/to/old/redis.sock') is_expected.not_to have_key(:url) @@ -24,7 +32,7 @@ describe Gitlab::Redis do context 'with new format' do it 'returns path key instead' do - expect_any_instance_of(described_class).to receive(:config_file) { config_new } + stub_const("#{described_class}::CONFIG_FILE", config_new) is_expected.to include(path: '/path/to/redis.sock') is_expected.not_to have_key(:url) @@ -38,7 +46,7 @@ describe Gitlab::Redis do context 'with old format' do it 'returns hash with host, port, db, and password' do - expect_any_instance_of(described_class).to receive(:config_file) { config_old } + stub_const("#{described_class}::CONFIG_FILE", config_old) is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99) is_expected.not_to have_key(:url) @@ -47,7 +55,7 @@ describe Gitlab::Redis do context 'with new format' do it 'returns hash with host, port, db, and password' do - expect_any_instance_of(described_class).to receive(:config_file) { config_new } + stub_const("#{described_class}::CONFIG_FILE", config_new) is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99) is_expected.not_to have_key(:url) @@ -56,6 +64,30 @@ describe Gitlab::Redis do end end + describe '.url' do + it 'withstands mutation' do + url1 = described_class.url + url2 = described_class.url + url1 << 'foobar' + + expect(url2).not_to end_with('foobar') + end + end + + describe '._raw_config' do + subject { described_class._raw_config } + + it 'should be frozen' do + expect(subject).to be_frozen + end + + it 'returns false when the file does not exist' do + stub_const("#{described_class}::CONFIG_FILE", '/var/empty/doesnotexist') + + expect(subject).to eq(false) + end + end + describe '#raw_config_hash' do it 'returns default redis url when no config file is present' do expect(subject).to receive(:fetch_config) { false } @@ -71,9 +103,15 @@ describe Gitlab::Redis do describe '#fetch_config' do it 'returns false when no config file is present' do - allow(File).to receive(:exist?).with(redis_config) { false } + allow(described_class).to receive(:_raw_config) { false } expect(subject.send(:fetch_config)).to be_falsey end end + + def clear_raw_config + described_class.remove_instance_variable(:@_raw_config) + rescue NameError + # raised if @_raw_config was not set; ignore + end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 6c7fa7e7c15..b5b685da904 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -1,8 +1,16 @@ require 'spec_helper' describe Gitlab::Workhorse, lib: true do - let(:project) { create(:project) } - let(:subject) { Gitlab::Workhorse } + let(:project) { create(:project) } + let(:repository) { project.repository } + + def decode_workhorse_header(array) + key, value = array + command, encoded_params = value.split(":") + params = JSON.parse(Base64.urlsafe_decode64(encoded_params)) + + [key, command, params] + end describe ".send_git_archive" do context "when the repository doesn't have an archive file path" do @@ -11,11 +19,37 @@ describe Gitlab::Workhorse, lib: true do end it "raises an error" do - expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError) + expect { described_class.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError) end end end + describe '.send_git_patch' do + let(:diff_refs) { double(base_sha: "base", head_sha: "head") } + subject { described_class.send_git_patch(repository, diff_refs) } + + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-format-patch") + expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + end + end + + describe '.send_git_diff' do + let(:diff_refs) { double(base_sha: "base", head_sha: "head") } + subject { described_class.send_git_patch(repository, diff_refs) } + + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-format-patch") + expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + end + end + describe ".secret" do subject { described_class.secret } diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index 92e0f7f27ce..dd033480527 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -50,8 +50,9 @@ describe GlobalMilestone, models: true do milestone1_project2, milestone1_project3, ] + milestones_relation = Milestone.where(id: milestones.map(&:id)) - @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones) + @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones_relation) end it 'has exactly one group milestone' do @@ -67,7 +68,7 @@ describe GlobalMilestone, models: true do let(:milestone) { create(:milestone, title: "git / test", project: project1) } it 'strips out slashes and spaces' do - global_milestone = GlobalMilestone.new(milestone.title, [milestone]) + global_milestone = GlobalMilestone.new(milestone.title, Milestone.where(id: milestone.id)) expect(global_milestone.safe_title).to eq('git-test') end |