Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--CHANGELOG25
-rw-r--r--CONTRIBUTING.md22
-rw-r--r--Gemfile19
-rw-r--r--Gemfile.lock70
-rw-r--r--LICENSE2
-rw-r--r--app/assets/images/brand_logo.pngbin27059 -> 0 bytes
-rw-r--r--app/assets/images/gitlab_logo.pngbin0 -> 5189 bytes
-rw-r--r--app/assets/javascripts/application.js.coffee6
-rw-r--r--app/assets/javascripts/awards_handler.coffee28
-rw-r--r--app/assets/javascripts/calendar.js.coffee5
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee2
-rw-r--r--app/assets/javascripts/issue.js.coffee34
-rw-r--r--app/assets/javascripts/issues.js.coffee11
-rw-r--r--app/assets/javascripts/logo.js.coffee43
-rw-r--r--app/assets/javascripts/merge_requests.js.coffee4
-rw-r--r--app/assets/javascripts/shortcuts.js.coffee10
-rw-r--r--app/assets/javascripts/users_select.js.coffee2
-rw-r--r--app/assets/stylesheets/framework/calendar.scss42
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss7
-rw-r--r--app/controllers/abuse_reports_controller.rb11
-rw-r--r--app/controllers/admin/application_settings_controller.rb11
-rw-r--r--app/controllers/admin/builds_controller.rb6
-rw-r--r--app/controllers/explore/groups_controller.rb2
-rw-r--r--app/controllers/projects/builds_controller.rb6
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb23
-rw-r--r--app/controllers/sessions_controller.rb18
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/groups_finder.rb44
-rw-r--r--app/finders/joined_groups_finder.rb49
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/issues_helper.rb18
-rw-r--r--app/helpers/page_layout_helper.rb74
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/mailers/abuse_report_mailer.rb10
-rw-r--r--app/mailers/emails/notes.rb2
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/abuse_report.rb6
-rw-r--r--app/models/application_setting.rb28
-rw-r--r--app/models/ci/build.rb58
-rw-r--r--app/models/concerns/issuable.rb14
-rw-r--r--app/models/concerns/mentionable.rb2
-rw-r--r--app/models/group.rb8
-rw-r--r--app/models/note.rb15
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/user.rb5
-rw-r--r--app/services/notification_service.rb1
-rw-r--r--app/services/projects/transfer_service.rb3
-rw-r--r--app/services/system_hooks_service.rb11
-rw-r--r--app/uploaders/artifact_uploader.rb8
-rw-r--r--app/views/abuse_reports/new.html.haml4
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml18
-rw-r--r--app/views/admin/abuse_reports/index.html.haml3
-rw-r--r--app/views/admin/application_settings/_form.html.haml79
-rw-r--r--app/views/admin/builds/index.html.haml14
-rw-r--r--app/views/admin/dashboard/index.html.haml22
-rw-r--r--app/views/admin/groups/index.html.haml2
-rw-r--r--app/views/admin/users/index.html.haml14
-rw-r--r--app/views/devise/shared/_signup_box.html.haml12
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/groups/edit.html.haml9
-rw-r--r--app/views/groups/show.html.haml4
-rw-r--r--app/views/help/_shortcuts.html.haml8
-rw-r--r--app/views/layouts/_head.html.haml25
-rw-r--r--app/views/layouts/nav/_admin.html.haml6
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/layouts/nav/_group.html.haml4
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml6
-rw-r--r--app/views/projects/_home_panel.html.haml21
-rw-r--r--app/views/projects/builds/index.html.haml12
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml5
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml18
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml5
-rw-r--r--app/views/projects/merge_requests/index.html.haml5
-rw-r--r--app/views/projects/milestones/show.html.haml4
-rw-r--r--app/views/projects/show.html.haml13
-rw-r--r--app/views/shared/_logo.svg14
-rw-r--r--app/views/shared/issuable/_filter.html.haml8
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/users/show.html.haml5
-rw-r--r--app/views/votes/_votes_block.html.haml2
-rw-r--r--config/database.yml.env9
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--config/initializers/1_settings.rb9
-rw-r--r--config/initializers/metrics.rb63
-rw-r--r--config/schedule.rb8
-rw-r--r--db/migrate/20151228111122_remove_public_from_namespace.rb6
-rw-r--r--db/migrate/20151228150906_influxdb_settings.rb18
-rw-r--r--db/migrate/20151228175719_add_recaptcha_to_application_settings.rb9
-rw-r--r--db/migrate/20151229102248_influxdb_udp_port_setting.rb5
-rw-r--r--db/migrate/20151229112614_influxdb_remote_database_setting.rb5
-rw-r--r--db/schema.rb517
-rw-r--r--doc/README.md3
-rw-r--r--doc/administration/environment_variables.md53
-rw-r--r--doc/api/merge_requests.md17
-rw-r--r--doc/api/notes.md3
-rw-r--r--doc/ci/triggers/README.md5
-rw-r--r--doc/integration/README.md1
-rw-r--r--doc/integration/recaptcha.md23
-rw-r--r--doc/permissions/permissions.md6
-rw-r--r--doc/raketasks/backup_restore.md43
-rw-r--r--doc/security/README.md1
-rw-r--r--doc/security/crime_vulnerability.md63
-rw-r--r--doc/ssh/README.md11
-rw-r--r--doc/system_hooks/system_hooks.md50
-rw-r--r--doc/update/8.2-to-8.3.md2
-rw-r--r--features/explore/groups.feature15
-rw-r--r--features/project/fork.feature11
-rw-r--r--features/steps/project/fork.rb19
-rw-r--r--features/steps/project/issues/award_emoji.rb10
-rw-r--r--features/steps/project/issues/issues.rb9
-rw-r--r--lib/api/entities.rb1
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb12
-rw-r--r--lib/banzai/filter/redactor_filter.rb8
-rw-r--r--lib/banzai/filter/reference_filter.rb10
-rw-r--r--lib/banzai/filter/reference_gatherer_filter.rb10
-rw-r--r--lib/banzai/filter/relative_link_filter.rb2
-rw-r--r--lib/banzai/filter/user_reference_filter.rb14
-rw-r--r--lib/banzai/querying.rb18
-rw-r--r--lib/banzai/renderer.rb21
-rw-r--r--lib/gitlab/contributions_calendar.rb4
-rw-r--r--lib/gitlab/current_settings.rb4
-rw-r--r--lib/gitlab/metrics.rb102
-rw-r--r--lib/gitlab/metrics/delta.rb32
-rw-r--r--lib/gitlab/metrics/instrumentation.rb148
-rw-r--r--lib/gitlab/metrics/metric.rb28
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb49
-rw-r--r--lib/gitlab/metrics/sampler.rb107
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb23
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb54
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb22
-rw-r--r--lib/gitlab/metrics/system.rb35
-rw-r--r--lib/gitlab/metrics/transaction.rb79
-rw-r--r--lib/gitlab/recaptcha.rb14
-rw-r--r--lib/gitlab/reference_extractor.rb19
-rw-r--r--spec/controllers/abuse_reports_controller_spec.rb80
-rw-r--r--spec/features/admin/admin_builds_spec.rb119
-rw-r--r--spec/features/builds_spec.rb23
-rw-r--r--spec/features/projects_spec.rb6
-rw-r--r--spec/finders/groups_finder_spec.rb48
-rw-r--r--spec/finders/joined_groups_finder_spec.rb49
-rw-r--r--spec/helpers/issues_helper_spec.rb7
-rw-r--r--spec/helpers/page_layout_helper_spec.rb129
-rw-r--r--spec/helpers/search_helper_spec.rb2
-rw-r--r--spec/javascripts/fixtures/issues_show.html.haml12
-rw-r--r--spec/javascripts/issue_spec.js.coffee86
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb19
-rw-r--r--spec/lib/banzai/querying_spec.rb13
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/delta_spec.rb16
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb240
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb51
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb63
-rw-r--r--spec/lib/gitlab/metrics/sampler_spec.rb119
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb26
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_view_spec.rb40
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb35
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb29
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb89
-rw-r--r--spec/lib/gitlab/metrics_spec.rb72
-rw-r--r--spec/mailers/abuse_report_mailer_spec.rb38
-rw-r--r--spec/models/abuse_report_spec.rb17
-rw-r--r--spec/models/concerns/issuable_spec.rb32
-rw-r--r--spec/models/concerns/mentionable_spec.rb4
-rw-r--r--spec/models/group_spec.rb27
-rw-r--r--spec/models/note_spec.rb11
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb9
-rw-r--r--spec/services/notification_service_spec.rb18
-rw-r--r--spec/services/system_hooks_service_spec.rb47
-rw-r--r--vendor/assets/javascripts/jquery.blockUI.js590
-rw-r--r--vendor/assets/javascripts/jquery.history.js1
183 files changed, 3543 insertions, 1616 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a8da3de83f8..c23a7a3bf0e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,6 +12,7 @@ before_script:
spec:feature:
script:
+ - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
tags:
- ruby
diff --git a/CHANGELOG b/CHANGELOG
index a0bd75706dd..0dcd362f024 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,18 +1,33 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.4.0 (unreleased)
+ - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu)
+ - Don't notify users twice if they are both project watchers and subscribers (Stan Hu)
- Implement new UI for group page
- Implement search inside emoji picker
- Add API support for looking up a user by username (Stan Hu)
- Add project permissions to all project API endpoints (Stan Hu)
- Link to milestone in "Milestone changed" system note
- Expose Git's version in the admin area
+ - Only allow group/project members to mention `@all`
+ - Expose Git's version in the admin area (Trey Davis)
- Add "Frequently used" category to emoji picker
- Add CAS support (tduehr)
- - Add link to merge request on build detail page.
-
-v 8.3.2 (unreleased)
- - Enable "Add key" button when user fills in a proper key
+ - Add link to merge request on build detail page
+ - Revert back upvote and downvote button to the issue and MR pages
+ - Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg)
+ - Add system hook messages for project rename and transfer (Steve Norman)
+ - Fix version check image in Safari
+ - Show 'All' tab by default in the builds page
+ - Fix API project lookups when querying with a namespace with dots (Stan Hu)
+
+v 8.3.3 (unreleased)
+ - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu)
+ - Enable "Add key" button when user fills in a proper key (Stan Hu)
+
+v 8.3.2
+ - Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu)
+ - Add support for Google reCAPTCHA in user registration
v 8.3.1
- Fix Error 500 when global milestones have slashes (Stan Hu)
@@ -83,6 +98,8 @@ v 8.3.0
- Do not show build status unless builds are enabled and `.gitlab-ci.yml` is present
- Persist runners registration token in database
- Fix online editor should not remove newlines at the end of the file
+ - Expose Git's version in the admin area
+ - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
v 8.2.3
- Fix application settings cache not expiring after changes (Stan Hu)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 950824e35ab..b9c2b3d2f8e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -155,6 +155,28 @@ sudo -u git -H bundle exec rake gitlab:env:info)
```
+### Issue weight
+
+Issue weight allows us to get an idea of the amount of work required to solve
+one or multiple issues. This makes it possible to schedule work more accurately.
+
+You are encouraged to set the weight of any issue. Following the guidelines
+below will make it easy to manage this, without unnecessary overhead.
+
+1. Set weight for any issue at the earliest possible convenience
+1. If you don't agree with a set weight, discuss with other developers until
+consensus is reached about the weight
+1. Issue weights are an abstract measurement of complexity of the issue. Do not
+relate issue weight directly to time. This is called [anchoring](https://en.wikipedia.org/wiki/Anchoring)
+and something you want to avoid.
+1. Something that has a weight of 1 (or no weight) is really small and simple.
+Something that is 9 is rewriting a large fundamental part of GitLab,
+which might lead to many hard problems to solve. Changing some text in GitLab
+is probably 1, adding a new Git Hook maybe 4 or 5, big features 7-9.
+1. If something is very large, it should probably be split up in multiple
+issues or chunks. You can simply not set the weight of a parent issue and set
+weights to children issues.
+
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
diff --git a/Gemfile b/Gemfile
index db54bf2f186..3ce4ba4a2a5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -35,6 +35,9 @@ gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd'
gem 'rack-oauth2', '~> 1.2.1'
+# reCAPTCHA protection
+gem 'recaptcha', require: 'recaptcha/rails'
+
# Two-factor authentication
gem 'devise-two-factor', '~> 2.0.0'
gem 'rqrcode-rails3', '~> 0.1.7'
@@ -166,10 +169,10 @@ gem 'asana', '~> 0.4.0'
gem 'ruby-fogbugz', '~> 0.2.1'
# d3
-gem 'd3_rails', '~> 3.5.5'
+gem 'd3_rails', '~> 3.5.0'
#cal-heatmap
-gem "cal-heatmap-rails", "~> 0.0.1"
+gem 'cal-heatmap-rails', '~> 3.5.0'
# underscore-rails
gem "underscore-rails", "~> 1.8.0"
@@ -197,7 +200,7 @@ gem 'turbolinks', '~> 2.5.0'
gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
-gem 'bootstrap-sass', '~> 3.0'
+gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.2.0'
gem 'gon', '~> 6.0.1'
@@ -212,9 +215,17 @@ gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
+# Metrics
+group :metrics do
+ gem 'allocations', '~> 1.0', require: false, platform: :mri
+ gem 'method_source', '~> 0.8', require: false
+ gem 'influxdb', '~> 0.2', require: false
+ gem 'connection_pool', '~> 2.0', require: false
+end
+
group :development do
gem "foreman"
- gem 'brakeman', '3.0.1', require: false
+ gem 'brakeman', '~> 3.1.0', require: false
gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4f4b10c0fb7..ffb7cef0aba 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -49,6 +49,7 @@ GEM
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
+ allocations (1.0.3)
annotate (2.6.10)
activerecord (>= 3.2, <= 4.3)
rake (~> 10.4)
@@ -65,7 +66,7 @@ GEM
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
- autoprefixer-rails (6.1.1)
+ autoprefixer-rails (6.2.3)
execjs
json
awesome_print (1.2.0)
@@ -81,18 +82,20 @@ GEM
erubis (>= 2.6.6)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
- bootstrap-sass (3.3.5)
- autoprefixer-rails (>= 5.0.0.1)
- sass (>= 3.2.19)
- brakeman (3.0.1)
+ bootstrap-sass (3.3.6)
+ autoprefixer-rails (>= 5.2.1)
+ sass (>= 3.3.4)
+ brakeman (3.1.4)
erubis (~> 2.6)
fastercsv (~> 1.5)
haml (>= 3.0, < 5.0)
- highline (~> 1.6.20)
+ highline (>= 1.6.20, < 2.0)
multi_json (~> 1.2)
- ruby2ruby (~> 2.1.1)
- ruby_parser (~> 3.5.0)
+ ruby2ruby (>= 2.1.1, < 2.3.0)
+ ruby_parser (~> 3.7.0)
+ safe_yaml (>= 1.0)
sass (~> 3.0)
+ slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4)
browser (1.0.1)
builder (3.2.2)
@@ -102,8 +105,8 @@ GEM
bundler-audit (0.4.0)
bundler (~> 1.2)
thor (~> 0.18)
- byebug (8.2.0)
- cal-heatmap-rails (0.0.1)
+ byebug (8.2.1)
+ cal-heatmap-rails (3.5.1)
capybara (2.4.4)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
@@ -117,6 +120,7 @@ GEM
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
json (>= 1.7)
+ cause (0.1)
charlock_holmes (0.7.3)
chunky_png (1.3.5)
cliver (0.3.2)
@@ -140,10 +144,10 @@ GEM
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
tins (~> 1.6.0)
- crack (0.4.2)
+ crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
- d3_rails (3.5.6)
+ d3_rails (3.5.11)
railties (>= 3.1.0)
daemons (1.2.3)
database_cleaner (1.4.1)
@@ -230,7 +234,7 @@ GEM
ipaddress (~> 0.5)
nokogiri (~> 1.5, >= 1.5.11)
opennebula
- fog-brightbox (0.9.0)
+ fog-brightbox (0.10.1)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
@@ -249,7 +253,7 @@ GEM
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
- fog-sakuracloud (1.4.0)
+ fog-sakuracloud (1.5.0)
fog-core
fog-json
fog-softlayer (1.0.2)
@@ -277,11 +281,11 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
- gemojione (2.1.0)
+ gemojione (2.1.1)
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
- github-linguist (4.7.2)
+ github-linguist (4.7.3)
charlock_holmes (~> 0.7.3)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
@@ -298,7 +302,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.2.0)
gemojione (~> 2.1)
- gitlab_git (7.2.21)
+ gitlab_git (7.2.22)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -347,7 +351,7 @@ GEM
html2haml (>= 1.0.1)
railties (>= 4.0.1)
hashie (3.4.3)
- highline (1.6.21)
+ highline (1.7.8)
hike (1.2.3)
hipchat (1.5.2)
httparty
@@ -370,6 +374,9 @@ GEM
i18n (0.7.0)
ice_nine (0.11.1)
inflecto (0.0.2)
+ influxdb (0.2.3)
+ cause
+ json
ipaddress (0.8.0)
jquery-atwho-rails (1.3.2)
jquery-rails (4.0.5)
@@ -417,7 +424,7 @@ GEM
net-ldap (0.12.1)
net-ssh (3.0.1)
netrc (0.11.0)
- newrelic-grape (2.0.0)
+ newrelic-grape (2.1.0)
grape
newrelic_rpm
newrelic_rpm (3.9.4.245)
@@ -566,6 +573,8 @@ GEM
trollop
rdoc (3.12.2)
json (~> 1.4)
+ recaptcha (1.0.2)
+ json
redcarpet (3.3.3)
redis (3.2.2)
redis-actionpack (4.0.1)
@@ -636,10 +645,10 @@ GEM
ruby-saml (1.0.0)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
- ruby2ruby (2.1.4)
+ ruby2ruby (2.2.0)
ruby_parser (~> 3.1)
sexp_processor (~> 4.0)
- ruby_parser (3.5.0)
+ ruby_parser (3.7.2)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
@@ -693,6 +702,9 @@ GEM
tilt (>= 1.3, < 3)
six (0.2.0)
slack-notifier (1.2.1)
+ slim (3.0.6)
+ temple (~> 0.7.3)
+ tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
spinach (0.8.10)
colorize
@@ -734,6 +746,7 @@ GEM
railties (>= 3.2.5, < 5)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
+ temple (0.7.6)
term-ansicolor (1.3.2)
tins (~> 1.0)
terminal-table (1.5.2)
@@ -789,7 +802,7 @@ GEM
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
- warden (1.2.3)
+ warden (1.2.4)
rack (>= 1.0)
web-console (2.2.1)
activemodel (>= 4.0)
@@ -820,6 +833,7 @@ DEPENDENCIES
acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8)
after_commit_queue
+ allocations (~> 1.0)
annotate (~> 2.6.0)
asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
@@ -829,22 +843,23 @@ DEPENDENCIES
benchmark-ips
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
- bootstrap-sass (~> 3.0)
- brakeman (= 3.0.1)
+ bootstrap-sass (~> 3.3.0)
+ brakeman (~> 3.1.0)
browser (~> 1.0.0)
bullet
bundler-audit
byebug
- cal-heatmap-rails (~> 0.0.1)
+ cal-heatmap-rails (~> 3.5.0)
capybara (~> 2.4.0)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.9.0)
charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0)
colorize (~> 0.7.0)
+ connection_pool (~> 2.0)
coveralls (~> 0.8.2)
creole (~> 0.5.0)
- d3_rails (~> 3.5.5)
+ d3_rails (~> 3.5.0)
database_cleaner (~> 1.4.0)
default_value_for (~> 3.0.0)
devise (~> 3.5.3)
@@ -879,6 +894,7 @@ DEPENDENCIES
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
+ influxdb (~> 0.2)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.0.0)
jquery-scrollto-rails (~> 1.4.3)
@@ -887,6 +903,7 @@ DEPENDENCIES
kaminari (~> 0.16.3)
letter_opener (~> 1.1.2)
mail_room (~> 0.6.1)
+ method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
@@ -924,6 +941,7 @@ DEPENDENCIES
raphael-rails (~> 2.1.2)
rblineprof
rdoc (~> 3.6)
+ recaptcha
redcarpet (~> 3.3.3)
redis-namespace
redis-rails (~> 4.0.0)
diff --git a/LICENSE b/LICENSE
index d8cb29f3638..1dc1bdb7411 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011-2015 GitLab B.V.
+Copyright (c) 2011-2016 GitLab B.V.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/app/assets/images/brand_logo.png b/app/assets/images/brand_logo.png
deleted file mode 100644
index 9c564bb6141..00000000000
--- a/app/assets/images/brand_logo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/gitlab_logo.png b/app/assets/images/gitlab_logo.png
new file mode 100644
index 00000000000..0c157546b9c
--- /dev/null
+++ b/app/assets/images/gitlab_logo.png
Binary files differ
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index affab5bb030..b9b095e004a 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -10,12 +10,12 @@
#= require jquery.cookie
#= require jquery.endless-scroll
#= require jquery.highlight
-#= require jquery.history
#= require jquery.waitforimages
#= require jquery.atwho
#= require jquery.scrollTo
-#= require jquery.blockUI
#= require jquery.turbolinks
+#= require d3
+#= require cal-heatmap
#= require turbolinks
#= require autosave
#= require bootstrap
@@ -27,7 +27,6 @@
#= require branch-graph
#= require ace/ace
#= require ace/ext-searchbox
-#= require d3
#= require underscore
#= require nprogress
#= require nprogress-turbolinks
@@ -39,7 +38,6 @@
#= require shortcuts_dashboard_navigation
#= require shortcuts_issuable
#= require shortcuts_network
-#= require cal-heatmap
#= require jquery.nicescroll.min
#= require_tree .
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 04bf5cc7bb5..619abb1fb07 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -43,15 +43,19 @@ class @AwardsHandler
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
+ emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
- counter.parent().removeClass("active")
+ emojiIcon.removeClass("active")
@removeMeFromAuthorList(emoji)
+ else if emoji =="thumbsup" || emoji == "thumbsdown"
+ emojiIcon.tooltip("destroy")
+ counter.text(0)
+ emojiIcon.removeClass("active")
else
- award = counter.parent()
- award.tooltip("destroy")
- award.remove()
+ emojiIcon.tooltip("destroy")
+ emojiIcon.remove()
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
@@ -127,21 +131,19 @@ class @AwardsHandler
getFrequentlyUsedEmojis: ->
frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",")
-
- frequently_used_emojis = ["thumbsup", "thumbsdown"].concat(frequently_used_emojis)
-
_.compact(_.uniq(frequently_used_emojis))
renderFrequentlyUsedBlock: ->
- frequently_used_emojis = @getFrequentlyUsedEmojis()
+ if $.cookie('frequently_used_emojis')
+ frequently_used_emojis = @getFrequentlyUsedEmojis()
- ul = $("<ul>")
+ ul = $("<ul>")
- for emoji in frequently_used_emojis
- do (emoji) ->
- $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul)
+ for emoji in frequently_used_emojis
+ do (emoji) ->
+ $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul)
- $("input.emoji-search").after(ul).after($("<h5>").text("Frequently used"))
+ $("input.emoji-search").after(ul).after($("<h5>").text("Frequently used"))
setupSearch: ->
$("input.emoji-search").keyup (ev) =>
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee
index 97621236924..d80e0e716ce 100644
--- a/app/assets/javascripts/calendar.js.coffee
+++ b/app/assets/javascripts/calendar.js.coffee
@@ -1,9 +1,4 @@
class @Calendar
- options =
- month: "short"
- day: "numeric"
- year: "numeric"
-
constructor: (timestamps, starting_year, starting_month, calendar_activities_path) ->
cal = new CalHeatMap()
cal.init
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 599b4c49540..69e061ce6e9 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -49,7 +49,7 @@ class Dispatcher
new DropzoneInput($('.release-form'))
when 'projects:merge_requests:show'
new Diff()
- shortcut_handler = new ShortcutsIssuable()
+ shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
when "projects:merge_requests:diffs"
new Diff()
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index eff80bf63bb..c256ec8f41b 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -1,3 +1,4 @@
+#= require flash
#= require jquery.waitforimages
#= require task_list
@@ -6,13 +7,44 @@ class @Issue
# Prevent duplicate event bindings
@disableTaskList()
- if $("a.btn-close").length
+ if $('a.btn-close').length
@initTaskList()
+ @initIssueBtnEventListeners()
initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
+ initIssueBtnEventListeners: ->
+ issueFailMessage = 'Unable to update this issue at this time.'
+ $('a.btn-close, a.btn-reopen').on 'click', (e) ->
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ $this = $(this)
+ isClose = $this.hasClass('btn-close')
+ $this.prop('disabled', true)
+ url = $this.attr('href')
+ $.ajax
+ type: 'PUT'
+ url: url,
+ error: (jqXHR, textStatus, errorThrown) ->
+ issueStatus = if isClose then 'close' else 'open'
+ new Flash(issueFailMessage, 'alert')
+ success: (data, textStatus, jqXHR) ->
+ if data.saved
+ $this.addClass('hidden')
+ if isClose
+ $('a.btn-reopen').removeClass('hidden')
+ $('div.status-box-closed').removeClass('hidden')
+ $('div.status-box-open').addClass('hidden')
+ else
+ $('a.btn-close').removeClass('hidden')
+ $('div.status-box-closed').addClass('hidden')
+ $('div.status-box-open').removeClass('hidden')
+ else
+ new Flash(issueFailMessage, 'alert')
+ $this.prop('disabled', false)
+
disableTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index ac9e022e727..a0acf3028bf 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -15,13 +15,6 @@
$(this).html totalIssues + 1
else
$(this).html totalIssues - 1
- $("body").on "click", ".issues-other-filters .dropdown-menu a", ->
- $('.issues-list').block(
- message: null,
- overlayCSS:
- backgroundColor: '#DDD'
- opacity: .4
- )
reload: ->
Issues.initSelects()
@@ -54,7 +47,7 @@
form = $("#issue_search_form")
search = $("#issue_search").val()
$('.issues-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '? '+ form.serialize()
+ issues_url = form.attr('action') + '?' + form.serialize()
$.ajax
type: "GET"
@@ -65,7 +58,7 @@
success: (data) ->
$('.issues-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- History.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issues_url}, document.title, issues_url
Issues.reload()
dataType: "json"
diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee
new file mode 100644
index 00000000000..e864a674cdd
--- /dev/null
+++ b/app/assets/javascripts/logo.js.coffee
@@ -0,0 +1,43 @@
+NProgress.configure(showSpinner: false)
+
+defaultClass = 'tanuki-shape'
+pieces = [
+ 'path#tanuki-right-cheek',
+ 'path#tanuki-right-eye, path#tanuki-right-ear',
+ 'path#tanuki-nose',
+ 'path#tanuki-left-eye, path#tanuki-left-ear',
+ 'path#tanuki-left-cheek',
+]
+pieceIndex = 0
+firstPiece = pieces[0]
+
+currentTimer = null
+delay = 150
+
+clearHighlights = ->
+ $(".#{defaultClass}.highlight").attr('class', defaultClass)
+
+start = ->
+ clearHighlights()
+ pieceIndex = 0
+ pieces.reverse() unless pieces[0] == firstPiece
+ currentTimer = setInterval(work, delay)
+
+stop = ->
+ clearInterval(currentTimer)
+ clearHighlights()
+
+work = ->
+ clearHighlights()
+ $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight")
+
+ # If we hit the last piece, reset the index and then reverse the array to
+ # get a nice back-and-forth sweeping look
+ if pieceIndex == pieces.length - 1
+ pieceIndex = 0
+ pieces.reverse()
+ else
+ pieceIndex++
+
+$(document).on('page:fetch', start)
+$(document).on('page:change', stop)
diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee
index 83434c1b9ba..b3c73ffce5d 100644
--- a/app/assets/javascripts/merge_requests.js.coffee
+++ b/app/assets/javascripts/merge_requests.js.coffee
@@ -16,7 +16,7 @@
form = $("#issue_search_form")
search = $("#issue_search").val()
$('.merge-requests-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '? '+ form.serialize()
+ issues_url = form.attr('action') + '?' + form.serialize()
$.ajax
type: "GET"
@@ -27,7 +27,7 @@
success: (data) ->
$('.merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- History.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issues_url}, document.title, issues_url
MergeRequests.reload()
dataType: "json"
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
index e9aeb1e9525..4d915bfc8c5 100644
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ b/app/assets/javascripts/shortcuts.js.coffee
@@ -7,7 +7,7 @@ class @Shortcuts
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
-
+
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
@@ -17,8 +17,7 @@ class @Shortcuts
dataType: 'script',
success: (e) ->
if location and location.length > 0
- for l in location
- $(l).show()
+ $(l).show() for l in location
else
$('.hidden-shortcut').show()
$('.js-more-help-button').remove()
@@ -28,3 +27,8 @@ class @Shortcuts
@focusSearch: (e) ->
$('#search').focus()
e.preventDefault()
+
+$(document).on 'click.more_help', '.js-more-help-button', (e) ->
+ $(@).remove()
+ $('.hidden-shortcut').show()
+ e.preventDefault()
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 12abf806bfa..9467011799f 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -117,5 +117,5 @@ class @UsersSelect
callback(users)
buildUrl: (url) ->
- url = gon.relative_url_root + url if gon.relative_url_root?
+ url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root?
return url
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index a36fefe22c5..580012abd77 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -19,38 +19,33 @@
}
}
}
+
/**
* This overwrites the default values of the cal-heatmap gem
*/
.calendar {
.qi {
- background-color: #999;
fill: #fff;
}
.q1 {
- background-color: #dae289;
- fill: #ededed;
+ fill: #ededed !important;
}
.q2 {
- background-color: #cedb9c;
- fill: #ACD5F2;
+ fill: #ACD5F2 !important;
}
.q3 {
- background-color: #b5cf6b;
- fill: #7FA8D1;
+ fill: #7FA8D1 !important;
}
.q4 {
- background-color: #637939;
- fill: #49729B;
+ fill: #49729B !important;
}
.q5 {
- background-color: #3b6427;
- fill: #254E77;
+ fill: #254E77 !important;
}
.domain-background {
@@ -59,32 +54,7 @@
}
.ch-tooltip {
- position: absolute;
- display: none;
- margin-top: 22px;
- margin-left: 1px;
- font-size: 13px;
padding: 3px;
font-weight: 550;
- background-color: #222;
- span {
- position: absolute;
- width: 200px;
- text-align: center;
- visibility: hidden;
- border-radius: 10px;
- &:after {
- content: '';
- position: absolute;
- top: 100%;
- left: 50%;
- margin-left: -8px;
- width: 0;
- height: 0;
- border-top: 8px solid #000000;
- border-right: 8px solid transparent;
- border-left: 8px solid transparent;
- }
- }
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 458af76cb75..83243dd2457 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -105,7 +105,7 @@
.tanuki-shape {
transition: all 0.8s;
- &:hover {
+ &:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index c3e4ad0ad00..714369d9f15 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -54,17 +54,17 @@
h3 {
margin: 24px 0 12px 0;
- font-size: 1.25em;
+ font-size: 1.1em;
}
h4 {
margin: 24px 0 12px 0;
- font-size: 1.1em;
+ font-size: 0.98em;
}
h5 {
margin: 24px 0 12px 0;
- font-size: 1em;
+ font-size: 0.95em;
}
h6 {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index cff3edb7ed2..be6ef43e49c 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -26,6 +26,13 @@
}
.project-home-panel {
+
+ .cover-controls {
+ .project-settings-dropdown {
+ margin-left: 10px;
+ }
+ }
+
.project-identicon-holder {
margin-bottom: 16px;
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 20bc5173f1d..38814459f66 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -9,12 +9,10 @@ class AbuseReportsController < ApplicationController
@abuse_report.reporter = current_user
if @abuse_report.save
- if current_application_settings.admin_notification_email.present?
- AbuseReportMailer.notify(@abuse_report.id).deliver_later
- end
+ @abuse_report.notify
message = "Thank you for your report. A GitLab administrator will look into it shortly."
- redirect_to root_path, notice: message
+ redirect_to @abuse_report.user, notice: message
else
render :new
end
@@ -23,6 +21,9 @@ class AbuseReportsController < ApplicationController
private
def report_params
- params.require(:abuse_report).permit(:user_id, :message)
+ params.require(:abuse_report).permit(%i(
+ message
+ user_id
+ ))
end
end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 2f4a855c118..10e736fd362 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -67,6 +67,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:user_oauth_applications,
:shared_runners_enabled,
:max_artifacts_size,
+ :metrics_enabled,
+ :metrics_host,
+ :metrics_port,
+ :metrics_username,
+ :metrics_password,
+ :metrics_pool_size,
+ :metrics_timeout,
+ :metrics_method_call_threshold,
+ :recaptcha_enabled,
+ :recaptcha_site_key,
+ :recaptcha_private_key,
restricted_visibility_levels: [],
import_sources: []
)
diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb
index 83d9684c706..0db91eaaf2e 100644
--- a/app/controllers/admin/builds_controller.rb
+++ b/app/controllers/admin/builds_controller.rb
@@ -5,12 +5,12 @@ class Admin::BuildsController < Admin::ApplicationController
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
- when 'all'
- @builds
+ when 'running'
+ @builds.running_or_pending.reverse_order
when 'finished'
@builds.finished
else
- @builds.running_or_pending.reverse_order
+ @builds
end
@builds = @builds.page(params[:page]).per(30)
end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index 9575a87ee41..a9bf4321f73 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,6 +1,6 @@
class Explore::GroupsController < Explore::ApplicationController
def index
- @groups = GroupsFinder.new.execute(current_user)
+ @groups = Group.order_id_desc
@groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page]).per(PER_PAGE)
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 26ba12520c7..39d3ba26ba2 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -12,12 +12,12 @@ class Projects::BuildsController < Projects::ApplicationController
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
- when 'all'
- @builds
+ when 'running'
+ @builds.running_or_pending.reverse_order
when 'finished'
@builds.finished
else
- @builds.running_or_pending.reverse_order
+ @builds
end
@builds = @builds.page(params[:page]).per(30)
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 58fb946dbc2..04a88990bf4 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -9,7 +9,7 @@ class Projects::CommitsController < Projects::ApplicationController
def show
@repo = @project.repository
- @limit, @offset = (params[:limit] || 40), (params[:offset] || 0)
+ @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
@commits = @repo.commits(@ref, @path, @limit, @offset)
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 2dab04f2a7c..3004722bce0 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -178,7 +178,7 @@ class ProjectsController < ApplicationController
def markdown_preview
text = params[:text]
- ext = Gitlab::ReferenceExtractor.new(@project, current_user)
+ ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
ext.analyze(text)
render json: {
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 3b3dc86cb68..c48175a4c5a 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,10 +1,21 @@
class RegistrationsController < Devise::RegistrationsController
before_action :signup_enabled?
+ include Recaptcha::Verify
def new
redirect_to(new_user_session_path)
end
+ def create
+ if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
+ super
+ else
+ flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
+ flash.delete :recaptcha_error
+ render action: 'new'
+ end
+ end
+
def destroy
DeleteUserService.new(current_user).execute(current_user)
@@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController
def sign_up_params
params.require(:user).permit(:username, :email, :name, :password, :password_confirmation)
end
+
+ def resource_name
+ :user
+ end
+
+ def resource
+ @resource ||= User.new(sign_up_params)
+ end
+
+ def devise_mapping
+ @devise_mapping ||= Devise.mappings[:user]
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1b60d3e27d0..825f85199be 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,9 +1,11 @@
class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
+ include Recaptcha::ClientHelper
prepend_before_action :authenticate_with_two_factor, only: [:create]
prepend_before_action :store_redirect_path, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new]
+ before_action :load_recaptcha
def new
if Gitlab.config.ldap.enabled
@@ -40,7 +42,7 @@ class SessionsController < Devise::SessionsController
User.find(session[:otp_user_id])
end
end
-
+
def store_redirect_path
redirect_path =
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
@@ -87,14 +89,14 @@ class SessionsController < Devise::SessionsController
provider = Gitlab.config.omniauth.auto_sign_in_with_provider
return unless provider.present?
- # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is
- # registered or no alert at all. In case of another alert (such as a blocked user), it is safer
+ # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is
+ # registered or no alert at all. In case of another alert (such as a blocked user), it is safer
# to do nothing to prevent redirection loops with certain Omniauth providers.
return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated')
-
+
# Prevent alert from popping up on the first page shown after authentication.
- flash[:alert] = nil
-
+ flash[:alert] = nil
+
redirect_to user_omniauth_authorize_path(provider.to_sym)
end
@@ -107,4 +109,8 @@ class SessionsController < Devise::SessionsController
AuditEventService.new(user, user, options).
for_authentication.security_event
end
+
+ def load_recaptcha
+ Gitlab::Recaptcha.load_configurations!
+ end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 30cb869eb2a..280228dbcc0 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -7,7 +7,7 @@ class UsersController < ApplicationController
@projects = PersonalProjectsFinder.new(@user).execute(current_user)
- @groups = JoinedGroupsFinder.new(@user).execute(current_user)
+ @groups = @user.groups.order_id_desc
respond_to do |format|
format.html
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
deleted file mode 100644
index 91cb0f228f0..00000000000
--- a/app/finders/groups_finder.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-class GroupsFinder
- # Finds the groups available to the given user.
- #
- # current_user - The user to find the groups for.
- #
- # Returns an ActiveRecord::Relation.
- def execute(current_user = nil)
- if current_user
- relation = groups_visible_to_user(current_user)
- else
- relation = public_groups
- end
-
- relation.order_id_desc
- end
-
- private
-
- # This method returns the groups "current_user" can see.
- def groups_visible_to_user(current_user)
- base = groups_for_projects(public_and_internal_projects)
-
- union = Gitlab::SQL::Union.
- new([base.select(:id), current_user.authorized_groups.select(:id)])
-
- Group.where("namespaces.id IN (#{union.to_sql})")
- end
-
- def public_groups
- groups_for_projects(public_projects)
- end
-
- def groups_for_projects(projects)
- Group.public_and_given_groups(projects.select(:namespace_id))
- end
-
- def public_projects
- Project.unscoped.public_only
- end
-
- def public_and_internal_projects
- Project.unscoped.public_and_internal_only
- end
-end
diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb
deleted file mode 100644
index e7523136fea..00000000000
--- a/app/finders/joined_groups_finder.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# Class for finding the groups a user is a member of.
-class JoinedGroupsFinder
- def initialize(user = nil)
- @user = user
- end
-
- # Finds the groups of the source user, optionally limited to those visible to
- # the current user.
- #
- # current_user - If given the groups of "@user" will only include the groups
- # "current_user" can also see.
- #
- # Returns an ActiveRecord::Relation.
- def execute(current_user = nil)
- if current_user
- relation = groups_visible_to_user(current_user)
- else
- relation = public_groups
- end
-
- relation.order_id_desc
- end
-
- private
-
- # Returns the groups the user in "current_user" can see.
- #
- # This list includes all public/internal projects as well as the projects of
- # "@user" that "current_user" also has access to.
- def groups_visible_to_user(current_user)
- base = @user.authorized_groups.visible_to_user(current_user)
- extra = public_and_internal_groups
- union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
-
- Group.where("namespaces.id IN (#{union.to_sql})")
- end
-
- def public_groups
- groups_for_projects(@user.authorized_projects.public_only)
- end
-
- def public_and_internal_groups
- groups_for_projects(@user.authorized_projects.public_and_internal_only)
- end
-
- def groups_for_projects(projects)
- @user.groups.public_and_given_groups(projects.select(:namespace_id))
- end
-end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0b00b9a0702..f7f7a1a02d3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
- user = User.find_by(email: user_or_email)
+ user = User.find_by(email: user_or_email.downcase)
end
if user
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 4fe84322199..c12456a187f 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -69,6 +69,10 @@ module IssuesHelper
end
end
+ def issue_button_visibility(issue, closed)
+ return 'hidden' if issue.closed? == closed
+ end
+
def issue_to_atom(xml, issue)
xml.entry do
xml.id namespace_project_issue_url(issue.project.namespace,
@@ -95,7 +99,7 @@ module IssuesHelper
end
def emoji_icon(name, unicode = nil, aliases = [])
- unicode ||= Emoji.emoji_filename(name)
+ unicode ||= Emoji.emoji_filename(name) rescue ""
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
@@ -120,6 +124,18 @@ module IssuesHelper
end
end
+ def awards_sort(awards)
+ awards.sort_by do |award, notes|
+ if award == "thumbsup"
+ 0
+ elsif award == "thumbsdown"
+ 1
+ else
+ 2
+ end
+ end.to_h
+ end
+
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 9bf750124b2..791cb9e50bd 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -8,6 +8,80 @@ module PageLayoutHelper
@page_title.join(" \u00b7 ")
end
+ # Define or get a description for the current page
+ #
+ # description - String (default: nil)
+ #
+ # If this helper is called multiple times with an argument, only the last
+ # description will be returned when called without an argument. Descriptions
+ # have newlines replaced with spaces and all HTML tags are sanitized.
+ #
+ # Examples:
+ #
+ # page_description # => "GitLab Community Edition"
+ # page_description("Foo")
+ # page_description # => "Foo"
+ #
+ # page_description("<b>Bar</b>\nBaz")
+ # page_description # => "Bar Baz"
+ #
+ # Returns an HTML-safe String.
+ def page_description(description = nil)
+ @page_description ||= page_description_default
+
+ if description.present?
+ @page_description = description.squish
+ else
+ sanitize(@page_description, tags: []).truncate_words(30)
+ end
+ end
+
+ # Default value for page_description when one hasn't been defined manually by
+ # a view
+ def page_description_default
+ if @project
+ @project.description || brand_title
+ else
+ brand_title
+ end
+ end
+
+ def page_image
+ default = image_url('gitlab_logo.png')
+
+ if @project
+ @project.avatar_url || default
+ elsif @user
+ avatar_icon(@user)
+ else
+ default
+ end
+ end
+
+ # Define or get attributes to be used as Twitter card metadata
+ #
+ # map - Hash of label => data pairs. Keys become labels, values become data
+ #
+ # Raises ArgumentError if given more than two attributes
+ def page_card_attributes(map = {})
+ raise ArgumentError, 'cannot provide more than two attributes' if map.length > 2
+
+ @page_card_attributes ||= {}
+ @page_card_attributes = map.reject { |_,v| v.blank? } if map.present?
+ @page_card_attributes
+ end
+
+ def page_card_meta_tags
+ tags = ''
+
+ page_card_attributes.each_with_index do |pair, i|
+ tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0])
+ tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1])
+ end
+
+ tags.html_safe
+ end
+
def header_title(title = nil, title_url = nil)
if title
@header_title = title
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index a6ee6880247..d4f78258626 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -70,7 +70,7 @@ module SearchHelper
# Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5)
- GroupsFinder.new.execute(current_user).search(term).limit(limit).map do |group|
+ Group.search(term).limit(limit).map do |group|
{
label: "group: #{search_result_sanitize(group.name)}",
url: group_path(group)
diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb
index f0c41f69a5c..d0ce827a595 100644
--- a/app/mailers/abuse_report_mailer.rb
+++ b/app/mailers/abuse_report_mailer.rb
@@ -2,11 +2,19 @@ class AbuseReportMailer < BaseMailer
include Gitlab::CurrentSettings
def notify(abuse_report_id)
+ return unless deliverable?
+
@abuse_report = AbuseReport.find(abuse_report_id)
mail(
- to: current_application_settings.admin_notification_email,
+ to: current_application_settings.admin_notification_email,
subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse"
)
end
+
+ private
+
+ def deliverable?
+ current_application_settings.admin_notification_email.present?
+ end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 65f37e92677..e1382d2da12 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -48,7 +48,7 @@ module Emails
yield
- SentNotification.record(@note, recipient_id, reply_key)
+ SentNotification.record_note(@note, recipient_id, reply_key)
end
end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 1b3ee757040..5a1a67db8e1 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -69,7 +69,7 @@ class Ability
subject.group
end
- if group && group.public_profile?
+ if group && group.projects.public_only.any?
[:read_group]
else
[]
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 89b3116b9f2..55864236b2f 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -18,4 +18,10 @@ class AbuseReport < ActiveRecord::Base
validates :user, presence: true
validates :message, presence: true
validates :user_id, uniqueness: true
+
+ def notify
+ return unless self.persisted?
+
+ AbuseReportMailer.notify(self.id).deliver_later
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 7c107da116c..be69d317d73 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -44,24 +44,32 @@ class ApplicationSetting < ActiveRecord::Base
attr_accessor :restricted_signup_domains_raw
validates :session_expire_delay,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :home_page_url,
- allow_blank: true,
- url: true,
- if: :home_page_url_column_exist
+ allow_blank: true,
+ url: true,
+ if: :home_page_url_column_exist
validates :after_sign_out_path,
- allow_blank: true,
- url: true
+ allow_blank: true,
+ url: true
validates :admin_notification_email,
- allow_blank: true,
- email: true
+ allow_blank: true,
+ email: true
validates :two_factor_grace_period,
- numericality: { greater_than_or_equal_to: 0 }
+ numericality: { greater_than_or_equal_to: 0 }
+
+ validates :recaptcha_site_key,
+ presence: true,
+ if: :recaptcha_enabled
+
+ validates :recaptcha_private_key,
+ presence: true,
+ if: :recaptcha_enabled
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7b89fe069ea..3e67b2771c1 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -194,8 +194,11 @@ module Ci
end
def raw_trace
- if File.exist?(path_to_trace)
+ if File.file?(path_to_trace)
File.read(path_to_trace)
+ elsif project.ci_id && File.file?(old_path_to_trace)
+ # Temporary fix for build trace data integrity
+ File.read(old_path_to_trace)
else
# backward compatibility
read_attribute :trace
@@ -212,8 +215,8 @@ module Ci
end
def trace=(trace)
- unless Dir.exists? dir_to_trace
- FileUtils.mkdir_p dir_to_trace
+ unless Dir.exists?(dir_to_trace)
+ FileUtils.mkdir_p(dir_to_trace)
end
File.write(path_to_trace, trace)
@@ -231,6 +234,55 @@ module Ci
"#{dir_to_trace}/#{id}.log"
end
+ ##
+ # Deprecated
+ #
+ # This is a hotfix for CI build data integrity, see #4246
+ # Should be removed in 8.4, after CI files migration has been done.
+ #
+ def old_dir_to_trace
+ File.join(
+ Settings.gitlab_ci.builds_path,
+ created_at.utc.strftime("%Y_%m"),
+ project.ci_id.to_s
+ )
+ end
+
+ ##
+ # Deprecated
+ #
+ # This is a hotfix for CI build data integrity, see #4246
+ # Should be removed in 8.4, after CI files migration has been done.
+ #
+ def old_path_to_trace
+ "#{old_dir_to_trace}/#{id}.log"
+ end
+
+ ##
+ # Deprecated
+ #
+ # This contains a hotfix for CI build data integrity, see #4246
+ #
+ # This method is used by `ArtifactUploader` to create a store_dir.
+ # Warning: Uploader uses it after AND before file has been stored.
+ #
+ # This method returns old path to artifacts only if it already exists.
+ #
+ def artifacts_path
+ old = File.join(created_at.utc.strftime('%Y_%m'),
+ project.ci_id.to_s,
+ id.to_s)
+
+ old_store = File.join(ArtifactUploader.artifacts_path, old)
+ return old if project.ci_id && File.directory?(old_store)
+
+ File.join(
+ created_at.utc.strftime('%Y_%m'),
+ project.id.to_s,
+ id.to_s
+ )
+ end
+
def token
project.runners_token
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index f56fd3e02d4..18a00f95b48 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -95,14 +95,12 @@ module Issuable
opened? || reopened?
end
- # Deprecated. Still exists to preserve API compatibility.
def downvotes
- 0
+ notes.awards.where(note: "thumbsdown").count
end
- # Deprecated. Still exists to preserve API compatibility.
def upvotes
- 0
+ notes.awards.where(note: "thumbsup").count
end
def subscribed?(user)
@@ -161,6 +159,14 @@ module Issuable
self.class.to_s.underscore
end
+ # Returns a Hash of attributes to be used for Twitter card metadata
+ def card_attributes
+ {
+ 'Author' => author.try(:name),
+ 'Assignee' => assignee.try(:name)
+ }
+ end
+
def notes_with_associations
notes.includes(:author, :project)
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 1fdcda97520..6316ee208b5 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -44,7 +44,7 @@ module Mentionable
end
def all_references(current_user = self.author, text = nil)
- ext = Gitlab::ReferenceExtractor.new(self.project, current_user)
+ ext = Gitlab::ReferenceExtractor.new(self.project, current_user, self.author)
if text
ext.analyze(text)
diff --git a/app/models/group.rb b/app/models/group.rb
index 1b5b875a19e..b8f2ab6ae5d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -50,10 +50,6 @@ class Group < Namespace
User.reference_pattern
end
- def public_and_given_groups(ids)
- where('public IS TRUE OR namespaces.id IN (?)', ids)
- end
-
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
@@ -125,10 +121,6 @@ class Group < Namespace
end
end
- def public_profile?
- self.public || projects.public_only.any?
- end
-
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
diff --git a/app/models/note.rb b/app/models/note.rb
index 8c5b5836f9a..3d5b663c99f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -107,9 +107,16 @@ class Note < ActiveRecord::Base
end
def grouped_awards
+ notes = {}
+
awards.select(:note).distinct.map do |note|
- [ note.note, where(note: note.note) ]
+ notes[note.note] = where(note: note.note)
end
+
+ notes["thumbsup"] ||= Note.none
+ notes["thumbsdown"] ||= Note.none
+
+ notes
end
end
@@ -339,14 +346,12 @@ class Note < ActiveRecord::Base
read_attribute(:system)
end
- # Deprecated. Still exists to preserve API compatibility.
def downvote?
- false
+ is_award && note == "thumbsdown"
end
- # Deprecated. Still exists to preserve API compatibility.
def upvote?
- false
+ is_award && note == "thumbsup"
end
def editable?
diff --git a/app/models/project.rb b/app/models/project.rb
index 75f85310d5f..eadc42d1da5 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -81,6 +81,7 @@ class Project < ActiveRecord::Base
acts_as_taggable_on :tags
attr_accessor :new_default_branch
+ attr_accessor :old_path_with_namespace
# Relations
belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
@@ -555,7 +556,9 @@ class Project < ActiveRecord::Base
end
def send_move_instructions(old_path_with_namespace)
- NotificationService.new.project_was_moved(self, old_path_with_namespace)
+ # New project path needs to be committed to the DB or notification will
+ # retrieve stale information
+ run_after_commit { NotificationService.new.project_was_moved(self, old_path_with_namespace) }
end
def owner
@@ -699,6 +702,11 @@ class Project < ActiveRecord::Base
gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
reset_events_cache
+
+ @old_path_with_namespace = old_path_with_namespace
+
+ SystemHooksService.new.execute_hooks_for(self, :rename)
+
@repository = nil
rescue
# Returning false does not rollback after_* transaction but gives
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 9f688e3b45b..a9bf4eb4033 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -76,7 +76,9 @@ class Repository
path: path,
limit: limit,
offset: offset,
- follow: path.present?
+ # --follow doesn't play well with --skip. See:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
+ follow: false
}
commits = Gitlab::Git::Commit.where(options)
diff --git a/app/models/user.rb b/app/models/user.rb
index df87f3b79bd..20f907e4347 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -352,10 +352,13 @@ class User < ActiveRecord::Base
end
def namespace_uniq
+ # Return early if username already failed the first uniqueness validation
+ return if self.errors[:username].include?('has already been taken')
+
namespace_name = self.username
existing_namespace = Namespace.by_path(namespace_name)
if existing_namespace && existing_namespace != self.namespace
- self.errors.add :username, "already exists"
+ self.errors.add(:username, 'has already been taken')
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index bdf7b3ad2bb..e4edc55bf69 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -413,6 +413,7 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, target)
recipients.delete(current_user)
+ recipients = recipients.uniq
recipients
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 64ea6dd42eb..2e734654466 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -55,6 +55,9 @@ module Projects
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+ project.old_path_with_namespace = old_path
+
+ SystemHooksService.new.execute_hooks_for(project, :transfer)
true
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 8b5143e1eb7..6dc854ec33d 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -18,7 +18,8 @@ class SystemHooksService
def build_event_data(model, event)
data = {
event_name: build_event_name(model, event),
- created_at: model.created_at.xmlschema
+ created_at: model.created_at.xmlschema,
+ updated_at: model.updated_at.xmlschema
}
case model
@@ -34,6 +35,14 @@ class SystemHooksService
end
when Project
data.merge!(project_data(model))
+
+ if event == :rename || event == :transfer
+ data.merge!({
+ old_path_with_namespace: model.old_path_with_namespace
+ })
+ end
+
+ data
when User
data.merge!({
name: model.name,
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 1dccc39e7e2..1b0ae6c0056 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -20,16 +20,12 @@ class ArtifactUploader < CarrierWave::Uploader::Base
@build, @field = build, field
end
- def artifacts_path
- File.join(build.created_at.utc.strftime('%Y_%m'), build.project.id.to_s, build.id.to_s)
- end
-
def store_dir
- File.join(ArtifactUploader.artifacts_path, artifacts_path)
+ File.join(self.class.artifacts_path, @build.artifacts_path)
end
def cache_dir
- File.join(ArtifactUploader.artifacts_cache_path, artifacts_path)
+ File.join(self.class.artifacts_cache_path, @build.artifacts_path)
end
def file_storage?
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index cffd7684008..3e5cdd2ce4a 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -2,7 +2,7 @@
%h3.page-title Report abuse
%p Please use this form to report users who create spam issues, comments or behave inappropriately.
%hr
-= form_for @abuse_report, html: { class: 'form-horizontal'} do |f|
+= form_for @abuse_report, html: { class: 'form-horizontal js-requires-input'} do |f|
= f.hidden_field :user_id
- if @abuse_report.errors.any?
.alert.alert-danger
@@ -16,7 +16,7 @@
.form-group
= f.label :message, class: 'control-label'
.col-sm-10
- = f.text_area :message, class: "form-control", rows: 2, required: true
+ = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true
.help-block
Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index d3afc658cd6..cf50a376e11 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -2,19 +2,21 @@
- user = abuse_report.user
%tr
%td
- - if reporter
- = link_to reporter.name, reporter
+ - if user
+ = link_to user.name, [:admin, user]
+ .light.small
+ Joined #{time_ago_with_tooltip(user.created_at)}
- else
(removed)
%td
- = abuse_report.created_at.to_s(:short)
- %td
- = abuse_report.message
- %td
- - if user
- = link_to user.name, user
+ - if reporter
+ = link_to reporter.name, [:admin, reporter]
- else
(removed)
+ .light.small
+ = time_ago_with_tooltip(abuse_report.created_at)
+ %td
+ = markdown(abuse_report.message.squish!, pipeline: :single_line)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 40a5fe4628b..bc4a9cedb2c 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -6,10 +6,9 @@
%table.table
%thead
%tr
+ %th User
%th Reported by
- %th Reported at
%th Message
- %th User
%th Primary action
%th
= render @abuse_reports
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 58f5c621f4a..89b38a0dad0 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -149,12 +149,89 @@
.checkbox
= f.label :shared_runners_enabled do
= f.check_box :shared_runners_enabled
- Enable shared runners for a new projects
+ Enable shared runners for new projects
.form-group
= f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
+ %fieldset
+ %legend Metrics
+ %p
+ These settings require a restart to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :metrics_enabled do
+ = f.check_box :metrics_enabled
+ Enable InfluxDB Metrics
+ .form-group
+ = f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
+ .form-group
+ = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
+ .help-block
+ The UDP port to use for connecting to InfluxDB. InfluxDB requires that
+ your server configuration specifies a database to store data in when
+ sending messages to this port, without it metrics data will not be
+ saved.
+ .form-group
+ = f.label :metrics_username, 'InfluxDB username', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_username, class: 'form-control'
+ .form-group
+ = f.label :metrics_password, 'InfluxDB password', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_password, class: 'form-control'
+ .form-group
+ = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_pool_size, class: 'form-control'
+ .help-block
+ The amount of InfluxDB connections to open. Connections are opened
+ lazily. Users using multi-threaded application servers should ensure
+ enough connections are available (at minimum the amount of application
+ server threads).
+ .form-group
+ = f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_timeout, class: 'form-control'
+ .help-block
+ The amount of seconds after which an InfluxDB connection will time
+ out.
+ .form-group
+ = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_method_call_threshold, class: 'form-control'
+ .help-block
+ A method call is only tracked when it takes longer to complete than
+ the given amount of milliseconds.
+
+ %fieldset
+ %legend Spam and Anti-bot Protection
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :recaptcha_enabled do
+ = f.check_box :recaptcha_enabled
+ Enable reCAPTCHA
+ %span.help-block#recaptcha_help_block Helps preventing bots from creating accounts
+
+ .form-group
+ = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_site_key, class: 'form-control'
+ .help-block
+ Generate site and private keys here:
+ %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha
+ .form-group
+ = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_private_key, class: 'form-control'
+
.form-actions
= f.submit 'Save', class: 'btn btn-primary'
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 55da06a7fe9..ddd4e1481eb 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -7,18 +7,18 @@
%ul.center-top-menu
%li{class: ('active' if @scope.nil?)}
= link_to admin_builds_path do
+ All
+ %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to admin_builds_path(scope: :running) do
Running
- %span.badge.js-running-count= @all_builds.running_or_pending.count(:id)
+ %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id))
%li{class: ('active' if @scope == 'finished')}
= link_to admin_builds_path(scope: :finished) do
Finished
- %span.badge.js-running-count= @all_builds.finished.count(:id)
-
- %li{class: ('active' if @scope == 'all')}
- = link_to admin_builds_path(scope: :all) do
- All
- %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+ %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id))
.gray-content-block
#{(@scope || 'running').capitalize} builds
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 531247e9148..cc389c3ae08 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -6,35 +6,35 @@
%p
Forks
%span.light.pull-right
- = ForkedProjectLink.count
+ = number_with_delimiter(ForkedProjectLink.count)
%p
Issues
%span.light.pull-right
- = Issue.count
+ = number_with_delimiter(Issue.count)
%p
Merge Requests
%span.light.pull-right
- = MergeRequest.count
+ = number_with_delimiter(MergeRequest.count)
%p
Notes
%span.light.pull-right
- = Note.count
+ = number_with_delimiter(Note.count)
%p
Snippets
%span.light.pull-right
- = Snippet.count
+ = number_with_delimiter(Snippet.count)
%p
SSH Keys
%span.light.pull-right
- = Key.count
+ = number_with_delimiter(Key.count)
%p
Milestones
%span.light.pull-right
- = Milestone.count
+ = number_with_delimiter(Milestone.count)
%p
Active Users
%span.light.pull-right
- = User.active.count
+ = number_with_delimiter(User.active.count)
.col-md-4
%h4
Features
@@ -99,7 +99,7 @@
%h4 Projects
.data
= link_to admin_namespaces_projects_path do
- %h1= Project.count
+ %h1= number_with_delimiter(Project.count)
%hr
= link_to('New Project', new_project_path, class: "btn btn-new")
.col-sm-4
@@ -107,7 +107,7 @@
%h4 Users
.data
= link_to admin_users_path do
- %h1= User.count
+ %h1= number_with_delimiter(User.count)
%hr
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -115,7 +115,7 @@
%h4 Groups
.data
= link_to admin_groups_path do
- %h1= Group.count
+ %h1= number_with_delimiter(Group.count)
%hr
= link_to 'New Group', new_admin_group_path, class: "btn btn-new"
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 5ce7cdf2f8d..3940210e19b 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Groups"
%h3.page-title
- Groups (#{@groups.total_count})
+ Groups (#{number_with_delimiter(@groups.total_count)})
= link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right"
%p.light
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index bc08458312c..a92c9c152b9 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -8,27 +8,27 @@
%li{class: "#{'active' unless params[:filter]}"}
= link_to admin_users_path do
Active
- %small.pull-right= User.active.count
+ %small.pull-right= number_with_delimiter(User.active.count)
%li{class: "#{'active' if params[:filter] == "admins"}"}
= link_to admin_users_path(filter: "admins") do
Admins
- %small.pull-right= User.admins.count
+ %small.pull-right= number_with_delimiter(User.admins.count)
%li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
- %small.pull-right= User.with_two_factor.count
+ %small.pull-right= number_with_delimiter(User.with_two_factor.count)
%li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
- %small.pull-right= User.without_two_factor.count
+ %small.pull-right= number_with_delimiter(User.without_two_factor.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do
Blocked
- %small.pull-right= User.blocked.count
+ %small.pull-right= number_with_delimiter(User.blocked.count)
%li{class: "#{'active' if params[:filter] == "wop"}"}
= link_to admin_users_path(filter: "wop") do
Without projects
- %small.pull-right= User.without_projects.count
+ %small.pull-right= number_with_delimiter(User.without_projects.count)
%hr
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
@@ -42,7 +42,7 @@
%section.col-md-9
.panel.panel-default
.panel-heading
- Users (#{@users.total_count})
+ Users (#{number_with_delimiter(@users.total_count)})
.panel-head-actions
.dropdown.inline
%a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 9dc6aeffd59..cb93ff2465e 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -6,17 +6,21 @@
.login-heading
%h3 Create an account
.login-body
+ - user = params[:user].present? ? params[:user] : {}
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
.devise-errors
= devise_error_messages!
%div
- = f.text_field :name, class: "form-control top", placeholder: "Name", required: true
+ = f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true
%div
- = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
+ = f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true
%div
- = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
+ = f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true
.form-group.append-bottom-20#password-strength
- = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true
+ = f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true
+ %div
+ - if current_application_settings.recaptcha_enabled
+ = recaptcha_tags
%div
= f.submit "Sign up", class: "btn-create btn"
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 9aacc79d686..46432a92348 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -3,7 +3,7 @@
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
- = cache [event, "v2.1"] do
+ = cache [event, current_application_settings, "v2.1"] do
= image_tag avatar_icon(event.author_email, 46), class: "avatar s46", alt:''
- if event.created_project?
= render "events/event/created_project", event: event
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 8daac585960..7e3e2e28bc9 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -24,15 +24,6 @@
%hr
= link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- .form-group
- %hr
- = f.label :public, class: 'control-label' do
- Public
- .col-sm-10
- .checkbox
- = f.check_box :public
- %span.descr Make this group public (even if there is no any public project inside this group)
-
.form-actions
= f.submit 'Save group', class: "btn btn-save"
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index c2c7c581b3e..a607d860d7d 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -47,5 +47,5 @@
= render "projects", projects: @projects
- else
- %p
- This group does not have public projects
+ %p.center-top-menu.no-top
+ No projects to show
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 7e801b5332d..e8e331dd109 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -219,11 +219,3 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
-
-
-:javascript
- $('.js-more-help-button').click(function (e) {
- $(this).remove()l
- $('.hidden-shortcut').show();
- e.preventDefault();
- });
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 74174a72f5a..dd133ee8b56 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,11 +1,24 @@
-- page_title "GitLab"
-%head
+%head{prefix: "og: http://ogp.me/ns#"}
%meta{charset: "utf-8"}
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
- %meta{content: "GitLab Community Edition", name: "description"}
- %meta{name: 'referrer', content: 'origin-when-cross-origin'}
- %title= page_title
+ -# Open Graph - http://ogp.me/
+ %meta{property: 'og:type', content: "object"}
+ %meta{property: 'og:site_name', content: "GitLab"}
+ %meta{property: 'og:title', content: page_title}
+ %meta{property: 'og:description', content: page_description}
+ %meta{property: 'og:image', content: page_image}
+ %meta{property: 'og:url', content: request.base_url + request.fullpath}
+
+ -# Twitter Card - https://dev.twitter.com/cards/types/summary
+ %meta{property: 'twitter:card', content: "summary"}
+ %meta{property: 'twitter:title', content: page_title}
+ %meta{property: 'twitter:description', content: page_description}
+ %meta{property: 'twitter:image', content: page_image}
+ = page_card_meta_tags
+
+ %title= page_title('GitLab')
+ %meta{name: "description", content: page_description}
= favicon_link_tag 'favicon.ico'
@@ -18,6 +31,8 @@
= include_gon
+ - unless browser.safari?
+ %meta{name: 'referrer', content: 'origin-when-cross-origin'}
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'}
%meta{name: 'theme-color', content: '#474D57'}
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index c60ac5eefac..cffdb52cc23 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -29,13 +29,13 @@
= icon('cog fw')
%span
Runners
- %span.count= Ci::Runner.count(:all)
+ %span.count= number_with_delimiter(Ci::Runner.count(:all))
= nav_link path: 'builds#index' do
= link_to admin_builds_path do
= icon('link fw')
%span
Builds
- %span.count= Ci::Build.count(:all)
+ %span.count= number_with_delimiter(Ci::Build.count(:all))
= nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs' do
= icon('file-text fw')
@@ -80,7 +80,7 @@
= icon('exclamation-circle fw')
%span
Abuse Reports
- %span.count= AbuseReport.count(:all)
+ %span.count= number_with_delimiter(AbuseReport.count(:all))
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
= link_to admin_application_settings_path, title: 'Settings' do
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index da698831300..106abd24a56 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -24,13 +24,13 @@
= icon('exclamation-circle fw')
%span
Issues
- %span.count= current_user.assigned_issues.opened.count
+ %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do
= icon('tasks fw')
%span
Merge Requests
- %span.count= current_user.assigned_merge_requests.opened.count
+ %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do
= link_to dashboard_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 68da8d5de2a..e5e2a59eaed 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -25,14 +25,14 @@
%span
Issues
- if current_user
- %span.count= Issue.opened.of_group(@group).count
+ %span.count= number_with_delimiter(Issue.opened.of_group(@group).count)
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
= icon('tasks fw')
%span
Merge Requests
- if current_user
- %span.count= MergeRequest.opened.of_group(@group).count
+ %span.count= number_with_delimiter(MergeRequest.opened.of_group(@group).count)
= nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do
= icon('users fw')
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 64b30783c05..f3ded04419b 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -27,7 +27,7 @@
= icon('envelope-o fw')
%span
Emails
- %span.count= current_user.emails.count + 1
+ %span.count= number_with_delimiter(current_user.emails.count + 1)
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
@@ -45,7 +45,7 @@
= icon('key fw')
%span
SSH Keys
- %span.count= current_user.keys.count
+ %span.count= number_with_delimiter(current_user.keys.count)
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
-# TODO (rspeicher): Better icon?
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index c0d62028639..d3eaf0f3209 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -44,7 +44,7 @@
= icon('cubes fw')
%span
Builds
- %span.count.builds_counter= @project.builds.running_or_pending.count(:all)
+ %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all))
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
@@ -67,7 +67,7 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.count.issue_counter= @project.issues.opened.count
+ %span.count.issue_counter= number_with_delimiter(@project.issues.opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
@@ -75,7 +75,7 @@
= icon('tasks fw')
%span
Merge Requests
- %span.count.merge_counter= @project.merge_requests.opened.count
+ %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
- if project_nav_tab? :settings
= nav_link(controller: [:project_members, :teams]) do
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index e92115b9b98..0f61e623396 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -18,13 +18,26 @@
= visibility_level_label(@project.visibility_level)
.cover-controls
- - if can?(current_user, :admin_project, @project)
- = link_to edit_project_path(@project), class: 'btn btn-gray' do
- = icon('pencil')
- if current_user
- &nbsp;
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), class: 'btn btn-gray' do
= icon('rss')
+ - access = user_max_access_in_project(current_user.id, @project)
+ - can_edit = can?(current_user, :admin_project, @project)
+ - if access || can_edit
+ %span.dropdown.project-settings-dropdown
+ %a.dropdown-new.btn.btn-gray#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('angle-down')
+ %ul.dropdown-menu.dropdown-menu-right
+ - if can_edit
+ %li
+ = link_to edit_project_path(@project) do
+ Edit Project
+ - if access
+ %li
+ = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
+ data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
+ Leave Project
.project-repo-buttons
.split-one.count-buttons
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 1a26908ab11..2fa5ad80fda 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -11,6 +11,12 @@
%ul.center-top-menu
%li{class: ('active' if @scope.nil?)}
= link_to project_builds_path(@project) do
+ All
+ %span.badge.js-totalbuilds-count
+ = number_with_delimiter(@all_builds.count(:id))
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to project_builds_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@all_builds.running_or_pending.count(:id))
@@ -21,12 +27,6 @@
%span.badge.js-running-count
= number_with_delimiter(@all_builds.finished.count(:id))
- %li{class: ('active' if @scope == 'all')}
- = link_to project_builds_path(@project, scope: :all) do
- All
- %span.badge.js-totalbuilds-count
- = number_with_delimiter(@all_builds.count(:id))
-
.gray-content-block
#{(@scope || 'running').capitalize} builds from this project
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 1f639fecc30..f9ab78e7874 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -8,9 +8,10 @@
= link_to url_for_new_issue(@project, only_path: true) do
= icon('exclamation-circle fw')
New issue
- - if can?(current_user, :create_merge_request, @project)
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - if merge_project
%li
- = link_to new_namespace_project_merge_request_path(@project.namespace, @project) do
+ = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do
= icon('tasks fw')
New merge request
- if can?(current_user, :create_snippet, @project)
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 28b82dd31f3..012825f0fdb 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -5,7 +5,7 @@
- note_count = notes.user.count
- ci_commit = project.ci_commit(commit.sha)
-- cache_key = [project.path_with_namespace, commit.id, note_count]
+- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
- cache_key.push(ci_commit.status) if ci_commit
= cache(cache_key) do
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index ca5b1a8386d..e0e89b764d5 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -7,7 +7,7 @@
- if @issues.present?
.issuable-filter-count
%span.pull-right
- = @issues.total_count
+ = number_with_delimiter(@issues.total_count)
issues for this filter
= paginate @issues, theme: "gitlab"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index b6efa05a1ae..f931a0d3b92 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,13 +1,13 @@
-- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_description @issue.description
+- page_card_attributes @issue.card_attributes
+
= render "header_title"
.issue
.detail-page-header
- .status-box{ class: status_box_class(@issue) }
- - if @issue.closed?
- Closed
- - else
- Open
+ .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed
+ .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open
%span.identifier
Issue ##{@issue.iid}
%span.creator
@@ -27,10 +27,8 @@
= icon('plus')
New Issue
- if can?(current_user, :update_issue, @issue)
- - if @issue.closed?
- = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen'
- - else
- = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close Issue'
+ = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
+ = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index 0af970e4b92..29d09d0a652 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -7,7 +7,7 @@
- if @merge_requests.present?
.issuable-filter-count
%span.pull-right
- = @merge_requests.total_count
+ = number_with_delimiter(@merge_requests.total_count)
merge requests for this filter
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index e9ffbd06be2..ba7c2c01e93 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,4 +1,7 @@
-- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_description @merge_request.description
+- page_card_attributes @merge_request.card_attributes
+
= render "header_title"
- if params[:view] == 'parallel'
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 086298e5af1..8d5d0394a82 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -6,9 +6,10 @@
.controls
= render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
- - if can? current_user, :create_merge_request, @project
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - if merge_project
.pull-left.hidden-xs
- = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new", title: "New Merge Request" do
+ = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
%i.fa.fa-plus
New Merge Request
= render 'shared/issuable/filter', type: :merge_requests
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 7e73ae274e9..1670ea8741a 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,4 +1,6 @@
-- page_title @milestone.title, "Milestones"
+- page_title @milestone.title, "Milestones"
+- page_description @milestone.description
+
= render "header_title"
.detail-page-header
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 7466a098e24..ffbe445b447 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -68,15 +68,4 @@
= render 'projects/last_commit', commit: @repository.commit, project: @project
%div{class: "project-show-#{default_project_view}"}
- = render default_project_view
-
-- if current_user
- - access = user_max_access_in_project(current_user.id, @project)
- - if access
- .prepend-top-20.project-footer
- .gray-content-block.footer-block.center
- You have #{access} access to this project.
- - if @project.project_member_by_id(current_user)
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
- data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do
- Leave this project
+ = render default_project_view \ No newline at end of file
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index da49c48acd3..3d279ec228c 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -5,13 +5,13 @@
<g id="Fill-1-+-Group-24">
<g id="Group-24">
<g id="Group">
- <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329" class="tanuki-shape"></path>
+ <path id="tanuki-right-ear" d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" fill="#E24329" class="tanuki-shape"></path>
+ <path id="tanuki-right-cheek" d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" fill="#FCA326" class="tanuki-shape"></path>
+ <path id="tanuki-right-eye" d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" fill="#FC6D26" class="tanuki-shape"></path>
+ <path id="tanuki-nose" d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" fill="#E24329" class="tanuki-shape"></path>
+ <path id="tanuki-left-eye" d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" fill="#FC6D26" class="tanuki-shape"></path>
+ <path id="tanuki-left-cheek" d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" fill="#FCA326" class="tanuki-shape"></path>
+ <path id="tanuki-left-ear" d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" fill="#E24329" class="tanuki-shape"></path>
</g>
</g>
</g>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index ac6c248ccf1..be06738eac9 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -30,13 +30,13 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- = users_select_tag(:assignee_id, selected: params[:assignee_id],
- placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
-
- .filter-item.inline
= users_select_tag(:author_id, selected: params[:author_id],
placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ .filter-item.inline
+ = users_select_tag(:assignee_id, selected: params[:assignee_id],
+ placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
+
.filter-item.inline.milestone-filter
= select_tag('milestone_title', projects_milestones_options,
class: 'select2 trigger-submit', include_blank: true,
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index c36995b94d7..86249851a82 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -5,7 +5,7 @@
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" unless project.description.present?
%li.project-row{ class: css_class }
- = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2.2'] do
+ = cache [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] do
= link_to project_path(project), class: dom_class(project) do
- if avatar
.dash-project-avatar
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index b7a7eb4e6f7..0bca8177e14 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,5 +1,6 @@
-- page_title @user.name
-- header_title @user.name, user_path(@user)
+- page_title @user.name
+- page_description @user.bio
+- header_title @user.name, user_path(@user)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index e16187bb42f..ce0a0113403 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,5 +1,5 @@
.awards.votes-block
- - votable.notes.awards.grouped_awards.each do |emoji, notes|
+ - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
.award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
= emoji_icon(emoji)
.counter
diff --git a/config/database.yml.env b/config/database.yml.env
new file mode 100644
index 00000000000..b2ff23cb5ab
--- /dev/null
+++ b/config/database.yml.env
@@ -0,0 +1,9 @@
+<%= ENV['RAILS_ENV'] %>:
+ adapter: <%= ENV['GITLAB_DATABASE_ADAPTER'] || 'postgresql' %>
+ encoding: <%= ENV['GITLAB_DATABASE_ENCODING'] || 'unicode' %>
+ database: <%= ENV['GITLAB_DATABASE_DATABASE'] || "gitlab_#{ENV['RAILS_ENV']}" %>
+ pool: <%= ENV['GITLAB_DATABASE_POOL'] || '10' %>
+ username: <%= ENV['GITLAB_DATABASE_USERNAME'] || 'root' %>
+ password: <%= ENV['GITLAB_DATABASE_PASSWORD'] || '' %>
+ host: <%= ENV['GITLAB_DATABASE_HOST'] || 'localhost' %>
+ port: <%= ENV['GITLAB_DATABASE_PORT'] || '5432' %>
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index db68b5512b8..2d9f730c183 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -4,8 +4,8 @@
#
########################### NOTE #####################################
# This file should not receive new settings. All configuration options #
-# that do not require an application restart are being moved to #
-# ApplicationSetting model! #
+# * are being moved to ApplicationSetting model! #
+# If a setting requires an application restart say so in that screen. #
# If you change this file in a Merge Request, please also create #
# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests #
########################################################################
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 816cb0c02a9..4fbd84ee890 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -131,6 +131,7 @@ Settings.omniauth.cas3['session_duration'] ||= 8.hours
Settings.omniauth['session_tickets'] ||= Settingslogic.new({})
Settings.omniauth.session_tickets['cas3'] = 'ticket'
+
Settings['shared'] ||= Settingslogic.new({})
Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
@@ -144,16 +145,16 @@ Settings.gitlab['default_projects_limit'] ||= 10
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
-Settings.gitlab['host'] ||= 'localhost'
+Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
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['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil?
-Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}"
-Settings.gitlab['email_display_name'] ||= "GitLab"
-Settings.gitlab['email_reply_to'] ||= "noreply@#{Settings.gitlab.host}"
+Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}"
+Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab'
+Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}"
Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url)
Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url)
Settings.gitlab['user'] ||= 'git'
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
new file mode 100644
index 00000000000..52ace27b7ae
--- /dev/null
+++ b/config/initializers/metrics.rb
@@ -0,0 +1,63 @@
+if Gitlab::Metrics.enabled?
+ require 'influxdb'
+ require 'connection_pool'
+ require 'method_source'
+
+ # These are manually require'd so the classes are registered properly with
+ # ActiveSupport.
+ require 'gitlab/metrics/subscribers/action_view'
+ require 'gitlab/metrics/subscribers/active_record'
+
+ Gitlab::Application.configure do |config|
+ config.middleware.use(Gitlab::Metrics::RackMiddleware)
+ end
+
+ Sidekiq.configure_server do |config|
+ config.server_middleware do |chain|
+ chain.add Gitlab::Metrics::SidekiqMiddleware
+ end
+ end
+
+ # This instruments all methods residing in app/models that (appear to) use any
+ # of the ActiveRecord methods. This has to take place _after_ initializing as
+ # for some unknown reason calling eager_load! earlier breaks Devise.
+ Gitlab::Application.config.after_initialize do
+ Rails.application.eager_load!
+
+ models = Rails.root.join('app', 'models').to_s
+
+ regex = Regexp.union(
+ ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
+ )
+
+ Gitlab::Metrics::Instrumentation.
+ instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
+ # Instrumenting the ApplicationSetting class can lead to an infinite
+ # loop. Since the data is cached any way we don't really need to
+ # instrument it.
+ if klass == ApplicationSetting
+ false
+ else
+ loc = method.source_location
+
+ loc && loc[0].start_with?(models) && method.source =~ regex
+ end
+ end
+ end
+
+ Gitlab::Metrics::Instrumentation.configure do |config|
+ config.instrument_instance_methods(Gitlab::Shell)
+
+ config.instrument_methods(Gitlab::Git)
+
+ Gitlab::Git.constants.each do |name|
+ const = Gitlab::Git.const_get(name)
+
+ config.instrument_methods(const) if const.is_a?(Module)
+ end
+ end
+
+ GC::Profiler.enable
+
+ Gitlab::Metrics::Sampler.new.start
+end
diff --git a/config/schedule.rb b/config/schedule.rb
deleted file mode 100644
index 8122f7cc69c..00000000000
--- a/config/schedule.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# Use this file to easily define all of your cron jobs.
-#
-# If you make changes to this file, please create also an issue on
-# https://gitlab.com/gitlab-org/omnibus-gitlab/issues . This is necessary
-# because the omnibus packages manage cron jobs using Chef instead of Whenever.
-every 1.hour do
- rake "ci:schedule_builds"
-end
diff --git a/db/migrate/20151228111122_remove_public_from_namespace.rb b/db/migrate/20151228111122_remove_public_from_namespace.rb
new file mode 100644
index 00000000000..f4c848bbf47
--- /dev/null
+++ b/db/migrate/20151228111122_remove_public_from_namespace.rb
@@ -0,0 +1,6 @@
+# Migration type: online
+class RemovePublicFromNamespace < ActiveRecord::Migration
+ def change
+ remove_column :namespaces, :public, :boolean
+ end
+end
diff --git a/db/migrate/20151228150906_influxdb_settings.rb b/db/migrate/20151228150906_influxdb_settings.rb
new file mode 100644
index 00000000000..3012bd52cfd
--- /dev/null
+++ b/db/migrate/20151228150906_influxdb_settings.rb
@@ -0,0 +1,18 @@
+class InfluxdbSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :metrics_enabled, :boolean, default: false
+
+ add_column :application_settings, :metrics_host, :string,
+ default: 'localhost'
+
+ add_column :application_settings, :metrics_database, :string,
+ default: 'gitlab'
+
+ add_column :application_settings, :metrics_username, :string
+ add_column :application_settings, :metrics_password, :string
+ add_column :application_settings, :metrics_pool_size, :integer, default: 16
+ add_column :application_settings, :metrics_timeout, :integer, default: 10
+ add_column :application_settings, :metrics_method_call_threshold,
+ :integer, default: 10
+ end
+end
diff --git a/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb
new file mode 100644
index 00000000000..259fd0248d2
--- /dev/null
+++ b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb
@@ -0,0 +1,9 @@
+class AddRecaptchaToApplicationSettings < ActiveRecord::Migration
+ def change
+ change_table :application_settings do |t|
+ t.boolean :recaptcha_enabled, default: false
+ t.string :recaptcha_site_key
+ t.string :recaptcha_private_key
+ end
+ end
+end
diff --git a/db/migrate/20151229102248_influxdb_udp_port_setting.rb b/db/migrate/20151229102248_influxdb_udp_port_setting.rb
new file mode 100644
index 00000000000..ae0499f936d
--- /dev/null
+++ b/db/migrate/20151229102248_influxdb_udp_port_setting.rb
@@ -0,0 +1,5 @@
+class InfluxdbUdpPortSetting < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :metrics_port, :integer, default: 8089
+ end
+end
diff --git a/db/migrate/20151229112614_influxdb_remote_database_setting.rb b/db/migrate/20151229112614_influxdb_remote_database_setting.rb
new file mode 100644
index 00000000000..f0e1ee1e7a7
--- /dev/null
+++ b/db/migrate/20151229112614_influxdb_remote_database_setting.rb
@@ -0,0 +1,5 @@
+class InfluxdbRemoteDatabaseSetting < ActiveRecord::Migration
+ def change
+ remove_column :application_settings, :metrics_database
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 49fa258660d..48e6983684a 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: 20151224123230) do
+ActiveRecord::Schema.define(version: 20151229112614) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -32,33 +32,44 @@ ActiveRecord::Schema.define(version: 20151224123230) do
t.text "sign_in_text"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "home_page_url"
- t.integer "default_branch_protection", default: 2
- t.boolean "twitter_sharing_enabled", default: true
+ t.string "home_page_url", limit: 255
+ t.integer "default_branch_protection", default: 2
+ t.boolean "twitter_sharing_enabled", default: true
t.text "restricted_visibility_levels"
- t.boolean "version_check_enabled", default: true
- t.integer "max_attachment_size", default: 10, null: false
+ t.boolean "version_check_enabled", default: true
+ t.integer "max_attachment_size", default: 10, null: false
t.integer "default_project_visibility"
t.integer "default_snippet_visibility"
t.text "restricted_signup_domains"
- t.boolean "user_oauth_applications", default: true
- t.string "after_sign_out_path"
- t.integer "session_expire_delay", default: 10080, null: false
+ t.boolean "user_oauth_applications", default: true
+ t.string "after_sign_out_path", limit: 255
+ t.integer "session_expire_delay", default: 10080, null: false
t.text "import_sources"
t.text "help_page_text"
- t.string "admin_notification_email"
- t.boolean "shared_runners_enabled", default: true, null: false
- t.integer "max_artifacts_size", default: 100, null: false
+ t.string "admin_notification_email", limit: 255
+ t.boolean "shared_runners_enabled", default: true, null: false
+ t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token"
t.boolean "require_two_factor_authentication", default: false
t.integer "two_factor_grace_period", default: 48
+ t.boolean "metrics_enabled", default: false
+ t.string "metrics_host", default: "localhost"
+ t.string "metrics_username"
+ t.string "metrics_password"
+ t.integer "metrics_pool_size", default: 16
+ t.integer "metrics_timeout", default: 10
+ t.integer "metrics_method_call_threshold", default: 10
+ t.boolean "recaptcha_enabled", default: false
+ t.string "recaptcha_site_key"
+ t.string "recaptcha_private_key"
+ t.integer "metrics_port", default: 8089
end
create_table "audit_events", force: :cascade do |t|
- t.integer "author_id", null: false
- t.string "type", null: false
- t.integer "entity_id", null: false
- t.string "entity_type", null: false
+ t.integer "author_id", null: false
+ t.string "type", limit: 255, null: false
+ t.integer "entity_id", null: false
+ t.string "entity_type", limit: 255, null: false
t.text "details"
t.datetime "created_at"
t.datetime "updated_at"
@@ -69,14 +80,14 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
create_table "broadcast_messages", force: :cascade do |t|
- t.text "message", null: false
+ t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
t.integer "alert_type"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "color"
- t.string "font"
+ t.string "color", limit: 255
+ t.string "font", limit: 255
end
create_table "ci_application_settings", force: :cascade do |t|
@@ -88,7 +99,7 @@ ActiveRecord::Schema.define(version: 20151224123230) do
create_table "ci_builds", force: :cascade do |t|
t.integer "project_id"
- t.string "status"
+ t.string "status", limit: 255
t.datetime "finished_at"
t.text "trace"
t.datetime "created_at"
@@ -99,19 +110,19 @@ ActiveRecord::Schema.define(version: 20151224123230) do
t.integer "commit_id"
t.text "commands"
t.integer "job_id"
- t.string "name"
- t.boolean "deploy", default: false
+ t.string "name", limit: 255
+ t.boolean "deploy", default: false
t.text "options"
- t.boolean "allow_failure", default: false, null: false
- t.string "stage"
+ t.boolean "allow_failure", default: false, null: false
+ t.string "stage", limit: 255
t.integer "trigger_request_id"
t.integer "stage_idx"
t.boolean "tag"
- t.string "ref"
+ t.string "ref", limit: 255
t.integer "user_id"
- t.string "type"
- t.string "target_url"
- t.string "description"
+ t.string "type", limit: 255
+ t.string "target_url", limit: 255
+ t.string "description", limit: 255
t.text "artifacts_file"
t.integer "gl_project_id"
end
@@ -130,13 +141,13 @@ ActiveRecord::Schema.define(version: 20151224123230) do
create_table "ci_commits", force: :cascade do |t|
t.integer "project_id"
- t.string "ref"
- t.string "sha"
- t.string "before_sha"
+ t.string "ref", limit: 255
+ t.string "sha", limit: 255
+ t.string "before_sha", limit: 255
t.text "push_data"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "tag", default: false
+ t.boolean "tag", default: false
t.text "yaml_errors"
t.datetime "committed_at"
t.integer "gl_project_id"
@@ -163,16 +174,16 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
create_table "ci_jobs", force: :cascade do |t|
- t.integer "project_id", null: false
+ t.integer "project_id", null: false
t.text "commands"
- t.boolean "active", default: true, null: false
+ t.boolean "active", default: true, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "name"
- t.boolean "build_branches", default: true, null: false
- t.boolean "build_tags", default: false, null: false
- t.string "job_type", default: "parallel"
- t.string "refs"
+ t.string "name", limit: 255
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", limit: 255, default: "parallel"
+ t.string "refs", limit: 255
t.datetime "deleted_at"
end
@@ -180,25 +191,25 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
create_table "ci_projects", force: :cascade do |t|
- t.string "name"
- t.integer "timeout", default: 3600, null: false
+ t.string "name", limit: 255
+ t.integer "timeout", default: 3600, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "token"
- t.string "default_ref"
- t.string "path"
- t.boolean "always_build", default: false, null: false
+ t.string "token", limit: 255
+ t.string "default_ref", limit: 255
+ t.string "path", limit: 255
+ t.boolean "always_build", default: false, null: false
t.integer "polling_interval"
- t.boolean "public", default: false, null: false
- t.string "ssh_url_to_repo"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo", limit: 255
t.integer "gitlab_id"
- t.boolean "allow_git_fetch", default: true, null: false
- t.string "email_recipients", default: "", null: false
- t.boolean "email_add_pusher", default: true, null: false
- t.boolean "email_only_broken_builds", default: true, null: false
- t.string "skip_refs"
- t.string "coverage_regex"
- t.boolean "shared_runners_enabled", default: false
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", limit: 255, default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs", limit: 255
+ t.string "coverage_regex", limit: 255
+ t.boolean "shared_runners_enabled", default: false
t.text "generated_yaml_config"
end
@@ -217,34 +228,34 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
create_table "ci_runners", force: :cascade do |t|
- t.string "token"
+ t.string "token", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
- t.string "description"
+ t.string "description", limit: 255
t.datetime "contacted_at"
- t.boolean "active", default: true, null: false
- t.boolean "is_shared", default: false
- t.string "name"
- t.string "version"
- t.string "revision"
- t.string "platform"
- t.string "architecture"
+ t.boolean "active", default: true, null: false
+ t.boolean "is_shared", default: false
+ t.string "name", limit: 255
+ t.string "version", limit: 255
+ t.string "revision", limit: 255
+ t.string "platform", limit: 255
+ t.string "architecture", limit: 255
end
create_table "ci_services", force: :cascade do |t|
- t.string "type"
- t.string "title"
- t.integer "project_id", null: false
+ t.string "type", limit: 255
+ t.string "title", limit: 255
+ t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "active", default: false, null: false
+ t.boolean "active", default: false, null: false
t.text "properties"
end
add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
create_table "ci_sessions", force: :cascade do |t|
- t.string "session_id", null: false
+ t.string "session_id", limit: 255, null: false
t.text "data"
t.datetime "created_at"
t.datetime "updated_at"
@@ -256,9 +267,9 @@ ActiveRecord::Schema.define(version: 20151224123230) do
create_table "ci_taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
- t.string "taggable_type"
+ t.string "taggable_type", limit: 255
t.integer "tagger_id"
- t.string "tagger_type"
+ t.string "tagger_type", limit: 255
t.string "context", limit: 128
t.datetime "created_at"
end
@@ -267,8 +278,8 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
create_table "ci_tags", force: :cascade do |t|
- t.string "name"
- t.integer "taggings_count", default: 0
+ t.string "name", limit: 255
+ t.integer "taggings_count", default: 0
end
add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
@@ -282,7 +293,7 @@ ActiveRecord::Schema.define(version: 20151224123230) do
end
create_table "ci_triggers", force: :cascade do |t|
- t.string "token"
+ t.string "token", limit: 255
t.integer "project_id"
t.datetime "deleted_at"
t.datetime "created_at"
@@ -295,19 +306,19 @@ ActiveRecord::Schema.define(version: 20151224123230) do
create_table "ci_variables", force: :cascade do |t|
t.integer "project_id"
- t.string "key"
+ t.string "key", limit: 255
t.text "value"
t.text "encrypted_value"
- t.string "encrypted_value_salt"
- t.string "encrypted_value_iv"
+ t.string "encrypted_value_salt", limit: 255
+ t.string "encrypted_value_iv", limit: 255
t.integer "gl_project_id"
end
add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
create_table "ci_web_hooks", force: :cascade do |t|
- t.string "url", null: false
- t.integer "project_id", null: false
+ t.string "url", limit: 255, null: false
+ t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -322,8 +333,8 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
create_table "emails", force: :cascade do |t|
- t.integer "user_id", null: false
- t.string "email", null: false
+ t.integer "user_id", null: false
+ t.string "email", limit: 255, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -332,9 +343,9 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
create_table "events", force: :cascade do |t|
- t.string "target_type"
+ t.string "target_type", limit: 255
t.integer "target_id"
- t.string "title"
+ t.string "title", limit: 255
t.text "data"
t.integer "project_id"
t.datetime "created_at"
@@ -360,8 +371,8 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
create_table "identities", force: :cascade do |t|
- t.string "extern_uid"
- t.string "provider"
+ t.string "extern_uid", limit: 255
+ t.string "provider", limit: 255
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
@@ -371,17 +382,17 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
create_table "issues", force: :cascade do |t|
- t.string "title"
+ t.string "title", limit: 255
t.integer "assignee_id"
t.integer "author_id"
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "position", default: 0
- t.string "branch_name"
+ t.integer "position", default: 0
+ t.string "branch_name", limit: 255
t.text "description"
t.integer "milestone_id"
- t.string "state"
+ t.string "state", limit: 255
t.integer "iid"
t.integer "updated_by_id"
end
@@ -401,10 +412,10 @@ ActiveRecord::Schema.define(version: 20151224123230) do
t.datetime "created_at"
t.datetime "updated_at"
t.text "key"
- t.string "title"
- t.string "type"
- t.string "fingerprint"
- t.boolean "public", default: false, null: false
+ t.string "title", limit: 255
+ t.string "type", limit: 255
+ t.string "fingerprint", limit: 255
+ t.boolean "public", default: false, null: false
end
add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
@@ -413,7 +424,7 @@ ActiveRecord::Schema.define(version: 20151224123230) do
create_table "label_links", force: :cascade do |t|
t.integer "label_id"
t.integer "target_id"
- t.string "target_type"
+ t.string "target_type", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -422,22 +433,22 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
create_table "labels", force: :cascade do |t|
- t.string "title"
- t.string "color"
+ t.string "title", limit: 255
+ t.string "color", limit: 255
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "template", default: false
+ t.boolean "template", default: false
end
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
- t.string "oid", null: false
- t.integer "size", null: false
+ t.string "oid", limit: 255, null: false
+ t.integer "size", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "file"
+ t.string "file", limit: 255
end
add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
@@ -452,17 +463,17 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
create_table "members", force: :cascade do |t|
- t.integer "access_level", null: false
- t.integer "source_id", null: false
- t.string "source_type", null: false
+ t.integer "access_level", null: false
+ t.integer "source_id", null: false
+ t.string "source_type", limit: 255, null: false
t.integer "user_id"
- t.integer "notification_level", null: false
- t.string "type"
+ t.integer "notification_level", null: false
+ t.string "type", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
t.integer "created_by_id"
- t.string "invite_email"
- t.string "invite_token"
+ t.string "invite_email", limit: 255
+ t.string "invite_token", limit: 255
t.datetime "invite_accepted_at"
end
@@ -474,10 +485,10 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
create_table "merge_request_diffs", force: :cascade do |t|
- t.string "state"
+ t.string "state", limit: 255
t.text "st_commits"
t.text "st_diffs"
- t.integer "merge_request_id", null: false
+ t.integer "merge_request_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -485,26 +496,26 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
create_table "merge_requests", force: :cascade do |t|
- t.string "target_branch", null: false
- t.string "source_branch", null: false
- t.integer "source_project_id", null: false
+ t.string "target_branch", limit: 255, null: false
+ t.string "source_branch", limit: 255, null: false
+ t.integer "source_project_id", null: false
t.integer "author_id"
t.integer "assignee_id"
- t.string "title"
+ t.string "title", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
t.integer "milestone_id"
- t.string "state"
- t.string "merge_status"
- t.integer "target_project_id", null: false
+ t.string "state", limit: 255
+ t.string "merge_status", limit: 255
+ t.integer "target_project_id", null: false
t.integer "iid"
t.text "description"
- t.integer "position", default: 0
+ t.integer "position", default: 0
t.datetime "locked_at"
t.integer "updated_by_id"
- t.string "merge_error"
+ t.string "merge_error", limit: 255
t.text "merge_params"
- t.boolean "merge_when_build_succeeds", default: false, null: false
+ t.boolean "merge_when_build_succeeds", default: false, null: false
t.integer "merge_user_id"
end
@@ -520,13 +531,13 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
create_table "milestones", force: :cascade do |t|
- t.string "title", null: false
- t.integer "project_id", null: false
+ t.string "title", limit: 255, null: false
+ t.integer "project_id", null: false
t.text "description"
t.date "due_date"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "state"
+ t.string "state", limit: 255
t.integer "iid"
end
@@ -536,39 +547,37 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
create_table "namespaces", force: :cascade do |t|
- t.string "name", null: false
- t.string "path", null: false
+ t.string "name", limit: 255, null: false
+ t.string "path", limit: 255, null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type"
- t.string "description", default: "", null: false
- t.string "avatar"
- t.boolean "public", default: false
+ t.string "type", limit: 255
+ t.string "description", limit: 255, default: "", null: false
+ t.string "avatar", limit: 255
end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
- add_index "namespaces", ["public"], name: "index_namespaces_on_public", using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t|
t.text "note"
- t.string "noteable_type"
+ t.string "noteable_type", limit: 255
t.integer "author_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "project_id"
- t.string "attachment"
- t.string "line_code"
- t.string "commit_id"
+ t.string "attachment", limit: 255
+ t.string "line_code", limit: 255
+ t.string "commit_id", limit: 255
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
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
@@ -584,14 +593,14 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
create_table "oauth_access_grants", force: :cascade do |t|
- t.integer "resource_owner_id", null: false
- t.integer "application_id", null: false
- t.string "token", null: false
- t.integer "expires_in", null: false
- t.text "redirect_uri", null: false
- t.datetime "created_at", null: false
+ t.integer "resource_owner_id", null: false
+ t.integer "application_id", null: false
+ t.string "token", limit: 255, null: false
+ t.integer "expires_in", null: false
+ t.text "redirect_uri", null: false
+ t.datetime "created_at", null: false
t.datetime "revoked_at"
- t.string "scopes"
+ t.string "scopes", limit: 255
end
add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
@@ -599,12 +608,12 @@ ActiveRecord::Schema.define(version: 20151224123230) do
create_table "oauth_access_tokens", force: :cascade do |t|
t.integer "resource_owner_id"
t.integer "application_id"
- t.string "token", null: false
- t.string "refresh_token"
+ t.string "token", limit: 255, null: false
+ t.string "refresh_token", limit: 255
t.integer "expires_in"
t.datetime "revoked_at"
- t.datetime "created_at", null: false
- t.string "scopes"
+ t.datetime "created_at", null: false
+ t.string "scopes", limit: 255
end
add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
@@ -612,15 +621,15 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
create_table "oauth_applications", force: :cascade do |t|
- t.string "name", null: false
- t.string "uid", null: false
- t.string "secret", null: false
- t.text "redirect_uri", null: false
- t.string "scopes", default: "", null: false
+ t.string "name", limit: 255, null: false
+ t.string "uid", limit: 255, null: false
+ t.string "secret", limit: 255, null: false
+ t.text "redirect_uri", null: false
+ t.string "scopes", limit: 255, default: "", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "owner_id"
- t.string "owner_type"
+ t.string "owner_type", limit: 255
end
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
@@ -632,39 +641,39 @@ ActiveRecord::Schema.define(version: 20151224123230) do
end
create_table "projects", force: :cascade do |t|
- t.string "name"
- t.string "path"
+ t.string "name", limit: 255
+ t.string "path", limit: 255
t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "wall_enabled", default: true, null: false
- t.boolean "merge_requests_enabled", default: true, null: false
- t.boolean "wiki_enabled", default: true, null: false
+ t.boolean "issues_enabled", default: true, null: false
+ t.boolean "wall_enabled", default: true, null: false
+ t.boolean "merge_requests_enabled", default: true, null: false
+ t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id"
- t.string "issues_tracker", default: "gitlab", null: false
- t.string "issues_tracker_id"
- t.boolean "snippets_enabled", default: true, null: false
+ t.string "issues_tracker", limit: 255, default: "gitlab", null: false
+ t.string "issues_tracker_id", limit: 255
+ t.boolean "snippets_enabled", default: true, null: false
t.datetime "last_activity_at"
- t.string "import_url"
- t.integer "visibility_level", default: 0, null: false
- t.boolean "archived", default: false, null: false
- t.string "avatar"
- t.string "import_status"
- t.float "repository_size", default: 0.0
- t.integer "star_count", default: 0, null: false
- t.string "import_type"
- t.string "import_source"
- t.integer "commit_count", default: 0
+ t.string "import_url", limit: 255
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
+ t.string "avatar", limit: 255
+ t.string "import_status", limit: 255
+ t.float "repository_size", default: 0.0
+ t.integer "star_count", default: 0, null: false
+ t.string "import_type", limit: 255
+ t.string "import_source", limit: 255
+ t.integer "commit_count", default: 0
t.text "import_error"
t.integer "ci_id"
- t.boolean "builds_enabled", default: true, null: false
- t.boolean "shared_runners_enabled", default: true, null: false
+ t.boolean "builds_enabled", default: true, null: false
+ t.boolean "shared_runners_enabled", default: true, null: false
t.string "runners_token"
t.string "build_coverage_regex"
- t.boolean "build_allow_git_fetch", default: true, null: false
- t.integer "build_timeout", default: 3600, null: false
+ t.boolean "build_allow_git_fetch", default: true, null: false
+ t.integer "build_timeout", default: 3600, null: false
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
@@ -680,17 +689,17 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
create_table "protected_branches", force: :cascade do |t|
- t.integer "project_id", null: false
- t.string "name", null: false
+ t.integer "project_id", null: false
+ t.string "name", limit: 255, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "developers_can_push", default: false, null: false
+ t.boolean "developers_can_push", default: false, null: false
end
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
create_table "releases", force: :cascade do |t|
- t.string "tag"
+ t.string "tag", limit: 255
t.text "description"
t.integer "project_id"
t.datetime "created_at"
@@ -703,30 +712,30 @@ ActiveRecord::Schema.define(version: 20151224123230) do
create_table "sent_notifications", force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
- t.string "noteable_type"
+ t.string "noteable_type", limit: 255
t.integer "recipient_id"
- t.string "commit_id"
- t.string "reply_key", null: false
- t.string "line_code"
+ t.string "commit_id", limit: 255
+ t.string "reply_key", limit: 255, null: false
+ t.string "line_code", limit: 255
end
add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
create_table "services", force: :cascade do |t|
- t.string "type"
- t.string "title"
+ t.string "type", limit: 255
+ t.string "title", limit: 255
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "active", default: false, null: false
+ t.boolean "active", default: false, null: false
t.text "properties"
- t.boolean "template", default: false
- t.boolean "push_events", default: true
- t.boolean "issues_events", default: true
- t.boolean "merge_requests_events", default: true
- t.boolean "tag_push_events", default: true
- t.boolean "note_events", default: true, null: false
- t.boolean "build_events", default: false, null: false
+ t.boolean "template", default: false
+ t.boolean "push_events", default: true
+ t.boolean "issues_events", default: true
+ t.boolean "merge_requests_events", default: true
+ t.boolean "tag_push_events", default: true
+ t.boolean "note_events", default: true, null: false
+ t.boolean "build_events", default: false, null: false
end
add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree
@@ -734,16 +743,16 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "services", ["template"], name: "index_services_on_template", using: :btree
create_table "snippets", force: :cascade do |t|
- t.string "title"
+ t.string "title", limit: 255
t.text "content"
- t.integer "author_id", null: false
+ t.integer "author_id", null: false
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "file_name"
+ t.string "file_name", limit: 255
t.datetime "expires_at"
- t.string "type"
- t.integer "visibility_level", default: 0, null: false
+ t.string "type", limit: 255
+ t.integer "visibility_level", default: 0, null: false
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
@@ -756,7 +765,7 @@ ActiveRecord::Schema.define(version: 20151224123230) do
create_table "subscriptions", force: :cascade do |t|
t.integer "user_id"
t.integer "subscribable_id"
- t.string "subscribable_type"
+ t.string "subscribable_type", limit: 255
t.boolean "subscribed"
t.datetime "created_at"
t.datetime "updated_at"
@@ -767,10 +776,10 @@ ActiveRecord::Schema.define(version: 20151224123230) do
create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
- t.string "taggable_type"
+ t.string "taggable_type", limit: 255
t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context"
+ t.string "tagger_type", limit: 255
+ t.string "context", limit: 255
t.datetime "created_at"
end
@@ -778,67 +787,67 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
create_table "tags", force: :cascade do |t|
- t.string "name"
- t.integer "taggings_count", default: 0
+ t.string "name", limit: 255
+ t.integer "taggings_count", default: 0
end
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
create_table "users", force: :cascade do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", default: "", null: false
- t.string "reset_password_token"
+ t.string "email", limit: 255, default: "", null: false
+ t.string "encrypted_password", limit: 255, default: "", null: false
+ t.string "reset_password_token", limit: 255
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
- t.integer "sign_in_count", default: 0
+ t.integer "sign_in_count", default: 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
- t.string "current_sign_in_ip"
- t.string "last_sign_in_ip"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "name"
- t.boolean "admin", default: false, null: false
- t.integer "projects_limit", default: 10
- t.string "skype", default: "", null: false
- t.string "linkedin", default: "", null: false
- t.string "twitter", default: "", null: false
- t.string "authentication_token"
- t.integer "theme_id", default: 1, null: false
- t.string "bio"
- t.integer "failed_attempts", default: 0
+ t.string "current_sign_in_ip", limit: 255
+ t.string "last_sign_in_ip", limit: 255
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name", limit: 255
+ t.boolean "admin", default: false, null: false
+ t.integer "projects_limit", default: 10
+ t.string "skype", limit: 255, default: "", null: false
+ t.string "linkedin", limit: 255, default: "", null: false
+ t.string "twitter", limit: 255, default: "", null: false
+ t.string "authentication_token", limit: 255
+ t.integer "theme_id", default: 1, null: false
+ t.string "bio", limit: 255
+ t.integer "failed_attempts", default: 0
t.datetime "locked_at"
- t.string "username"
- t.boolean "can_create_group", default: true, null: false
- t.boolean "can_create_team", default: true, null: false
- t.string "state"
- t.integer "color_scheme_id", default: 1, null: false
- t.integer "notification_level", default: 1, null: false
+ t.string "username", limit: 255
+ t.boolean "can_create_group", default: true, null: false
+ t.boolean "can_create_team", default: true, null: false
+ t.string "state", limit: 255
+ t.integer "color_scheme_id", default: 1, null: false
+ t.integer "notification_level", default: 1, null: false
t.datetime "password_expires_at"
t.integer "created_by_id"
t.datetime "last_credential_check_at"
- t.string "avatar"
- t.string "confirmation_token"
+ t.string "avatar", limit: 255
+ t.string "confirmation_token", limit: 255
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
- t.string "unconfirmed_email"
- t.boolean "hide_no_ssh_key", default: false
- t.string "website_url", default: "", null: false
- t.string "notification_email"
- t.boolean "hide_no_password", default: false
- t.boolean "password_automatically_set", default: false
- t.string "location"
- t.string "encrypted_otp_secret"
- t.string "encrypted_otp_secret_iv"
- t.string "encrypted_otp_secret_salt"
- t.boolean "otp_required_for_login", default: false, null: false
+ t.string "unconfirmed_email", limit: 255
+ t.boolean "hide_no_ssh_key", default: false
+ t.string "website_url", limit: 255, default: "", null: false
+ t.string "notification_email", limit: 255
+ t.boolean "hide_no_password", default: false
+ t.boolean "password_automatically_set", default: false
+ t.string "location", limit: 255
+ t.string "encrypted_otp_secret", limit: 255
+ t.string "encrypted_otp_secret_iv", limit: 255
+ t.string "encrypted_otp_secret_salt", limit: 255
+ t.boolean "otp_required_for_login", default: false, null: false
t.text "otp_backup_codes"
- t.string "public_email", default: "", null: false
- t.integer "dashboard", default: 0
- t.integer "project_view", default: 0
+ t.string "public_email", limit: 255, default: "", null: false
+ t.integer "dashboard", default: 0
+ t.integer "project_view", default: 0
t.integer "consumed_timestep"
- t.integer "layout", default: 0
- t.boolean "hide_project_limit", default: false
+ t.integer "layout", default: 0
+ t.boolean "hide_project_limit", default: false
t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
end
@@ -865,19 +874,19 @@ ActiveRecord::Schema.define(version: 20151224123230) do
add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
create_table "web_hooks", force: :cascade do |t|
- t.string "url"
+ t.string "url", limit: 255
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type", default: "ProjectHook"
+ t.string "type", limit: 255, default: "ProjectHook"
t.integer "service_id"
- t.boolean "push_events", default: true, null: false
- t.boolean "issues_events", default: false, null: false
- t.boolean "merge_requests_events", default: false, null: false
- t.boolean "tag_push_events", default: false
- t.boolean "note_events", default: false, null: false
- t.boolean "enable_ssl_verification", default: true
- t.boolean "build_events", default: false, null: false
+ t.boolean "push_events", default: true, null: false
+ t.boolean "issues_events", default: false, null: false
+ t.boolean "merge_requests_events", default: false, null: false
+ t.boolean "tag_push_events", default: false
+ t.boolean "note_events", default: false, null: false
+ t.boolean "enable_ssl_verification", default: true
+ t.boolean "build_events", default: false, null: false
end
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
diff --git a/doc/README.md b/doc/README.md
index f40a257b42f..f4553a899d3 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -7,7 +7,7 @@
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
- [Markdown](markdown/markdown.md) GitLab's advanced formatting system.
-- [Migrating from SVN](migration/README.md) Convert a SVN repository to Git and GitLab
+- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab
- [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
@@ -56,6 +56,7 @@
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
- [Log system](logs/logs.md) Log system.
+- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
new file mode 100644
index 00000000000..1eb3a74d304
--- /dev/null
+++ b/doc/administration/environment_variables.md
@@ -0,0 +1,53 @@
+# Environment Variables
+
+## Introduction
+
+Commonly people configure GitLab via the gitlab.rb configuration file in the Omnibus package.
+
+But if you prefer to use environment variables we allow that too.
+
+## Supported environment variables
+
+Variable | Type | Explanation
+-------- | ---- | -----------
+GITLAB_ROOT_PASSWORD | string | sets the password for the `root` user on installation
+GITLAB_HOST | url | hostname of the GitLab server includes http or https
+RAILS_ENV | production / development / staging / test | Rails environment
+DATABASE_URL | url | For example: postgresql://localhost/blog_development?pool=5
+GITLAB_EMAIL_FROM | email | Email address used in the "From" field in mails sent by GitLab
+GITLAB_EMAIL_DISPLAY_NAME | string | Name used in the "From" field in mails sent by GitLab
+GITLAB_EMAIL_REPLY_TO | email | Email address used in the "Reply-To" field in mails sent by GitLab
+
+## Complete database variables
+
+As explained in the [Heroku documentation](https://devcenter.heroku.com/articles/rails-database-connection-behavior) the DATABASE_URL doesn't let you set:
+
+- adapter
+- database
+- username
+- password
+- host
+- port
+
+To do so please `cp config/database.yml.env config/database.yml` and use the following variables:
+
+Variable | Default
+--- | ---
+GITLAB_DATABASE_ADAPTER | postgresql
+GITLAB_DATABASE_ENCODING | unicode
+GITLAB_DATABASE_DATABASE | gitlab_#{ENV['RAILS_ENV']
+GITLAB_DATABASE_POOL | 10
+GITLAB_DATABASE_USERNAME | root
+GITLAB_DATABASE_PASSWORD |
+GITLAB_DATABASE_HOST | localhost
+GITLAB_DATABASE_PORT | 5432
+
+## Adding more variables
+
+We welcome merge requests to make more settings configurable via variables.
+Please stick to the naming scheme "GITLAB_#{name 1_settings.rb in upper case}".
+
+## Omnibus configuration
+
+It's possible to preconfigure the GitLab image by adding the environment variable: `GITLAB_OMNIBUS_CONFIG` to 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).
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 366a1f8abec..8bc0a67067a 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -4,8 +4,7 @@
Get all merge requests for this project.
The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
-The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. With GitLab 8.2 the return fields `upvotes` and
-`downvotes` are deprecated and always return `0`.
+The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
```
GET /projects/:id/merge_requests
@@ -58,7 +57,7 @@ Parameters:
## Get single MR
-Shows information about a single merge request. With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and always return `0`.
+Shows information about a single merge request.
```
GET /projects/:id/merge_request/:merge_request_id
@@ -141,8 +140,6 @@ Parameters:
## Get single MR changes
Shows information about the merge request including its files and changes.
-With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and
-always return `0`.
```
GET /projects/:id/merge_request/:merge_request_id/changes
@@ -213,9 +210,7 @@ Parameters:
## Create MR
-Creates a new merge request. With GitLab 8.2 the return fields `upvotes` and `
-downvotes` are deprecated and always return `0`.
-
+Creates a new merge request.
```
POST /projects/:id/merge_requests
```
@@ -266,8 +261,7 @@ If an error occurs, an error number and a message explaining the reason is retur
## Update MR
-Updates an existing merge request. You can change the target branch, title, or even close the MR. With GitLab 8.2 the return fields `upvotes` and `downvotes`
-are deprecated and always return `0`.
+Updates an existing merge request. You can change the target branch, title, or even close the MR.
```
PUT /projects/:id/merge_request/:merge_request_id
@@ -318,8 +312,7 @@ If an error occurs, an error number and a message explaining the reason is retur
## Accept MR
-Merge changes submitted with MR using this API. With GitLab 8.2 the return
-fields `upvotes` and `downvotes` are deprecated and always return `0`.
+Merge changes submitted with MR using this API.
If merge success you get `200 OK`.
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 4d7ef288df8..d4d63e825ab 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -6,8 +6,7 @@ Notes are comments on snippets, issues or merge requests.
### List project issue notes
-Gets a list of all notes for a single issue. With GitLab 8.2 the return fields
-`upvote` and `downvote` are deprecated and always return `false`.
+Gets a list of all notes for a single issue.
```
GET /projects/:id/issues/:issue_id/notes
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 2c1de5859f8..9f7c1bfe6a0 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -11,9 +11,6 @@ You can add a new trigger by going to your project's **Settings > Triggers**.
The **Add trigger** button will create a new token which you can then use to
trigger a rebuild of this particular project.
-Once at least one trigger is created, on the **Triggers** page you will find
-some descriptive information on how you can
-
Every new trigger you create, gets assigned a different token which you can
then use inside your scripts or `.gitlab-ci.yml`. You also have a nice
overview of the time the triggers were last used.
@@ -111,7 +108,7 @@ Now, whenever a new tag is pushed on project A, the build will run and the
`stage: test` complete successfully.
_**Note:** If your project is public, passing the token in plain text is
-probably not the wiser idea, so you might want to use a
+probably not the wisest idea, so you might want to use a
[secure variable](../variables/README.md#user-defined-variables-secure-variables)
for that purpose._
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 6263353851f..2a9f76533b7 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -13,6 +13,7 @@ See the documentation below for details on how to configure these services.
- [Slack](slack.md) Integrate with the Slack chat service
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
+- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html).
diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md
new file mode 100644
index 00000000000..a301d1a613c
--- /dev/null
+++ b/doc/integration/recaptcha.md
@@ -0,0 +1,23 @@
+# reCAPTCHA
+
+GitLab leverages [Google's reCAPTCHA](https://www.google.com/recaptcha/intro/index.html)
+to protect against spam and abuse. GitLab displays the CAPTCHA form on the sign-up page
+to confirm that a real user, not a bot, is attempting to create an account.
+
+## Configuration
+
+To use reCAPTCHA, first you must create a site and private key.
+
+1. Go to the URL: https://www.google.com/recaptcha/admin
+
+2. Fill out the form necessary to obtain reCAPTCHA keys.
+
+3. Login to your GitLab server, with administrator credentials.
+
+4. Go to Applications Settings on Admin Area (`admin/application_settings`)
+
+5. Fill all recaptcha fields with keys from previous steps
+
+6. Check the `Enable reCAPTCHA` checkbox
+
+7. Save the configuration.
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index bcd00cfc6bf..1be78ac1823 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -6,11 +6,11 @@ If a user is both in a project group and in the project itself, the highest perm
If a user is a GitLab administrator they receive all permissions.
-On public projects the Guest role is not enforced.
-All users will be able to create issues, leave comments, and pull or download the project code.
+On public projects the Guest role is not enforced.
+All users will be able to create issues, leave comments, and pull or download the project code.
To add or import a user, you can follow the [project users and members
-documentation](doc/workflow/add-user/add-user.md).
+documentation](../workflow/add-user/add-user.md).
## Project
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 093450a6de3..cdd6652b7b0 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -153,6 +153,49 @@ with the name of your bucket:
}
```
+### Uploading to locally mounted shares
+
+You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
+using the [`Local`](https://github.com/fog/fog-local#usage) storage provider.
+The directory pointed to by the `local_root` key **must** be owned by the `git`
+user **when mounted** (mounting with the `uid=` of the `git` user for `CIFS` and
+`SMB`) or the user that you are executing the backup tasks under (for omnibus
+packages, this is the `git` user).
+
+The `backup_upload_remote_directory` **must** be set in addition to the
+`local_root` key. This is the sub directory inside the mounted directory that
+backups will be copied to, and will be created if it does not exist. If the
+directory that you want to copy the tarballs to is the root of your mounted
+directory, just use `.` instead.
+
+For omnibus packages:
+
+```ruby
+gitlab_rails['backup_upload_connection'] = {
+ :provider => 'Local',
+ :local_root => '/mnt/backups'
+}
+
+# The directory inside the mounted folder to copy backups to
+# Use '.' to store them in the root directory
+gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups'
+```
+
+For installations from source:
+
+```yaml
+ backup:
+ # snip
+ upload:
+ # Fog storage connection settings, see http://fog.io/storage/ .
+ connection:
+ provider: Local
+ local_root: '/mnt/backups'
+ # The directory inside the mounted folder to copy backups to
+ # Use '.' to store them in the root directory
+ remote_directory: 'gitlab_backups'
+```
+
## Backup archive permissions
The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default.
diff --git a/doc/security/README.md b/doc/security/README.md
index 384df570394..f34c792d005 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -6,4 +6,5 @@
- [Information exclusivity](information_exclusivity.md)
- [Reset your root password](reset_root_password.md)
- [User File Uploads](user_file_uploads.md)
+- [How we manage the CRIME vulnerability](crime_vulnerability.md)
- [Enforce Two-Factor authentication](two_factor_authentication.md)
diff --git a/doc/security/crime_vulnerability.md b/doc/security/crime_vulnerability.md
new file mode 100644
index 00000000000..94ba5d1375d
--- /dev/null
+++ b/doc/security/crime_vulnerability.md
@@ -0,0 +1,63 @@
+# How we manage the TLS protocol CRIME vulnerability
+
+> CRIME ("Compression Ratio Info-leak Made Easy") is a security exploit against
+secret web cookies over connections using the HTTPS and SPDY protocols that also
+use data compression. When used to recover the content of secret
+authentication cookies, it allows an attacker to perform session hijacking on an
+authenticated web session, allowing the launching of further attacks.
+([CRIME](https://en.wikipedia.org/w/index.php?title=CRIME&oldid=692423806))
+
+### Description
+
+The TLS Protocol CRIME Vulnerability affects compression over HTTPS, therefore
+it warns against using SSL Compression (for example gzip) or SPDY which
+optionally uses compression as well.
+
+GitLab supports both gzip and [SPDY][ngx-spdy] and mitigates the CRIME
+vulnerability by deactivating gzip when HTTPS is enabled. You can see the
+sources of the files in question:
+
+* [Source installation NGINX file][source-nginx]
+* [Omnibus installation NGINX file][omnibus-nginx]
+
+Although SPDY is enabled in Omnibus installations, CRIME relies on compression
+(the 'C') and the default compression level in NGINX's SPDY module is 0
+(no compression).
+
+### Nessus
+
+The Nessus scanner, [reports a possible CRIME vulnerability][nessus] in GitLab
+similar to the following format:
+
+```
+Description
+
+This remote service has one of two configurations that are known to be required for the CRIME attack:
+SSL/TLS compression is enabled.
+TLS advertises the SPDY protocol earlier than version 4.
+
+...
+
+Output
+
+The following configuration indicates that the remote service may be vulnerable to the CRIME attack:
+SPDY support earlier than version 4 is advertised.
+```
+
+From the report above it is important to note that Nessus is only checking if
+TLS advertises the SPDY protocol earlier than version 4, it does not perform an
+attack nor does it check if compression is enabled. With just this approach, it
+cannot tell that SPDY's compression is disabled and not subject to the CRIME
+vulnerability.
+
+### References
+
+* Nginx ["Module ngx_http_spdy_module"][ngx-spdy]
+* Tenable Network Security, Inc. ["Transport Layer Security (TLS) Protocol CRIME Vulnerability"][nessus]
+* Wikipedia contributors, ["CRIME"][wiki-crime] Wikipedia, The Free Encyclopedia
+
+[source-nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/gitlab-ssl
+[omnibus-nginx]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/templates/default/nginx-gitlab-http.conf.erb
+[ngx-spdy]: http://nginx.org/en/docs/http/ngx_http_spdy_module.html
+[nessus]: https://www.tenable.com/plugins/index.php?view=single&id=62565
+[wiki-crime]: https://en.wikipedia.org/wiki/CRIME
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index fe5b45dd432..77eb53427e2 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -9,7 +9,7 @@ already has one by running the following command:
cat ~/.ssh/id_rsa.pub
```
-If you see a long string starting with `ssh-rsa` or `ssh-dsa`, you can skip the `ssh-keygen` step.
+If you see a long string starting with `ssh-rsa`, you can skip the `ssh-keygen` step.
Note: It is a best practice to use a password for an SSH key, but it is not
required and you can skip creating a password by pressing enter. Note that
@@ -20,8 +20,9 @@ To generate a new SSH key, use the following command:
ssh-keygen -t rsa -C "$your_email"
```
This command will prompt you for a location and filename to store the key
-pair and for a password. When prompted for the location and filename, you
-can press enter to use the default.
+pair and for a password. When prompted for the location and filename, just
+press enter to use the default. If you use a different name, the key will not
+be used automatically.
Use the command below to show your public key:
```bash
@@ -29,10 +30,10 @@ cat ~/.ssh/id_rsa.pub
```
Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your
-user profile. Please copy the complete key starting with `ssh-` and ending
+user profile. Please copy the complete key starting with `ssh-rsa` and ending
with your username and host.
-To copy your public key to the clipboard, use code below. Depending on your
+To copy your public key to the clipboard, use the code below. Depending on your
OS you'll need to use a different command:
**Windows:**
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 5cb05b13b3e..49f98ded046 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -1,6 +1,6 @@
# System hooks
-Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
+Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
System hooks can be used, e.g. for logging or changing information in a LDAP server.
@@ -17,6 +17,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_create",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -33,6 +34,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_destroy",
"name": "Underscore",
"owner_email": "johnsmith@gmail.com",
@@ -44,11 +46,48 @@ X-Gitlab-Event: System Hook
}
```
+**Project renamed:**
+
+```json
+{
+ "created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
+ "event_name": "project_rename",
+ "name": "Underscore",
+ "path": "underscore",
+ "path_with_namespace": "jsmith/underscore",
+ "project_id": 73,
+ "owner_name": "John Smith",
+ "owner_email": "johnsmith@gmail.com",
+ "project_visibility": "internal",
+ "old_path_with_namespace": "jsmith/overscore",
+}
+```
+
+**Project transferred:**
+
+```json
+{
+ "created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
+ "event_name": "project_transfer",
+ "name": "Underscore",
+ "path": "underscore",
+ "path_with_namespace": "scores/underscore",
+ "project_id": 73,
+ "owner_name": "John Smith",
+ "owner_email": "johnsmith@gmail.com",
+ "project_visibility": "internal",
+ "old_path_with_namespace": "jsmith/overscore",
+}
+```
+
**New Team Member:**
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_team",
"project_access": "Master",
"project_id": 74,
@@ -67,6 +106,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_team",
"project_access": "Master",
"project_id": 74,
@@ -85,6 +125,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:44:07Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"email": "js@gitlabhq.com",
"event_name": "user_create",
"name": "John Smith",
@@ -97,6 +138,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:44:07Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"email": "js@gitlabhq.com",
"event_name": "user_destroy",
"name": "John Smith",
@@ -110,6 +152,7 @@ X-Gitlab-Event: System Hook
{
"event_name": "key_create",
"created_at": "2014-08-18 18:45:16 UTC",
+ "updated_at": "2012-07-21T07:38:22Z",
"username": "root",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost",
"id": 4
@@ -122,6 +165,7 @@ X-Gitlab-Event: System Hook
{
"event_name": "key_destroy",
"created_at": "2014-08-18 18:45:16 UTC",
+ "updated_at": "2012-07-21T07:38:22Z",
"username": "root",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost",
"id": 4
@@ -133,6 +177,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_create",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -147,6 +192,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_destroy",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -161,6 +207,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_group",
"group_access": "Master",
"group_id": 78,
@@ -176,6 +223,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_group",
"group_access": "Master",
"group_id": 78,
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index c4661dc16af..3748941b781 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -99,8 +99,6 @@ 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
-# Update init.d script
-sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
### 7. Update configuration files
diff --git a/features/explore/groups.feature b/features/explore/groups.feature
index a42e59c98f2..5fc9b135601 100644
--- a/features/explore/groups.feature
+++ b/features/explore/groups.feature
@@ -105,15 +105,6 @@ Feature: Explore Groups
When I visit the public groups area
Then I should see group "TestGroup"
- Scenario: I should not see group with internal project in public groups area
- Given group "TestGroup" has internal project "Internal"
- When I visit the public groups area
- Then I should not see group "TestGroup"
-
- Scenario: I should not see group with private project in public groups area
- When I visit the public groups area
- Then I should not see group "TestGroup"
-
Scenario: I should see group with public project in public groups area as user
Given group "TestGroup" has public project "Community"
When I sign in as a user
@@ -125,9 +116,3 @@ Feature: Explore Groups
When I sign in as a user
And I visit the public groups area
Then I should see group "TestGroup"
-
- Scenario: I should not see group with private project in public groups area as user
- When I sign in as a user
- And I visit the public groups area
- Then I should not see group "TestGroup"
-
diff --git a/features/project/fork.feature b/features/project/fork.feature
index 22f68e5b340..37cd53ee977 100644
--- a/features/project/fork.feature
+++ b/features/project/fork.feature
@@ -14,3 +14,14 @@ Feature: Project Fork
And I click link "Fork"
When I fork to my namespace
Then I should see a "Name has already been taken" warning
+
+ Scenario: Merge request on canonical repo goes to fork merge request page
+ Given I click link "Fork"
+ And I fork to my namespace
+ Then I should see the forked project page
+ When I visit project "Shop" page
+ Then I should see "New merge request"
+ And I goto the Merge Requests page
+ Then I should see "New merge request"
+ And I click link "New merge request"
+ Then I should see the new merge request page for my namespace
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index b0230add34f..e98bd51ca89 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -30,4 +30,23 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
click_link current_user.name
end
end
+
+ step 'I should see "New merge request"' do
+ expect(page).to have_content(/new merge request/i)
+ end
+
+ step 'I goto the Merge Requests page' do
+ page.within '.page-sidebar-expanded' do
+ click_link "Merge Requests"
+ end
+ end
+
+ step 'I click link "New merge request"' do
+ expect(page).to have_content(/new merge request/i)
+ click_link "New Merge Request"
+ end
+
+ step 'I should see the new merge request page for my namespace' do
+ current_path.should have_content(/#{current_user.namespace.name}/i)
+ end
end
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index a7e15398819..2c2ed08655e 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -15,15 +15,17 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I click to emoji in the picker' do
- page.within '.emoji-menu' do
+ page.within '.emoji-menu-content' do
page.first('.emoji-icon').click
end
end
step 'I can remove it by clicking to icon' do
page.within '.awards' do
- page.first('.award').click
- expect(page).to_not have_selector '.award'
+ expect do
+ page.find('.award.active').click
+ sleep 0.3
+ end.to change{ page.all(".award").size }.from(3).to(2)
end
end
@@ -37,7 +39,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I have award added' do
page.within '.awards' do
expect(page).to have_selector '.award'
- expect(page.find('.award .counter')).to have_content '1'
+ expect(page.find('.award.active .counter')).to have_content '1'
end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 4a7ff21d385..8e8c9c57452 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -59,15 +59,14 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click "author" dropdown' do
- first('.ajax-users-select').click
+ first('#s2id_author_id').click
end
step 'I see current user as the first user' do
- expect(page).to have_selector('.user-result', visible: true, count: 4)
+ expect(page).to have_selector('.user-result', visible: true, count: 3)
users = page.all('.user-name')
- expect(users[0].text).to eq 'Any Assignee'
- expect(users[1].text).to eq 'Unassigned'
- expect(users[2].text).to eq current_user.name
+ expect(users[0].text).to eq 'Any Author'
+ expect(users[1].text).to eq current_user.name
end
step 'I submit new issue "500 error on profile"' do
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index f8511ac5f5c..26e7c956e8f 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -166,7 +166,6 @@ module API
class MergeRequest < ProjectEntity
expose :target_branch, :source_branch
- # deprecated, always returns 0
expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index a9e0960872a..0781236cf6d 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -3,7 +3,7 @@ module API
class Projects < Grape::API
before { authenticate! }
- resource :projects do
+ resource :projects, requirements: { id: /[^\/]+/ } do
helpers do
def map_public_to_visibility_level(attrs)
publik = attrs.delete(:public)
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index b99ccd98624..b2db10e6864 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -47,7 +47,17 @@ module Banzai
{ object_sym => LazyReference.new(object_class, node.attr(data_reference)) }
end
- delegate :object_class, :object_sym, :references_in, to: :class
+ def object_class
+ self.class.object_class
+ end
+
+ def object_sym
+ self.class.object_sym
+ end
+
+ def references_in(*args, &block)
+ self.class.references_in(*args, &block)
+ end
def find_object(project, id)
# Implement in child class
diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb
index 89e7a79789a..66f77902319 100644
--- a/lib/banzai/filter/redactor_filter.rb
+++ b/lib/banzai/filter/redactor_filter.rb
@@ -10,8 +10,8 @@ module Banzai
#
class RedactorFilter < HTML::Pipeline::Filter
def call
- doc.css('a.gfm').each do |node|
- unless user_can_reference?(node)
+ Querying.css(doc, 'a.gfm').each do |node|
+ unless user_can_see_reference?(node)
# The reference should be replaced by the original text,
# which is not always the same as the rendered text.
text = node.attr('data-original') || node.text
@@ -24,12 +24,12 @@ module Banzai
private
- def user_can_reference?(node)
+ def user_can_see_reference?(node)
if node.has_attribute?('data-reference-filter')
reference_type = node.attr('data-reference-filter')
reference_filter = Banzai::Filter.const_get(reference_type)
- reference_filter.user_can_reference?(current_user, node, context)
+ reference_filter.user_can_see_reference?(current_user, node, context)
else
true
end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index a22a7a7afd3..7198a8b03e2 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -12,7 +12,7 @@ module Banzai
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
class ReferenceFilter < HTML::Pipeline::Filter
- def self.user_can_reference?(user, node, context)
+ def self.user_can_see_reference?(user, node, context)
if node.has_attribute?('data-project')
project_id = node.attr('data-project').to_i
return true if project_id == context[:project].try(:id)
@@ -24,6 +24,10 @@ module Banzai
end
end
+ def self.user_can_reference?(user, node, context)
+ true
+ end
+
def self.referenced_by(node)
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
@@ -120,7 +124,7 @@ module Banzai
def replace_link_nodes_with_text(pattern)
return doc if project.nil?
- doc.search('a').each do |node|
+ doc.xpath('descendant-or-self::a').each do |node|
klass = node.attr('class')
next if klass && klass.include?('gfm')
@@ -158,7 +162,7 @@ module Banzai
def replace_link_nodes_with_href(pattern)
return doc if project.nil?
- doc.search('a').each do |node|
+ doc.xpath('descendant-or-self::a').each do |node|
klass = node.attr('class')
next if klass && klass.include?('gfm')
diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb
index 855f238ac1e..bef04112919 100644
--- a/lib/banzai/filter/reference_gatherer_filter.rb
+++ b/lib/banzai/filter/reference_gatherer_filter.rb
@@ -16,7 +16,7 @@ module Banzai
end
def call
- doc.css('a.gfm').each do |node|
+ Querying.css(doc, 'a.gfm').each do |node|
gather_references(node)
end
@@ -35,7 +35,9 @@ module Banzai
return if context[:reference_filter] && reference_filter != context[:reference_filter]
- return unless reference_filter.user_can_reference?(current_user, node, context)
+ return if author && !reference_filter.user_can_reference?(author, node, context)
+
+ return unless reference_filter.user_can_see_reference?(current_user, node, context)
references = reference_filter.referenced_by(node)
return unless references
@@ -57,6 +59,10 @@ module Banzai
def current_user
context[:current_user]
end
+
+ def author
+ context[:author]
+ end
end
end
end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 5a081125f21..66f166939e4 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -91,7 +91,7 @@ module Banzai
parts = request_path.split('/')
parts.pop if path_type(request_path) != 'tree'
- while parts.length > 1 && path.start_with?('../')
+ while path.start_with?('../')
parts.pop
path.sub!('../', '')
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 7f302d51dd7..964ab60f614 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -39,7 +39,7 @@ module Banzai
end
end
- def self.user_can_reference?(user, node, context)
+ def self.user_can_see_reference?(user, node, context)
if node.has_attribute?('data-group')
group = Group.find(node.attr('data-group')) rescue nil
Ability.abilities.allowed?(user, :read_group, group)
@@ -48,6 +48,18 @@ module Banzai
end
end
+ def self.user_can_reference?(user, node, context)
+ # Only team members can reference `@all`
+ if node.has_attribute?('data-project')
+ project = Project.find(node.attr('data-project')) rescue nil
+ return false unless project
+
+ user && project.team.member?(user)
+ else
+ super
+ end
+ end
+
def call
replace_text_nodes_matching(User.reference_pattern) do |content|
user_link_filter(content)
diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb
new file mode 100644
index 00000000000..1e1b51e683e
--- /dev/null
+++ b/lib/banzai/querying.rb
@@ -0,0 +1,18 @@
+module Banzai
+ module Querying
+ # Searches a Nokogiri document using a CSS query, optionally optimizing it
+ # whenever possible.
+ #
+ # document - A document/element to search.
+ # query - The CSS query to use.
+ #
+ # Returns a Nokogiri::XML::NodeSet.
+ def self.css(document, query)
+ # When using "a.foo" Nokogiri compiles this to "//a[...]" but
+ # "descendant::a[...]" is quite a bit faster and achieves the same result.
+ xpath = Nokogiri::CSS.xpath_for(query)[0].gsub(%r{^//}, 'descendant::')
+
+ document.xpath(xpath)
+ end
+ end
+end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index 115ae914524..910e1c6994e 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -1,7 +1,5 @@
module Banzai
module Renderer
- CACHE_ENABLED = false
-
# Convert a Markdown String into an HTML-safe String of HTML
#
# Note that while the returned HTML will have been sanitized of dangerous
@@ -20,13 +18,22 @@ module Banzai
cache_key = context.delete(:cache_key)
cache_key = full_cache_key(cache_key, context[:pipeline])
- if cache_key && CACHE_ENABLED
- Rails.cache.fetch(cache_key) do
- cacheless_render(text, context)
+ cacheless = cacheless_render(text, context)
+
+ if cache_key && ENV["DEBUG_BANZAI_CACHE"]
+ cached = Rails.cache.fetch(cache_key) { cacheless }
+
+ if cached != cacheless
+ Rails.logger.warn "Banzai cache mismatch"
+ Rails.logger.warn "Text: #{text.inspect}"
+ Rails.logger.warn "Context: #{context.inspect}"
+ Rails.logger.warn "Cache key: #{cache_key.inspect}"
+ Rails.logger.warn "Cacheless: #{cacheless.inspect}"
+ Rails.logger.warn "With cache: #{cached.inspect}"
end
- else
- cacheless_render(text, context)
end
+
+ cacheless
end
def self.render_result(text, context = {})
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 8a7f8dc5003..85583dce9ee 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -45,11 +45,11 @@ module Gitlab
end
def starting_year
- (Time.now - 1.year).strftime("%Y")
+ 1.year.ago.year
end
def starting_month
- Date.today.strftime("%m").to_i
+ Date.today.month
end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 46a4ef0e31f..7a86c09158e 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -38,7 +38,9 @@ module Gitlab
true
end
- use_db && ActiveRecord::Base.connection.active? && ActiveRecord::Base.connection.table_exists?('application_settings')
+ use_db && ActiveRecord::Base.connection.active? &&
+ !ActiveRecord::Migrator.needs_migration? &&
+ ActiveRecord::Base.connection.table_exists?('application_settings')
end
end
end
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
new file mode 100644
index 00000000000..ee88ab34d6c
--- /dev/null
+++ b/lib/gitlab/metrics.rb
@@ -0,0 +1,102 @@
+module Gitlab
+ module Metrics
+ extend Gitlab::CurrentSettings
+
+ RAILS_ROOT = Rails.root.to_s
+ METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s
+ PATH_REGEX = /^#{RAILS_ROOT}\/?/
+
+ def self.settings
+ @settings ||= {
+ enabled: current_application_settings[:metrics_enabled],
+ pool_size: current_application_settings[:metrics_pool_size],
+ timeout: current_application_settings[:metrics_timeout],
+ method_call_threshold: current_application_settings[:metrics_method_call_threshold],
+ host: current_application_settings[:metrics_host],
+ username: current_application_settings[:metrics_username],
+ password: current_application_settings[:metrics_password],
+ port: current_application_settings[:metrics_port]
+ }
+ end
+
+ def self.enabled?
+ settings[:enabled] || false
+ end
+
+ def self.mri?
+ RUBY_ENGINE == 'ruby'
+ end
+
+ def self.method_call_threshold
+ # This is memoized since this method is called for every instrumented
+ # method. Loading data from an external cache on every method call slows
+ # things down too much.
+ @method_call_threshold ||= settings[:method_call_threshold]
+ end
+
+ def self.pool
+ @pool
+ end
+
+ # Returns a relative path and line number based on the last application call
+ # frame.
+ def self.last_relative_application_frame
+ frame = caller_locations.find do |l|
+ l.path.start_with?(RAILS_ROOT) && !l.path.start_with?(METRICS_ROOT)
+ end
+
+ if frame
+ return frame.path.sub(PATH_REGEX, ''), frame.lineno
+ else
+ return nil, nil
+ end
+ end
+
+ def self.submit_metrics(metrics)
+ prepared = prepare_metrics(metrics)
+
+ pool.with do |connection|
+ prepared.each do |metric|
+ begin
+ connection.write_points([metric])
+ rescue StandardError
+ end
+ end
+ end
+ end
+
+ def self.prepare_metrics(metrics)
+ metrics.map do |hash|
+ new_hash = hash.symbolize_keys
+
+ new_hash[:tags].each do |key, value|
+ if value.blank?
+ new_hash[:tags].delete(key)
+ else
+ new_hash[:tags][key] = escape_value(value)
+ end
+ end
+
+ new_hash
+ end
+ end
+
+ def self.escape_value(value)
+ value.to_s.gsub('=', '\\=')
+ end
+
+ # When enabled this should be set before being used as the usual pattern
+ # "@foo ||= bar" is _not_ thread-safe.
+ if enabled?
+ @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do
+ host = settings[:host]
+ user = settings[:username]
+ pw = settings[:password]
+ port = settings[:port]
+
+ InfluxDB::Client.
+ new(udp: { host: host, port: port }, username: user, password: pw)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/delta.rb b/lib/gitlab/metrics/delta.rb
new file mode 100644
index 00000000000..bcf28eed84d
--- /dev/null
+++ b/lib/gitlab/metrics/delta.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module Metrics
+ # Class for calculating the difference between two numeric values.
+ #
+ # Every call to `compared_with` updates the internal value. This makes it
+ # possible to use a single Delta instance to calculate the delta over time
+ # of an ever increasing number.
+ #
+ # Example usage:
+ #
+ # delta = Delta.new(0)
+ #
+ # delta.compared_with(10) # => 10
+ # delta.compared_with(15) # => 5
+ # delta.compared_with(20) # => 5
+ class Delta
+ def initialize(value = 0)
+ @value = value
+ end
+
+ # new_value - The value to compare with as a Numeric.
+ #
+ # Returns a new Numeric (depending on the type of `new_value`).
+ def compared_with(new_value)
+ delta = new_value - @value
+ @value = new_value
+
+ delta
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
new file mode 100644
index 00000000000..d9fce2e6758
--- /dev/null
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -0,0 +1,148 @@
+module Gitlab
+ module Metrics
+ # Module for instrumenting methods.
+ #
+ # This module allows instrumenting of methods without having to actually
+ # alter the target code (e.g. by including modules).
+ #
+ # Example usage:
+ #
+ # Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login)
+ module Instrumentation
+ SERIES = 'method_calls'
+
+ def self.configure
+ yield self
+ end
+
+ # Instruments a class method.
+ #
+ # mod - The module to instrument as a Module/Class.
+ # name - The name of the method to instrument.
+ def self.instrument_method(mod, name)
+ instrument(:class, mod, name)
+ end
+
+ # Instruments an instance method.
+ #
+ # mod - The module to instrument as a Module/Class.
+ # name - The name of the method to instrument.
+ def self.instrument_instance_method(mod, name)
+ instrument(:instance, mod, name)
+ end
+
+ # Recursively instruments all subclasses of the given root module.
+ #
+ # This can be used to for example instrument all ActiveRecord models (as
+ # these all inherit from ActiveRecord::Base).
+ #
+ # This method can optionally take a block to pass to `instrument_methods`
+ # and `instrument_instance_methods`.
+ #
+ # root - The root module for which to instrument subclasses. The root
+ # module itself is not instrumented.
+ def self.instrument_class_hierarchy(root, &block)
+ visit = root.subclasses
+
+ until visit.empty?
+ klass = visit.pop
+
+ instrument_methods(klass, &block)
+ instrument_instance_methods(klass, &block)
+
+ klass.subclasses.each { |c| visit << c }
+ end
+ end
+
+ # Instruments all public methods of a module.
+ #
+ # This method optionally takes a block that can be used to determine if a
+ # method should be instrumented or not. The block is passed the receiving
+ # module and an UnboundMethod. If the block returns a non truthy value the
+ # method is not instrumented.
+ #
+ # mod - The module to instrument.
+ def self.instrument_methods(mod)
+ mod.public_methods(false).each do |name|
+ method = mod.method(name)
+
+ if method.owner == mod.singleton_class
+ if !block_given? || block_given? && yield(mod, method)
+ instrument_method(mod, name)
+ end
+ end
+ end
+ end
+
+ # Instruments all public instance methods of a module.
+ #
+ # See `instrument_methods` for more information.
+ #
+ # mod - The module to instrument.
+ def self.instrument_instance_methods(mod)
+ mod.public_instance_methods(false).each do |name|
+ method = mod.instance_method(name)
+
+ if method.owner == mod
+ if !block_given? || block_given? && yield(mod, method)
+ instrument_instance_method(mod, name)
+ end
+ end
+ end
+ end
+
+ # Instruments a method.
+ #
+ # type - The type (:class or :instance) of method to instrument.
+ # mod - The module containing the method.
+ # name - The name of the method to instrument.
+ def self.instrument(type, mod, name)
+ return unless Metrics.enabled?
+
+ name = name.to_sym
+ alias_name = :"_original_#{name}"
+ target = type == :instance ? mod : mod.singleton_class
+
+ if type == :instance
+ target = mod
+ label = "#{mod.name}##{name}"
+ else
+ target = mod.singleton_class
+ label = "#{mod.name}.#{name}"
+ end
+
+ target.class_eval <<-EOF, __FILE__, __LINE__ + 1
+ alias_method #{alias_name.inspect}, #{name.inspect}
+
+ def #{name}(*args, &block)
+ trans = Gitlab::Metrics::Instrumentation.transaction
+
+ if trans
+ start = Time.now
+ retval = __send__(#{alias_name.inspect}, *args, &block)
+ duration = (Time.now - start) * 1000.0
+
+ if duration >= Gitlab::Metrics.method_call_threshold
+ trans.increment(:method_duration, duration)
+
+ trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
+ { duration: duration },
+ method: #{label.inspect})
+ end
+
+ retval
+ else
+ __send__(#{alias_name.inspect}, *args, &block)
+ end
+ end
+ EOF
+ end
+
+ # Small layer of indirection to make it easier to stub out the current
+ # transaction.
+ def self.transaction
+ Transaction.current
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
new file mode 100644
index 00000000000..7ea9555cc8c
--- /dev/null
+++ b/lib/gitlab/metrics/metric.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module Metrics
+ # Class for storing details of a single metric (label, value, etc).
+ class Metric
+ attr_reader :series, :values, :tags, :created_at
+
+ # series - The name of the series (as a String) to store the metric in.
+ # values - A Hash containing the values to store.
+ # tags - A Hash containing extra tags to add to the metrics.
+ def initialize(series, values, tags = {})
+ @values = values
+ @series = series
+ @tags = tags
+ @created_at = Time.now.utc
+ end
+
+ # Returns a Hash in a format that can be directly written to InfluxDB.
+ def to_hash
+ {
+ series: @series,
+ tags: @tags,
+ values: @values,
+ timestamp: @created_at.to_i * 1_000_000_000
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
new file mode 100644
index 00000000000..5c0587c4c51
--- /dev/null
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Metrics
+ # Rack middleware for tracking Rails requests.
+ class RackMiddleware
+ CONTROLLER_KEY = 'action_controller.instance'
+
+ def initialize(app)
+ @app = app
+ end
+
+ # env - A Hash containing Rack environment details.
+ def call(env)
+ trans = transaction_from_env(env)
+ retval = nil
+
+ begin
+ retval = trans.run { @app.call(env) }
+
+ # Even in the event of an error we want to submit any metrics we
+ # might've gathered up to this point.
+ ensure
+ if env[CONTROLLER_KEY]
+ tag_controller(trans, env)
+ end
+
+ trans.finish
+ end
+
+ retval
+ end
+
+ def transaction_from_env(env)
+ trans = Transaction.new
+
+ trans.add_tag(:request_method, env['REQUEST_METHOD'])
+ trans.add_tag(:request_uri, env['REQUEST_URI'])
+
+ trans
+ end
+
+ def tag_controller(trans, env)
+ controller = env[CONTROLLER_KEY]
+ label = "#{controller.class.name}##{controller.action_name}"
+
+ trans.add_tag(:action, label)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/sampler.rb
new file mode 100644
index 00000000000..1ea425bc904
--- /dev/null
+++ b/lib/gitlab/metrics/sampler.rb
@@ -0,0 +1,107 @@
+module Gitlab
+ module Metrics
+ # Class that sends certain metrics to InfluxDB at a specific interval.
+ #
+ # This class is used to gather statistics that can't be directly associated
+ # with a transaction such as system memory usage, garbage collection
+ # statistics, etc.
+ class Sampler
+ # interval - The sampling interval in seconds.
+ def initialize(interval = 15)
+ @interval = interval
+ @metrics = []
+
+ @last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
+ @last_major_gc = Delta.new(GC.stat[:major_gc_count])
+
+ if Gitlab::Metrics.mri?
+ require 'allocations'
+
+ Allocations.start
+ end
+ end
+
+ def start
+ Thread.new do
+ Thread.current.abort_on_exception = true
+
+ loop do
+ sleep(@interval)
+
+ sample
+ end
+ end
+ end
+
+ def sample
+ sample_memory_usage
+ sample_file_descriptors
+ sample_objects
+ sample_gc
+
+ flush
+ ensure
+ GC::Profiler.clear
+ @metrics.clear
+ end
+
+ def flush
+ Metrics.submit_metrics(@metrics.map(&:to_hash))
+ end
+
+ def sample_memory_usage
+ add_metric('memory_usage', value: System.memory_usage)
+ end
+
+ def sample_file_descriptors
+ add_metric('file_descriptors', value: System.file_descriptor_count)
+ end
+
+ if Metrics.mri?
+ def sample_objects
+ sample = Allocations.to_hash
+ counts = sample.each_with_object({}) do |(klass, count), hash|
+ hash[klass.name] = count
+ end
+
+ # Symbols aren't allocated so we'll need to add those manually.
+ counts['Symbol'] = Symbol.all_symbols.length
+
+ counts.each do |name, count|
+ add_metric('object_counts', { count: count }, type: name)
+ end
+ end
+ else
+ def sample_objects
+ end
+ end
+
+ def sample_gc
+ time = GC::Profiler.total_time * 1000.0
+ stats = GC.stat.merge(total_time: time)
+
+ # We want the difference of GC runs compared to the last sample, not the
+ # total amount since the process started.
+ stats[:minor_gc_count] =
+ @last_minor_gc.compared_with(stats[:minor_gc_count])
+
+ stats[:major_gc_count] =
+ @last_major_gc.compared_with(stats[:major_gc_count])
+
+ stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
+
+ add_metric('gc_statistics', stats)
+ end
+
+ def add_metric(series, values, tags = {})
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def sidekiq?
+ Sidekiq.server?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
new file mode 100644
index 00000000000..ad441decfa2
--- /dev/null
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Metrics
+ # Sidekiq middleware for tracking jobs.
+ #
+ # This middleware is intended to be used as a server-side middleware.
+ class SidekiqMiddleware
+ def call(worker, message, queue)
+ trans = Transaction.new
+
+ begin
+ trans.run { yield }
+ ensure
+ tag_worker(trans, worker)
+ trans.finish
+ end
+ end
+
+ def tag_worker(trans, worker)
+ trans.add_tag(:action, "#{worker.class.name}#perform")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
new file mode 100644
index 00000000000..7c0105d543a
--- /dev/null
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module Metrics
+ module Subscribers
+ # Class for tracking the rendering timings of views.
+ class ActionView < ActiveSupport::Subscriber
+ attach_to :action_view
+
+ SERIES = 'views'
+
+ def render_template(event)
+ track(event) if current_transaction
+ end
+
+ alias_method :render_view, :render_template
+
+ private
+
+ def track(event)
+ values = values_for(event)
+ tags = tags_for(event)
+
+ current_transaction.increment(:view_duration, event.duration)
+ current_transaction.add_metric(SERIES, values, tags)
+ end
+
+ def relative_path(path)
+ path.gsub(/^#{Rails.root.to_s}\/?/, '')
+ end
+
+ def values_for(event)
+ { duration: event.duration }
+ end
+
+ def tags_for(event)
+ path = relative_path(event.payload[:identifier])
+ tags = { view: path }
+
+ file, line = Metrics.last_relative_application_frame
+
+ if file and line
+ tags[:file] = file
+ tags[:line] = line
+ end
+
+ tags
+ end
+
+ def current_transaction
+ Transaction.current
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
new file mode 100644
index 00000000000..8008b3bc895
--- /dev/null
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Metrics
+ module Subscribers
+ # Class for tracking the total query duration of a transaction.
+ class ActiveRecord < ActiveSupport::Subscriber
+ attach_to :active_record
+
+ def sql(event)
+ return unless current_transaction
+
+ current_transaction.increment(:sql_duration, event.duration)
+ end
+
+ private
+
+ def current_transaction
+ Transaction.current
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
new file mode 100644
index 00000000000..83371265278
--- /dev/null
+++ b/lib/gitlab/metrics/system.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module Metrics
+ # Module for gathering system/process statistics such as the memory usage.
+ #
+ # This module relies on the /proc filesystem being available. If /proc is
+ # not available the methods of this module will be stubbed.
+ module System
+ if File.exist?('/proc')
+ # Returns the current process' memory usage in bytes.
+ def self.memory_usage
+ mem = 0
+ match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
+
+ if match and match[1]
+ mem = match[1].to_f * 1024
+ end
+
+ mem
+ end
+
+ def self.file_descriptor_count
+ Dir.glob('/proc/self/fd/*').length
+ end
+ else
+ def self.memory_usage
+ 0.0
+ end
+
+ def self.file_descriptor_count
+ 0
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
new file mode 100644
index 00000000000..68b86de0655
--- /dev/null
+++ b/lib/gitlab/metrics/transaction.rb
@@ -0,0 +1,79 @@
+module Gitlab
+ module Metrics
+ # Class for storing metrics information of a single transaction.
+ class Transaction
+ THREAD_KEY = :_gitlab_metrics_transaction
+
+ attr_reader :uuid, :tags
+
+ def self.current
+ Thread.current[THREAD_KEY]
+ end
+
+ def initialize
+ @metrics = []
+ @uuid = SecureRandom.uuid
+
+ @started_at = nil
+ @finished_at = nil
+
+ @values = Hash.new(0)
+ @tags = {}
+ end
+
+ def duration
+ @finished_at ? (@finished_at - @started_at) * 1000.0 : 0.0
+ end
+
+ def run
+ Thread.current[THREAD_KEY] = self
+
+ @started_at = Time.now
+
+ yield
+ ensure
+ @finished_at = Time.now
+
+ Thread.current[THREAD_KEY] = nil
+ end
+
+ def add_metric(series, values, tags = {})
+ tags = tags.merge(transaction_id: @uuid)
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def increment(name, value)
+ @values[name] += value
+ end
+
+ def add_tag(key, value)
+ @tags[key] = value
+ end
+
+ def finish
+ track_self
+ submit
+ end
+
+ def track_self
+ values = { duration: duration }
+
+ @values.each do |name, value|
+ values[name] = value
+ end
+
+ add_metric('transactions', values, @tags)
+ end
+
+ def submit
+ Metrics.submit_metrics(@metrics.map(&:to_hash))
+ end
+
+ def sidekiq?
+ Sidekiq.server?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb
new file mode 100644
index 00000000000..70e7f25d518
--- /dev/null
+++ b/lib/gitlab/recaptcha.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module Recaptcha
+ def self.load_configurations!
+ if current_application_settings.recaptcha_enabled
+ ::Recaptcha.configure do |config|
+ config.public_key = current_application_settings.recaptcha_site_key
+ config.private_key = current_application_settings.recaptcha_private_key
+ end
+
+ true
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index c87068051ab..4164e998dd1 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -3,11 +3,12 @@ require 'banzai'
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
- attr_accessor :project, :current_user
+ attr_accessor :project, :current_user, :author
- def initialize(project, current_user = nil)
+ def initialize(project, current_user = nil, author = nil)
@project = project
@current_user = current_user
+ @author = author
@references = {}
@@ -20,18 +21,22 @@ module Gitlab
%i(user label milestone merge_request snippet commit commit_range).each do |type|
define_method("#{type}s") do
- @references[type] ||= references(type, project: project, current_user: current_user)
+ @references[type] ||= references(type, reference_context)
end
end
def issues
- options = { project: project, current_user: current_user }
-
if project && project.jira_tracker?
- @references[:external_issue] ||= references(:external_issue, options)
+ @references[:external_issue] ||= references(:external_issue, reference_context)
else
- @references[:issue] ||= references(:issue, options)
+ @references[:issue] ||= references(:issue, reference_context)
end
end
+
+ private
+
+ def reference_context
+ { project: project, current_user: current_user, author: author }
+ end
end
end
diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb
index 15824a1c67f..80a418feb3e 100644
--- a/spec/controllers/abuse_reports_controller_spec.rb
+++ b/spec/controllers/abuse_reports_controller_spec.rb
@@ -1,76 +1,46 @@
require 'spec_helper'
describe AbuseReportsController do
- let(:reporter) { create(:user) }
- let(:user) { create(:user) }
- let(:message) { "This user is a spammer" }
+ let(:reporter) { create(:user) }
+ let(:user) { create(:user) }
+ let(:attrs) do
+ attributes_for(:abuse_report) do |hash|
+ hash[:user_id] = user.id
+ end
+ end
before do
sign_in(reporter)
end
- describe "POST create" do
- context "with admin notification email set" do
- let(:admin_email) { "admin@example.com"}
-
- before(:each) do
- stub_application_setting(admin_notification_email: admin_email)
+ describe 'POST create' do
+ context 'with valid attributes' do
+ it 'saves the abuse report' do
+ expect do
+ post :create, abuse_report: attrs
+ end.to change { AbuseReport.count }.by(1)
end
- it "sends a notification email" do
- perform_enqueued_jobs do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
-
- email = ActionMailer::Base.deliveries.last
+ it 'calls notify' do
+ expect_any_instance_of(AbuseReport).to receive(:notify)
- expect(email.to).to eq([admin_email])
- expect(email.subject).to include(user.username)
- expect(email.text_part.body).to include(message)
- end
+ post :create, abuse_report: attrs
end
- it "saves the abuse report" do
- perform_enqueued_jobs do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.to change { AbuseReport.count }.by(1)
- end
- end
- end
+ it 'redirects back to the reported user' do
+ post :create, abuse_report: attrs
- context "without admin notification email set" do
- before(:each) do
- stub_application_setting(admin_notification_email: nil)
+ expect(response).to redirect_to user
end
+ end
- it "does not send a notification email" do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.not_to change { ActionMailer::Base.deliveries.count }
- end
+ context 'with invalid attributes' do
+ it 'renders new' do
+ attrs.delete(:user_id)
+ post :create, abuse_report: attrs
- it "saves the abuse report" do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.to change { AbuseReport.count }.by(1)
+ expect(response).to render_template(:new)
end
end
end
-
end
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index 72764b1629d..b955d0b0c46 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -1,69 +1,98 @@
require 'spec_helper'
-describe "Admin Builds" do
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:build) { FactoryGirl.create :ci_build, commit: commit }
-
+describe 'Admin Builds' do
before do
login_as :admin
end
- describe "GET /admin/builds" do
- before do
- build
- visit admin_builds_path
- end
-
- it { expect(page).to have_content "Running" }
- it { expect(page).to have_content build.short_sha }
- end
+ describe 'GET /admin/builds' do
+ let(:commit) { create(:ci_commit) }
- describe "Tabs" do
- it "shows all builds" do
- FactoryGirl.create :ci_build, commit: commit, status: "pending"
- FactoryGirl.create :ci_build, commit: commit, status: "running"
- FactoryGirl.create :ci_build, commit: commit, status: "success"
- FactoryGirl.create :ci_build, commit: commit, status: "failed"
+ context 'All tab' do
+ context 'when have builds' do
+ it 'shows all builds' do
+ create(:ci_build, commit: commit, status: :pending)
+ create(:ci_build, commit: commit, status: :running)
+ create(:ci_build, commit: commit, status: :success)
+ create(:ci_build, commit: commit, status: :failed)
- visit admin_builds_path
+ visit admin_builds_path
- within ".center-top-menu" do
- click_on "All"
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'All')
+ expect(page.all('.build-link').size).to eq(4)
+ expect(page).to have_link 'Cancel all'
+ end
end
- expect(page.all(".build-link").size).to eq(4)
+ context 'when have no builds' do
+ it 'shows a message' do
+ visit admin_builds_path
+
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'All')
+ expect(page).to have_content 'No builds to show'
+ expect(page).not_to have_link 'Cancel all'
+ end
+ end
end
- it "shows finished builds" do
- build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
- build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
- build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
+ context 'Running tab' do
+ context 'when have running builds' do
+ it 'shows running builds' do
+ build1 = create(:ci_build, commit: commit, status: :pending)
+ build2 = create(:ci_build, commit: commit, status: :success)
+ build3 = create(:ci_build, commit: commit, status: :failed)
+
+ visit admin_builds_path(scope: :running)
+
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running')
+ expect(page.find('.build-link')).to have_content(build1.id)
+ expect(page.find('.build-link')).not_to have_content(build2.id)
+ expect(page.find('.build-link')).not_to have_content(build3.id)
+ expect(page).to have_link 'Cancel all'
+ end
+ end
- visit admin_builds_path
+ context 'when have no builds running' do
+ it 'shows a message' do
+ create(:ci_build, commit: commit, status: :success)
- within ".center-top-menu" do
- click_on "Finished"
- end
+ visit admin_builds_path(scope: :running)
- expect(page.find(".build-link")).not_to have_content(build.id)
- expect(page.find(".build-link")).not_to have_content(build1.id)
- expect(page.find(".build-link")).to have_content(build2.id)
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running')
+ expect(page).to have_content 'No builds to show'
+ expect(page).not_to have_link 'Cancel all'
+ end
+ end
end
- it "shows running builds" do
- build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
- build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
- build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
+ context 'Finished tab' do
+ context 'when have finished builds' do
+ it 'shows finished builds' do
+ build1 = create(:ci_build, commit: commit, status: :pending)
+ build2 = create(:ci_build, commit: commit, status: :running)
+ build3 = create(:ci_build, commit: commit, status: :success)
+
+ visit admin_builds_path(scope: :finished)
+
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished')
+ expect(page.find('.build-link')).not_to have_content(build1.id)
+ expect(page.find('.build-link')).not_to have_content(build2.id)
+ expect(page.find('.build-link')).to have_content(build3.id)
+ expect(page).to have_link 'Cancel all'
+ end
+ end
- visit admin_builds_path
+ context 'when have no builds finished' do
+ it 'shows a message' do
+ create(:ci_build, commit: commit, status: :running)
- within ".center-top-menu" do
- click_on "Running"
- end
+ visit admin_builds_path(scope: :finished)
- expect(page.find(".build-link")).to have_content(build.id)
- expect(page.find(".build-link")).not_to have_content(build2.id)
- expect(page.find(".build-link")).not_to have_content(build3.id)
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished')
+ expect(page).to have_content 'No builds to show'
+ expect(page).to have_link 'Cancel all'
+ end
+ end
end
end
end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index f0031a0a247..240e56839df 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -15,11 +15,11 @@ describe "Builds" do
context "Running scope" do
before do
@build.run!
- visit namespace_project_builds_path(@project.namespace, @project)
+ visit namespace_project_builds_path(@project.namespace, @project, scope: :running)
end
- it { expect(page).to have_content 'Running' }
- it { expect(page).to have_content 'Cancel running' }
+ it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running') }
+ it { expect(page).to have_link 'Cancel running' }
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 }
@@ -31,21 +31,22 @@ describe "Builds" do
visit namespace_project_builds_path(@project.namespace, @project, scope: :finished)
end
+ it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished') }
it { expect(page).to have_content 'No builds to show' }
- it { expect(page).to have_content 'Cancel running' }
+ it { expect(page).to have_link 'Cancel running' }
end
context "All builds" do
before do
@project.builds.running_or_pending.each(&:success)
- visit namespace_project_builds_path(@project.namespace, @project, scope: :all)
+ visit namespace_project_builds_path(@project.namespace, @project)
end
- it { expect(page).to have_content 'All' }
+ it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') }
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 }
- it { expect(page).to_not have_content 'Cancel running' }
+ it { expect(page).to_not have_link 'Cancel running' }
end
end
@@ -56,8 +57,12 @@ describe "Builds" do
click_link "Cancel running"
end
- it { expect(page).to have_content 'No builds to show' }
- it { expect(page).to_not have_content 'Cancel running' }
+ it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') }
+ it { expect(page).to have_content 'canceled' }
+ 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 }
+ it { expect(page).to_not have_link 'Cancel running' }
end
describe "GET /:project/builds/:id" do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 74b148f5d17..9a01c89ae2a 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -80,8 +80,10 @@ feature 'Project', feature: true do
visit namespace_project_path(project.namespace, project)
end
- it { expect(page).to have_content('You have Master access to this project.') }
- it { expect(page).to have_link('Leave this project') }
+ it 'click project-settings and find leave project' do
+ find('#project-settings-button').click
+ expect(page).to have_link('Leave Project')
+ end
end
def remove_with_confirm(button_text, confirm_with)
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
deleted file mode 100644
index 4f6a000822e..00000000000
--- a/spec/finders/groups_finder_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe GroupsFinder do
- describe '#execute' do
- let(:user) { create(:user) }
-
- let(:group1) { create(:group) }
- let(:group2) { create(:group) }
- let(:group3) { create(:group) }
- let(:group4) { create(:group, public: true) }
-
- let!(:public_project) { create(:project, :public, group: group1) }
- let!(:internal_project) { create(:project, :internal, group: group2) }
- let!(:private_project) { create(:project, :private, group: group3) }
-
- let(:finder) { described_class.new }
-
- describe 'with a user' do
- subject { finder.execute(user) }
-
- describe 'when the user is not a member of any groups' do
- it { is_expected.to eq([group4, group2, group1]) }
- end
-
- describe 'when the user is a member of a group' do
- before do
- group3.add_user(user, Gitlab::Access::DEVELOPER)
- end
-
- it { is_expected.to eq([group4, group3, group2, group1]) }
- end
-
- describe 'when the user is a member of a private project' do
- before do
- private_project.team.add_user(user, Gitlab::Access::DEVELOPER)
- end
-
- it { is_expected.to eq([group4, group3, group2, group1]) }
- end
- end
-
- describe 'without a user' do
- subject { finder.execute }
-
- it { is_expected.to eq([group4, group1]) }
- end
- end
-end
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
deleted file mode 100644
index 2d9068cc720..00000000000
--- a/spec/finders/joined_groups_finder_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'spec_helper'
-
-describe JoinedGroupsFinder do
- describe '#execute' do
- let(:source_user) { create(:user) }
- let(:current_user) { create(:user) }
-
- let(:group1) { create(:group) }
- let(:group2) { create(:group) }
- let(:group3) { create(:group) }
- let(:group4) { create(:group, public: true) }
-
- let!(:public_project) { create(:project, :public, group: group1) }
- let!(:internal_project) { create(:project, :internal, group: group2) }
- let!(:private_project) { create(:project, :private, group: group3) }
-
- let(:finder) { described_class.new(source_user) }
-
- before do
- [group1, group2, group3, group4].each do |group|
- group.add_user(source_user, Gitlab::Access::MASTER)
- end
- end
-
- describe 'with a current user' do
- describe 'when the current user has access to the projects of the source user' do
- before do
- private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER)
- end
-
- subject { finder.execute(current_user) }
-
- it { is_expected.to eq([group4, group3, group2, group1]) }
- end
-
- describe 'when the current user does not have access to the projects of the source user' do
- subject { finder.execute(current_user) }
-
- it { is_expected.to eq([group4, group2, group1]) }
- end
- end
-
- describe 'without a current user' do
- subject { finder.execute }
-
- it { is_expected.to eq([group4, group1]) }
- end
- end
-end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 04e795025d2..ffd8ebae029 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -141,4 +141,11 @@ describe IssuesHelper do
expect(note_active_class(Note.all, @note.author)).to eq("active")
end
end
+
+ describe "#awards_sort" do
+ it "sorts a hash so thumbsup and thumbsdown are always on top" do
+ data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" }
+ expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"])
+ end
+ end
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
new file mode 100644
index 00000000000..fd7107779f6
--- /dev/null
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -0,0 +1,129 @@
+require 'rails_helper'
+
+describe PageLayoutHelper do
+ describe 'page_description' do
+ it 'defaults to value returned by page_description_default helper' do
+ allow(helper).to receive(:page_description_default).and_return('Foo')
+
+ expect(helper.page_description).to eq 'Foo'
+ end
+
+ it 'returns the last-pushed description' do
+ helper.page_description('Foo')
+ helper.page_description('Bar')
+ helper.page_description('Baz')
+
+ expect(helper.page_description).to eq 'Baz'
+ end
+
+ it 'squishes multiple newlines' do
+ helper.page_description("Foo\nBar\nBaz")
+
+ expect(helper.page_description).to eq 'Foo Bar Baz'
+ end
+
+ it 'truncates' do
+ helper.page_description <<-LOREM.strip_heredoc
+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo
+ ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis
+ dis parturient montes, nascetur ridiculus mus. Donec quam felis,
+ ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa
+ quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget,
+ arcu.
+ LOREM
+
+ expect(helper.page_description).to end_with 'quam felis,...'
+ end
+
+ it 'sanitizes all HTML' do
+ helper.page_description("<b>Bold</b> <h1>Header</h1>")
+
+ expect(helper.page_description).to eq 'Bold Header'
+ end
+ end
+
+ describe 'page_description_default' do
+ it 'uses Project description when available' do
+ project = double(description: 'Project Description')
+ helper.instance_variable_set(:@project, project)
+
+ expect(helper.page_description_default).to eq 'Project Description'
+ end
+
+ it 'uses brand_title when Project description is nil' do
+ project = double(description: nil)
+ helper.instance_variable_set(:@project, project)
+
+ expect(helper).to receive(:brand_title).and_return('Brand Title')
+ expect(helper.page_description_default).to eq 'Brand Title'
+ end
+
+ it 'falls back to brand_title' do
+ allow(helper).to receive(:brand_title).and_return('Brand Title')
+
+ expect(helper.page_description_default).to eq 'Brand Title'
+ end
+ end
+
+ describe 'page_image' do
+ it 'defaults to the GitLab logo' do
+ expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ end
+
+ context 'with @project' do
+ it 'uses Project avatar if available' do
+ project = double(avatar_url: 'http://example.com/uploads/avatar.png')
+ helper.instance_variable_set(:@project, project)
+
+ expect(helper.page_image).to eq project.avatar_url
+ end
+
+ it 'falls back to the default' do
+ project = double(avatar_url: nil)
+ helper.instance_variable_set(:@project, project)
+
+ expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ end
+ end
+
+ context 'with @user' do
+ it 'delegates to avatar_icon helper' do
+ user = double('User')
+ helper.instance_variable_set(:@user, user)
+
+ expect(helper).to receive(:avatar_icon).with(user)
+
+ helper.page_image
+ end
+ end
+ end
+
+ describe 'page_card_attributes' do
+ it 'raises ArgumentError when given more than two attributes' do
+ map = { foo: 'foo', bar: 'bar', baz: 'baz' }
+
+ expect { helper.page_card_attributes(map) }.
+ to raise_error(ArgumentError, /more than two attributes/)
+ end
+
+ it 'rejects blank values' do
+ map = { foo: 'foo', bar: '' }
+ helper.page_card_attributes(map)
+
+ expect(helper.page_card_attributes).to eq({ foo: 'foo' })
+ end
+ end
+
+ describe 'page_card_meta_tags' do
+ it 'returns the twitter:label and twitter:data tags' do
+ allow(helper).to receive(:page_card_attributes).and_return(foo: 'bar')
+
+ tags = helper.page_card_meta_tags
+
+ aggregate_failures do
+ expect(tags).to include %q(<meta property="twitter:label1" content="foo" />)
+ expect(tags).to include %q(<meta property="twitter:data1" content="bar" />)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index ebe9c29d91c..f0d553f5f1d 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -43,7 +43,7 @@ describe SearchHelper do
end
it "includes the public group" do
- group = create(:group, public: true)
+ group = create(:group)
expect(search_autocomplete_opts(group.name).size).to eq(1)
end
diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml
index 8447dfdda32..470cabeafbb 100644
--- a/spec/javascripts/fixtures/issues_show.html.haml
+++ b/spec/javascripts/fixtures/issues_show.html.haml
@@ -1,4 +1,14 @@
-%a.btn-close
+:css
+ .hidden { display: none !important; }
+
+.flash-container
+ .flash-alert
+ .flash-notice
+
+.status-box.status-box-open Open
+.status-box.status-box-closed.hidden Closed
+%a.btn-close{"href" => "http://gitlab.com/issues/6/close"} Close
+%a.btn-reopen.hidden{"href" => "http://gitlab.com/issues/6/reopen"} Reopen
.detail-page-description
.description.js-task-list-container
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
index 268e4c68c31..7e67c778861 100644
--- a/spec/javascripts/issue_spec.js.coffee
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -20,3 +20,89 @@ describe 'Issue', ->
expect(req.data.issue.description).not.toBe(null)
$('.js-task-list-field').trigger('tasklist:changed')
+describe 'reopen/close issue', ->
+ fixture.preload('issues_show.html')
+ beforeEach ->
+ fixture.load('issues_show.html')
+ @issue = new Issue()
+ it 'closes an issue', ->
+ $.ajax = (obj) ->
+ expect(obj.type).toBe('PUT')
+ expect(obj.url).toBe('http://gitlab.com/issues/6/close')
+ obj.success saved: true
+
+ $btnClose = $('a.btn-close')
+ $btnReopen = $('a.btn-reopen')
+ expect($btnReopen).toBeHidden()
+ expect($btnClose.text()).toBe('Close')
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined')
+
+ $btnClose.trigger('click')
+
+ expect($btnReopen).toBeVisible()
+ expect($btnClose).toBeHidden()
+ expect($('div.status-box-closed')).toBeVisible()
+ expect($('div.status-box-open')).toBeHidden()
+
+ it 'fails to closes an issue with success:false', ->
+
+ $.ajax = (obj) ->
+ expect(obj.type).toBe('PUT')
+ expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
+ obj.success saved: false
+
+ $btnClose = $('a.btn-close')
+ $btnReopen = $('a.btn-reopen')
+ $btnClose.attr('href','http://goesnowhere.nothing/whereami')
+ expect($btnReopen).toBeHidden()
+ expect($btnClose.text()).toBe('Close')
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined')
+
+ $btnClose.trigger('click')
+
+ expect($btnReopen).toBeHidden()
+ expect($btnClose).toBeVisible()
+ expect($('div.status-box-closed')).toBeHidden()
+ expect($('div.status-box-open')).toBeVisible()
+ expect($('div.flash-alert')).toBeVisible()
+ expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
+
+ it 'fails to closes an issue with HTTP error', ->
+
+ $.ajax = (obj) ->
+ expect(obj.type).toBe('PUT')
+ expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
+ obj.error()
+
+ $btnClose = $('a.btn-close')
+ $btnReopen = $('a.btn-reopen')
+ $btnClose.attr('href','http://goesnowhere.nothing/whereami')
+ expect($btnReopen).toBeHidden()
+ expect($btnClose.text()).toBe('Close')
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined')
+
+ $btnClose.trigger('click')
+
+ expect($btnReopen).toBeHidden()
+ expect($btnClose).toBeVisible()
+ expect($('div.status-box-closed')).toBeHidden()
+ expect($('div.status-box-open')).toBeVisible()
+ expect($('div.flash-alert')).toBeVisible()
+ expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
+
+ it 'reopens an issue', ->
+ $.ajax = (obj) ->
+ expect(obj.type).toBe('PUT')
+ expect(obj.url).toBe('http://gitlab.com/issues/6/reopen')
+ obj.success saved: true
+
+ $btnClose = $('a.btn-close')
+ $btnReopen = $('a.btn-reopen')
+ expect($btnReopen.text()).toBe('Reopen')
+
+ $btnReopen.trigger('click')
+
+ expect($btnReopen).toBeHidden()
+ expect($btnClose).toBeVisible()
+ expect($('div.status-box-open')).toBeVisible()
+ expect($('div.status-box-closed')).toBeHidden() \ No newline at end of file
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 0b3e5ecbc9f..0e6685f0ffb 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -92,6 +92,14 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
end
+ it 'rebuilds relative URL for a file in the repository root' do
+ relative_link = link('../README.md')
+ doc = filter(relative_link, requested_path: 'doc/some-file.md')
+
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/README.md"
+ end
+
it 'rebuilds relative URL for a file in the repo with an anchor' do
doc = filter(link('README.md#section'))
expect(doc.at_css('a')['href']).
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 3534bf97784..8bdebae1841 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -37,9 +37,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
.to eq urls.namespace_project_url(project.namespace, project)
end
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}")
- expect(result[:references][:user]).to eq [project.creator]
+ context "when the author is a member of the project" do
+
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Hey #{reference}", author: project.creator)
+ expect(result[:references][:user]).to eq [project.creator]
+ end
+ end
+
+ context "when the author is not a member of the project" do
+
+ let(:other_user) { create(:user) }
+
+ it "doesn't add to the results hash" do
+ result = reference_pipeline_result("Hey #{reference}", author: other_user)
+ expect(result[:references][:user]).to eq []
+ end
end
end
diff --git a/spec/lib/banzai/querying_spec.rb b/spec/lib/banzai/querying_spec.rb
new file mode 100644
index 00000000000..27da2a7439c
--- /dev/null
+++ b/spec/lib/banzai/querying_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Banzai::Querying do
+ describe '.css' do
+ it 'optimizes queries for elements with classes' do
+ document = double(:document)
+
+ expect(document).to receive(:xpath).with(/^descendant::a/)
+
+ described_class.css(document, 'a.gfm')
+ 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 c90133fbf03..d15100fc6d8 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
module Ci
describe GitlabCiYamlProcessor, lib: true do
let(:path) { 'path' }
-
+
describe "#builds_for_ref" do
let(:type) { 'test' }
@@ -29,7 +29,7 @@ module Ci
when: "on_success"
})
end
-
+
describe :only do
it "does not return builds if only has another branch" do
config = YAML.dump({
@@ -517,7 +517,7 @@ module Ci
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
end
- it "returns errors if there is no any jobs defined" do
+ it "returns errors if there are no jobs defined" do
config = YAML.dump({ before_script: ["bundle update"] })
expect do
GitlabCiYamlProcessor.new(config, path)
diff --git a/spec/lib/gitlab/metrics/delta_spec.rb b/spec/lib/gitlab/metrics/delta_spec.rb
new file mode 100644
index 00000000000..718387cdee1
--- /dev/null
+++ b/spec/lib/gitlab/metrics/delta_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Delta do
+ let(:delta) { described_class.new }
+
+ describe '#compared_with' do
+ it 'returns the delta as a Numeric' do
+ expect(delta.compared_with(5)).to eq(5)
+ end
+
+ it 'bases the delta on a previously used value' do
+ expect(delta.compared_with(5)).to eq(5)
+ expect(delta.compared_with(15)).to eq(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
new file mode 100644
index 00000000000..2a37cd40dde
--- /dev/null
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -0,0 +1,240 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Instrumentation do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+
+ before do
+ @dummy = Class.new do
+ def self.foo(text = 'foo')
+ text
+ end
+
+ def bar(text = 'bar')
+ text
+ end
+ end
+
+ allow(@dummy).to receive(:name).and_return('Dummy')
+ end
+
+ describe '.configure' do
+ it 'yields self' do
+ described_class.configure do |c|
+ expect(c).to eq(described_class)
+ end
+ end
+ end
+
+ describe '.instrument_method' do
+ describe 'with metrics enabled' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+
+ described_class.instrument_method(@dummy, :foo)
+ end
+
+ it 'renames the original method' do
+ expect(@dummy).to respond_to(:_original_foo)
+ end
+
+ it 'calls the instrumented method with the correct arguments' do
+ expect(@dummy.foo).to eq('foo')
+ end
+
+ it 'tracks the call duration upon calling the method' do
+ allow(Gitlab::Metrics).to receive(:method_call_threshold).
+ and_return(0)
+
+ 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')
+
+ @dummy.foo
+ end
+
+ it 'does not track method calls below a given duration threshold' do
+ allow(Gitlab::Metrics).to receive(:method_call_threshold).
+ and_return(100)
+
+ expect(transaction).to_not receive(:add_metric)
+
+ @dummy.foo
+ end
+ end
+
+ describe 'with metrics disabled' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(false)
+ end
+
+ it 'does not instrument the method' do
+ described_class.instrument_method(@dummy, :foo)
+
+ expect(@dummy).to_not respond_to(:_original_foo)
+ end
+ end
+ end
+
+ describe '.instrument_instance_method' do
+ describe 'with metrics enabled' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+
+ described_class.
+ instrument_instance_method(@dummy, :bar)
+ end
+
+ it 'renames the original method' do
+ expect(@dummy.method_defined?(:_original_bar)).to eq(true)
+ end
+
+ it 'calls the instrumented method with the correct arguments' do
+ expect(@dummy.new.bar).to eq('bar')
+ end
+
+ it 'tracks the call duration upon calling the method' do
+ allow(Gitlab::Metrics).to receive(:method_call_threshold).
+ and_return(0)
+
+ 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')
+
+ @dummy.new.bar
+ end
+
+ it 'does not track method calls below a given duration threshold' do
+ allow(Gitlab::Metrics).to receive(:method_call_threshold).
+ and_return(100)
+
+ expect(transaction).to_not receive(:add_metric)
+
+ @dummy.new.bar
+ end
+ end
+
+ describe 'with metrics disabled' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(false)
+ end
+
+ it 'does not instrument the method' do
+ described_class.
+ instrument_instance_method(@dummy, :bar)
+
+ expect(@dummy.method_defined?(:_original_bar)).to eq(false)
+ end
+ end
+ end
+
+ describe '.instrument_class_hierarchy' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+
+ @child1 = Class.new(@dummy) do
+ def self.child1_foo; end
+ def child1_bar; end
+ end
+
+ @child2 = Class.new(@child1) do
+ def self.child2_foo; end
+ def child2_bar; end
+ end
+ end
+
+ it 'recursively instruments a class hierarchy' do
+ described_class.instrument_class_hierarchy(@dummy)
+
+ expect(@child1).to respond_to(:_original_child1_foo)
+ expect(@child2).to respond_to(:_original_child2_foo)
+
+ expect(@child1.method_defined?(:_original_child1_bar)).to eq(true)
+ expect(@child2.method_defined?(:_original_child2_bar)).to eq(true)
+ end
+
+ it 'does not instrument the root module' do
+ described_class.instrument_class_hierarchy(@dummy)
+
+ expect(@dummy).to_not respond_to(:_original_foo)
+ expect(@dummy.method_defined?(:_original_bar)).to eq(false)
+ end
+ end
+
+ describe '.instrument_methods' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+ end
+
+ it 'instruments all public class methods' do
+ described_class.instrument_methods(@dummy)
+
+ expect(@dummy).to respond_to(:_original_foo)
+ end
+
+ it 'only instruments methods directly defined in the module' do
+ mod = Module.new do
+ def kittens
+ end
+ end
+
+ @dummy.extend(mod)
+
+ described_class.instrument_methods(@dummy)
+
+ expect(@dummy).to_not respond_to(:_original_kittens)
+ end
+
+ it 'can take a block to determine if a method should be instrumented' do
+ described_class.instrument_methods(@dummy) do
+ false
+ end
+
+ expect(@dummy).to_not respond_to(:_original_foo)
+ end
+ end
+
+ describe '.instrument_instance_methods' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+ end
+
+ it 'instruments all public instance methods' do
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(@dummy.method_defined?(:_original_bar)).to eq(true)
+ end
+
+ it 'only instruments methods directly defined in the module' do
+ mod = Module.new do
+ def kittens
+ end
+ end
+
+ @dummy.include(mod)
+
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(@dummy.method_defined?(:_original_kittens)).to eq(false)
+ end
+
+ it 'can take a block to determine if a method should be instrumented' do
+ described_class.instrument_instance_methods(@dummy) do
+ false
+ end
+
+ expect(@dummy.method_defined?(:_original_bar)).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
new file mode 100644
index 00000000000..f718d536130
--- /dev/null
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Metric do
+ let(:metric) do
+ described_class.new('foo', { number: 10 }, { host: 'localtoast' })
+ end
+
+ describe '#series' do
+ subject { metric.series }
+
+ it { is_expected.to eq('foo') }
+ end
+
+ describe '#values' do
+ subject { metric.values }
+
+ it { is_expected.to eq({ number: 10 }) }
+ end
+
+ describe '#tags' do
+ subject { metric.tags }
+
+ it { is_expected.to eq({ host: 'localtoast' }) }
+ end
+
+ describe '#to_hash' do
+ it 'returns a Hash' do
+ expect(metric.to_hash).to be_an_instance_of(Hash)
+ end
+
+ describe 'the returned Hash' do
+ let(:hash) { metric.to_hash }
+
+ it 'includes the series' do
+ expect(hash[:series]).to eq('foo')
+ end
+
+ it 'includes the tags' do
+ expect(hash[:tags]).to be_an_instance_of(Hash)
+ end
+
+ it 'includes the values' do
+ expect(hash[:values]).to eq({ number: 10 })
+ end
+
+ it 'includes the timestamp' do
+ expect(hash[:timestamp]).to be_an_instance_of(Fixnum)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
new file mode 100644
index 00000000000..a143fe4cfcd
--- /dev/null
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::RackMiddleware do
+ let(:app) { double(:app) }
+
+ let(:middleware) { described_class.new(app) }
+
+ let(:env) { { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/foo' } }
+
+ describe '#call' do
+ before do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
+ end
+
+ it 'tracks a transaction' do
+ expect(app).to receive(:call).with(env).and_return('yay')
+
+ expect(middleware.call(env)).to eq('yay')
+ end
+
+ it 'tags a transaction with the name and action of a controller' do
+ klass = double(:klass, name: 'TestController')
+ controller = double(:controller, class: klass, action_name: 'show')
+
+ env['action_controller.instance'] = controller
+
+ allow(app).to receive(:call).with(env)
+
+ expect(middleware).to receive(:tag_controller).
+ with(an_instance_of(Gitlab::Metrics::Transaction), env)
+
+ middleware.call(env)
+ end
+ end
+
+ describe '#transaction_from_env' do
+ let(:transaction) { middleware.transaction_from_env(env) }
+
+ it 'returns a Transaction' do
+ expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction)
+ end
+
+ it 'tags the transaction with the request method and URI' do
+ expect(transaction.tags[:request_method]).to eq('GET')
+ expect(transaction.tags[:request_uri]).to eq('/foo')
+ end
+ end
+
+ describe '#tag_controller' do
+ let(:transaction) { middleware.transaction_from_env(env) }
+
+ it 'tags a transaction with the name and action of a controller' do
+ klass = double(:klass, name: 'TestController')
+ controller = double(:controller, class: klass, action_name: 'show')
+
+ env['action_controller.instance'] = controller
+
+ middleware.tag_controller(transaction, env)
+
+ expect(transaction.tags[:action]).to eq('TestController#show')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb
new file mode 100644
index 00000000000..27211350fbe
--- /dev/null
+++ b/spec/lib/gitlab/metrics/sampler_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Sampler do
+ let(:sampler) { described_class.new(5) }
+
+ after do
+ Allocations.stop if Gitlab::Metrics.mri?
+ end
+
+ describe '#start' do
+ it 'gathers a sample at a given interval' do
+ expect(sampler).to receive(:sleep).with(5)
+ expect(sampler).to receive(:sample)
+ expect(sampler).to receive(:loop).and_yield
+
+ sampler.start.join
+ end
+ end
+
+ describe '#sample' do
+ it 'samples various statistics' do
+ expect(sampler).to receive(:sample_memory_usage)
+ expect(sampler).to receive(:sample_file_descriptors)
+ expect(sampler).to receive(:sample_objects)
+ expect(sampler).to receive(:sample_gc)
+ expect(sampler).to receive(:flush)
+
+ sampler.sample
+ end
+
+ it 'clears any GC profiles' do
+ expect(sampler).to receive(:flush)
+ expect(GC::Profiler).to receive(:clear)
+
+ sampler.sample
+ end
+ end
+
+ describe '#flush' do
+ it 'schedules the metrics using Sidekiq' do
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
+ with([an_instance_of(Hash)])
+
+ sampler.sample_memory_usage
+ sampler.flush
+ end
+ end
+
+ describe '#sample_memory_usage' do
+ it 'adds a metric containing the memory usage' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage).
+ and_return(9000)
+
+ expect(sampler).to receive(:add_metric).
+ with(/memory_usage/, value: 9000).
+ and_call_original
+
+ sampler.sample_memory_usage
+ end
+ end
+
+ describe '#sample_file_descriptors' do
+ it 'adds a metric containing the amount of open file descriptors' do
+ expect(Gitlab::Metrics::System).to receive(:file_descriptor_count).
+ and_return(4)
+
+ expect(sampler).to receive(:add_metric).
+ with(/file_descriptors/, value: 4).
+ and_call_original
+
+ sampler.sample_file_descriptors
+ end
+ end
+
+ describe '#sample_objects' do
+ it 'adds a metric containing the amount of allocated objects' do
+ expect(sampler).to receive(:add_metric).
+ with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
+ at_least(:once).
+ and_call_original
+
+ sampler.sample_objects
+ end
+ end
+
+ describe '#sample_gc' do
+ it 'adds a metric containing garbage collection statistics' do
+ expect(GC::Profiler).to receive(:total_time).and_return(0.24)
+
+ expect(sampler).to receive(:add_metric).
+ with(/gc_statistics/, an_instance_of(Hash)).
+ and_call_original
+
+ sampler.sample_gc
+ end
+ end
+
+ describe '#add_metric' do
+ it 'prefixes the series name for a Rails process' do
+ expect(sampler).to receive(:sidekiq?).and_return(false)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('rails_cats', { value: 10 }, {}).
+ and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+
+ it 'prefixes the series name for a Sidekiq process' do
+ expect(sampler).to receive(:sidekiq?).and_return(true)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('sidekiq_cats', { value: 10 }, {}).
+ and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
new file mode 100644
index 00000000000..5882e7d81c7
--- /dev/null
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::SidekiqMiddleware do
+ let(:middleware) { described_class.new }
+
+ describe '#call' do
+ it 'tracks the transaction' do
+ worker = Class.new.new
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
+
+ middleware.call(worker, 'test', :test) { nil }
+ end
+ end
+
+ describe '#tag_worker' do
+ it 'adds the worker class and action to the transaction' do
+ trans = Gitlab::Metrics::Transaction.new
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
+
+ expect(trans).to receive(:add_tag).with(:action, 'TestWorker#perform')
+
+ middleware.tag_worker(trans, worker)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
new file mode 100644
index 00000000000..05e4fbbeb51
--- /dev/null
+++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Subscribers::ActionView do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+
+ let(:subscriber) { described_class.new }
+
+ let(:event) do
+ root = Rails.root.to_s
+
+ double(:event, duration: 2.1,
+ payload: { identifier: "#{root}/app/views/x.html.haml" })
+ end
+
+ before do
+ allow(subscriber).to receive(:current_transaction).and_return(transaction)
+
+ allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
+ and_return(['app/views/x.html.haml', 4])
+ end
+
+ describe '#render_template' do
+ it 'tracks rendering of a template' do
+ values = { duration: 2.1 }
+ tags = {
+ view: 'app/views/x.html.haml',
+ file: 'app/views/x.html.haml',
+ line: 4
+ }
+
+ expect(transaction).to receive(:increment).
+ with(:view_duration, 2.1)
+
+ expect(transaction).to receive(:add_metric).
+ with(described_class::SERIES, values, tags)
+
+ subscriber.render_template(event)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
new file mode 100644
index 00000000000..7bc070a4d09
--- /dev/null
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Subscribers::ActiveRecord do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:subscriber) { described_class.new }
+
+ let(:event) do
+ double(:event, duration: 0.2,
+ payload: { sql: 'SELECT * FROM users WHERE id = 10' })
+ end
+
+ describe '#sql' do
+ describe 'without a current transaction' do
+ it 'simply returns' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to_not receive(:increment)
+
+ subscriber.sql(event)
+ end
+ end
+
+ describe 'with a current transaction' do
+ it 'increments the :sql_duration value' do
+ expect(subscriber).to receive(:current_transaction).
+ at_least(:once).
+ and_return(transaction)
+
+ expect(transaction).to receive(:increment).
+ with(:sql_duration, 0.2)
+
+ subscriber.sql(event)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
new file mode 100644
index 00000000000..f8c1d956ca1
--- /dev/null
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::System do
+ if File.exist?('/proc')
+ describe '.memory_usage' do
+ it "returns the process' memory usage in bytes" do
+ expect(described_class.memory_usage).to be > 0
+ end
+ end
+
+ describe '.file_descriptor_count' do
+ it 'returns the amount of open file descriptors' do
+ expect(described_class.file_descriptor_count).to be > 0
+ end
+ end
+ else
+ describe '.memory_usage' do
+ it 'returns 0.0' do
+ expect(described_class.memory_usage).to eq(0.0)
+ end
+ end
+
+ describe '.file_descriptor_count' do
+ it 'returns 0' do
+ expect(described_class.file_descriptor_count).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
new file mode 100644
index 00000000000..b9b94947afa
--- /dev/null
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Transaction do
+ let(:transaction) { described_class.new }
+
+ describe '#duration' do
+ it 'returns the duration of a transaction in seconds' do
+ transaction.run { sleep(0.5) }
+
+ expect(transaction.duration).to be >= 0.5
+ end
+ end
+
+ describe '#run' do
+ it 'yields the supplied block' do
+ expect { |b| transaction.run(&b) }.to yield_control
+ end
+
+ it 'stores the transaction in the current thread' do
+ transaction.run do
+ expect(Thread.current[described_class::THREAD_KEY]).to eq(transaction)
+ end
+ end
+
+ it 'removes the transaction from the current thread upon completion' do
+ transaction.run { }
+
+ expect(Thread.current[described_class::THREAD_KEY]).to be_nil
+ end
+ end
+
+ describe '#add_metric' do
+ it 'adds a metric tagged with the transaction UUID' do
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('rails_foo', { number: 10 }, { transaction_id: transaction.uuid })
+
+ transaction.add_metric('foo', number: 10)
+ end
+ end
+
+ describe '#increment' do
+ it 'increments a counter' do
+ transaction.increment(:time, 1)
+ transaction.increment(:time, 2)
+
+ expect(transaction).to receive(:add_metric).
+ with('transactions', { duration: 0.0, time: 3 }, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#add_tag' do
+ it 'adds a tag' do
+ transaction.add_tag(:foo, 'bar')
+
+ expect(transaction.tags).to eq({ foo: 'bar' })
+ end
+ end
+
+ describe '#finish' do
+ it 'tracks the transaction details and submits them to Sidekiq' do
+ expect(transaction).to receive(:track_self)
+ expect(transaction).to receive(:submit)
+
+ transaction.finish
+ end
+ end
+
+ describe '#track_self' do
+ it 'adds a metric for the transaction itself' do
+ expect(transaction).to receive(:add_metric).
+ with('transactions', { duration: transaction.duration }, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#submit' do
+ it 'submits the metrics to Sidekiq' do
+ transaction.track_self
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
+ with([an_instance_of(Hash)])
+
+ transaction.submit
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
new file mode 100644
index 00000000000..c2782f95c8e
--- /dev/null
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics do
+ describe '.settings' do
+ it 'returns a Hash' do
+ expect(described_class.settings).to be_an_instance_of(Hash)
+ end
+ end
+
+ describe '.enabled?' do
+ it 'returns a boolean' do
+ expect([true, false].include?(described_class.enabled?)).to eq(true)
+ end
+ end
+
+ describe '.last_relative_application_frame' do
+ it 'returns an Array containing a file path and line number' do
+ file, line = described_class.last_relative_application_frame
+
+ expect(line).to eq(__LINE__ - 2)
+ expect(file).to eq('spec/lib/gitlab/metrics_spec.rb')
+ end
+ end
+
+ describe '#submit_metrics' do
+ it 'prepares and writes the metrics to InfluxDB' do
+ connection = double(:connection)
+ pool = double(:pool)
+
+ expect(pool).to receive(:with).and_yield(connection)
+ expect(connection).to receive(:write_points).with(an_instance_of(Array))
+ expect(Gitlab::Metrics).to receive(:pool).and_return(pool)
+
+ described_class.submit_metrics([{ 'series' => 'kittens', 'tags' => {} }])
+ end
+ end
+
+ describe '#prepare_metrics' do
+ it 'returns a Hash with the keys as Symbols' do
+ metrics = described_class.
+ prepare_metrics([{ 'values' => {}, 'tags' => {} }])
+
+ expect(metrics).to eq([{ values: {}, tags: {} }])
+ end
+
+ it 'escapes tag values' do
+ metrics = described_class.prepare_metrics([
+ { 'values' => {}, 'tags' => { 'foo' => 'bar=' } }
+ ])
+
+ expect(metrics).to eq([{ values: {}, tags: { 'foo' => 'bar\\=' } }])
+ end
+
+ it 'drops empty tags' do
+ metrics = described_class.prepare_metrics([
+ { 'values' => {}, 'tags' => { 'cats' => '', 'dogs' => nil } }
+ ])
+
+ expect(metrics).to eq([{ values: {}, tags: {} }])
+ end
+ end
+
+ describe '#escape_value' do
+ it 'escapes an equals sign' do
+ expect(described_class.escape_value('foo=')).to eq('foo\\=')
+ end
+
+ it 'casts values to Strings' do
+ expect(described_class.escape_value(10)).to eq('10')
+ end
+ end
+end
diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb
new file mode 100644
index 00000000000..eb433c38873
--- /dev/null
+++ b/spec/mailers/abuse_report_mailer_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+describe AbuseReportMailer do
+ include EmailSpec::Matchers
+
+ describe '.notify' do
+ context 'with admin_notification_email set' do
+ before do
+ stub_application_setting(admin_notification_email: 'admin@example.com')
+ end
+
+ it 'sends to the admin_notification_email' do
+ report = create(:abuse_report)
+
+ mail = described_class.notify(report.id)
+
+ expect(mail).to deliver_to 'admin@example.com'
+ end
+
+ it 'includes the user in the subject' do
+ report = create(:abuse_report)
+
+ mail = described_class.notify(report.id)
+
+ expect(mail).to have_subject "#{report.user.name} (#{report.user.username}) was reported for abuse"
+ end
+ end
+
+ context 'with no admin_notification_email set' do
+ it 'returns early' do
+ stub_application_setting(admin_notification_email: nil)
+
+ expect { described_class.notify(spy).deliver_now }.
+ not_to change { ActionMailer::Base.deliveries.count }
+ end
+ end
+ end
+end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index d45319b25d4..46cab1644c7 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -28,4 +28,21 @@ RSpec.describe AbuseReport, type: :model do
it { is_expected.to validate_presence_of(:message) }
it { is_expected.to validate_uniqueness_of(:user_id) }
end
+
+ describe '#notify' do
+ it 'delivers' do
+ expect(AbuseReportMailer).to receive(:notify).with(subject.id).
+ and_return(spy)
+
+ subject.notify
+ end
+
+ it 'returns early when not persisted' do
+ report = build(:abuse_report)
+
+ expect(AbuseReportMailer).not_to receive(:notify)
+
+ report.notify
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 0f13c4410cd..021d62cdf0c 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -81,4 +81,36 @@ describe Issue, "Issuable" do
expect(hook_data[:object_attributes]).to eq(issue.hook_attrs)
end
end
+
+ describe '#card_attributes' do
+ it 'includes the author name' do
+ allow(issue).to receive(:author).and_return(double(name: 'Robert'))
+ allow(issue).to receive(:assignee).and_return(nil)
+
+ expect(issue.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+ end
+
+ it 'includes the assignee name' do
+ allow(issue).to receive(:author).and_return(double(name: 'Robert'))
+ allow(issue).to receive(:assignee).and_return(double(name: 'Douwe'))
+
+ expect(issue.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+ end
+ end
+
+ describe "votes" do
+ before do
+ author = create :user
+ project = create :empty_project
+ issue.notes.awards.create!(note: "thumbsup", author: author, project: project)
+ issue.notes.awards.create!(note: "thumbsdown", author: author, project: project)
+ end
+
+ it "returns correct values" do
+ expect(issue.upvotes).to eq(1)
+ expect(issue.downvotes).to eq(1)
+ end
+ end
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 6653621a83e..20f0c561e44 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -3,6 +3,10 @@ require 'spec_helper'
describe Mentionable do
include Mentionable
+ def author
+ nil
+ end
+
describe :references do
let(:project) { create(:project) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 646f767e6fe..ba5acceadff 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -38,14 +38,6 @@ describe Group, models: true do
it { is_expected.not_to validate_presence_of :owner }
end
- describe '.public_and_given_groups' do
- let!(:public_group) { create(:group, public: true) }
-
- subject { described_class.public_and_given_groups([group.id]) }
-
- it { is_expected.to eq([public_group, group]) }
- end
-
describe '.visible_to_user' do
let!(:group) { create(:group) }
let!(:user) { create(:user) }
@@ -112,23 +104,4 @@ describe Group, models: true do
expect(group.avatar_type).to eq(["only images allowed"])
end
end
-
- describe "public_profile?" do
- it "returns true for public group" do
- group = create(:group, public: true)
- expect(group.public_profile?).to be_truthy
- end
-
- it "returns true for non-public group with public project" do
- group = create(:group)
- create(:project, :public, group: group)
- expect(group.public_profile?).to be_truthy
- end
-
- it "returns false for non-public group with no public projects" do
- group = create(:group)
- create(:project, group: group)
- expect(group.public_profile?).to be_falsy
- end
- end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index b7006fa5e68..593d8f76215 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -137,9 +137,14 @@ describe Note, models: true do
create :note, note: "smile", is_award: true
end
- it "returns grouped array of notes" do
- expect(Note.grouped_awards.first.first).to eq("smile")
- expect(Note.grouped_awards.first.last).to match_array(Note.all)
+ it "returns grouped hash of notes" do
+ expect(Note.grouped_awards.keys.size).to eq(3)
+ expect(Note.grouped_awards["smile"]).to match_array(Note.all)
+ end
+
+ it "returns thumbsup and thumbsdown always" do
+ expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none)
+ expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none)
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 2f184bbaf92..a16161e673e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -106,7 +106,7 @@ describe User, models: true do
end
it 'validates uniqueness' do
- expect(subject).to validate_uniqueness_of(:username)
+ expect(subject).to validate_uniqueness_of(:username).case_insensitive
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 7f0f9454b10..ab2530859ea 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -382,6 +382,15 @@ describe API::API, api: true do
expect(response.status).to eq(404)
end
+ it 'should handle users with dots' do
+ dot_user = create(:user, username: 'dot.user')
+ project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+
+ get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
describe 'permissions' do
context 'all projects' do
it 'Contains permission information' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index d7a898e85ff..b5c7b01357a 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -52,6 +52,9 @@ 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(6).times
+
ActionMailer::Base.deliveries.clear
notification.new_note(note)
@@ -61,6 +64,7 @@ describe NotificationService, services: true do
should_email(note.noteable.assignee)
should_email(@u_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
should_not_email(note.author)
should_not_email(@u_participating)
@@ -115,6 +119,7 @@ describe NotificationService, services: true do
before do
build_team(note.project)
+ note.project.team << [note.author, :master]
ActionMailer::Base.deliveries.clear
end
@@ -126,6 +131,8 @@ describe NotificationService, services: true do
note.project.team.members.each do |member|
# User with disabled notification should not be notified
next if member.id == @u_disabled.id
+ # Author should not be notified
+ next if member.id == note.author.id
should_email(member)
end
@@ -242,6 +249,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -257,6 +265,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
end
@@ -279,6 +288,7 @@ describe NotificationService, services: true do
should_email(merge_request.assignee)
should_email(@u_watcher)
+ should_email(@watcher_and_subscriber)
should_email(@u_participant_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -293,6 +303,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -307,6 +318,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -321,6 +333,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -335,6 +348,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -384,14 +398,18 @@ describe NotificationService, services: true do
@subscriber = create :user
@unsubscriber = create :user
@subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING)
+ @watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH)
project.team << [@subscribed_participant, :master]
project.team << [@subscriber, :master]
project.team << [@unsubscriber, :master]
+ project.team << [@watcher_and_subscriber, :master]
issuable.subscriptions.create(user: @subscriber, subscribed: true)
issuable.subscriptions.create(user: @subscribed_participant, subscribed: true)
issuable.subscriptions.create(user: @unsubscriber, subscribed: false)
+ # Make the watcher a subscriber to detect dupes
+ issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true)
end
def sent_to_user?(user)
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index febc78d2784..4455ae7b321 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -9,37 +9,54 @@ describe SystemHooksService, services: true do
let(:group_member) { create(:group_member) }
context 'event data' do
- it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :email, :user_id) }
- it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) }
- it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
- it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
- it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
- it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
+ it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id) }
+ it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id) }
+ it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
+ it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
+ it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
+ it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
it { expect(event_data(key, :create)).to include(:username, :key, :id) }
it { expect(event_data(key, :destroy)).to include(:username, :key, :id) }
it do
+ project.old_path_with_namespace = 'renamed_from_path'
+ expect(event_data(project, :rename)).to include(
+ :event_name, :name, :created_at, :updated_at, :path, :project_id,
+ :owner_name, :owner_email, :project_visibility,
+ :old_path_with_namespace
+ )
+ end
+ it do
+ project.old_path_with_namespace = 'transfered_from_path'
+ expect(event_data(project, :transfer)).to include(
+ :event_name, :name, :created_at, :updated_at, :path, :project_id,
+ :owner_name, :owner_email, :project_visibility,
+ :old_path_with_namespace
+ )
+ end
+
+ it do
expect(event_data(group, :create)).to include(
- :event_name, :name, :created_at, :path, :group_id, :owner_name,
- :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id,
+ :owner_name, :owner_email
)
end
it do
expect(event_data(group, :destroy)).to include(
- :event_name, :name, :created_at, :path, :group_id, :owner_name,
- :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id,
+ :owner_name, :owner_email
)
end
it do
expect(event_data(group_member, :create)).to include(
- :event_name, :created_at, :group_name, :group_path, :group_id, :user_id,
- :user_name, :user_email, :group_access
+ :event_name, :created_at, :updated_at, :group_name, :group_path,
+ :group_id, :user_id, :user_name, :user_email, :group_access
)
end
it do
expect(event_data(group_member, :destroy)).to include(
- :event_name, :created_at, :group_name, :group_path, :group_id, :user_id,
- :user_name, :user_email, :group_access
+ :event_name, :created_at, :updated_at, :group_name, :group_path,
+ :group_id, :user_id, :user_name, :user_email, :group_access
)
end
end
@@ -49,6 +66,8 @@ describe SystemHooksService, services: true do
it { expect(event_name(user, :destroy)).to eq "user_destroy" }
it { expect(event_name(project, :create)).to eq "project_create" }
it { expect(event_name(project, :destroy)).to eq "project_destroy" }
+ it { expect(event_name(project, :rename)).to eq "project_rename" }
+ it { expect(event_name(project, :transfer)).to eq "project_transfer" }
it { expect(event_name(project_member, :create)).to eq "user_add_to_team" }
it { expect(event_name(project_member, :destroy)).to eq "user_remove_from_team" }
it { expect(event_name(key, :create)).to eq 'key_create' }
diff --git a/vendor/assets/javascripts/jquery.blockUI.js b/vendor/assets/javascripts/jquery.blockUI.js
deleted file mode 100644
index c8702d79b65..00000000000
--- a/vendor/assets/javascripts/jquery.blockUI.js
+++ /dev/null
@@ -1,590 +0,0 @@
-/*!
- * jQuery blockUI plugin
- * Version 2.60.0-2013.04.05
- * @requires jQuery v1.7 or later
- *
- * Examples at: http://malsup.com/jquery/block/
- * Copyright (c) 2007-2013 M. Alsup
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- *
- * Thanks to Amir-Hossein Sobhi for some excellent contributions!
- */
-
-;(function() {
-/*jshint eqeqeq:false curly:false latedef:false */
-"use strict";
-
- function setup($) {
- $.fn._fadeIn = $.fn.fadeIn;
-
- var noOp = $.noop || function() {};
-
- // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle
- // retarded userAgent strings on Vista)
- var msie = /MSIE/.test(navigator.userAgent);
- var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent);
- var mode = document.documentMode || 0;
- var setExpr = $.isFunction( document.createElement('div').style.setExpression );
-
- // global $ methods for blocking/unblocking the entire page
- $.blockUI = function(opts) { install(window, opts); };
- $.unblockUI = function(opts) { remove(window, opts); };
-
- // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl)
- $.growlUI = function(title, message, timeout, onClose) {
- var $m = $('<div class="growlUI"></div>');
- if (title) $m.append('<h1>'+title+'</h1>');
- if (message) $m.append('<h2>'+message+'</h2>');
- if (timeout === undefined) timeout = 3000;
- $.blockUI({
- message: $m, fadeIn: 700, fadeOut: 1000, centerY: false,
- timeout: timeout, showOverlay: false,
- onUnblock: onClose,
- css: $.blockUI.defaults.growlCSS
- });
- };
-
- // plugin method for blocking element content
- $.fn.block = function(opts) {
- if ( this[0] === window ) {
- $.blockUI( opts );
- return this;
- }
- var fullOpts = $.extend({}, $.blockUI.defaults, opts || {});
- this.each(function() {
- var $el = $(this);
- if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked'))
- return;
- $el.unblock({ fadeOut: 0 });
- });
-
- return this.each(function() {
- if ($.css(this,'position') == 'static') {
- this.style.position = 'relative';
- $(this).data('blockUI.static', true);
- }
- this.style.zoom = 1; // force 'hasLayout' in ie
- install(this, opts);
- });
- };
-
- // plugin method for unblocking element content
- $.fn.unblock = function(opts) {
- if ( this[0] === window ) {
- $.unblockUI( opts );
- return this;
- }
- return this.each(function() {
- remove(this, opts);
- });
- };
-
- $.blockUI.version = 2.60; // 2nd generation blocking at no extra cost!
-
- // override these in your code to change the default behavior and style
- $.blockUI.defaults = {
- // message displayed when blocking (use null for no message)
- message: '<h1>Please wait...</h1>',
-
- title: null, // title string; only used when theme == true
- draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded)
-
- theme: false, // set to true to use with jQuery UI themes
-
- // styles for the message when blocking; if you wish to disable
- // these and use an external stylesheet then do this in your code:
- // $.blockUI.defaults.css = {};
- css: {
- padding: 0,
- margin: 0,
- width: '30%',
- top: '40%',
- left: '35%',
- textAlign: 'center',
- color: '#000',
- border: '3px solid #aaa',
- backgroundColor:'#fff',
- cursor: 'wait'
- },
-
- // minimal style set used when themes are used
- themedCSS: {
- width: '30%',
- top: '40%',
- left: '35%'
- },
-
- // styles for the overlay
- overlayCSS: {
- backgroundColor: '#000',
- opacity: 0.6,
- cursor: 'wait'
- },
-
- // style to replace wait cursor before unblocking to correct issue
- // of lingering wait cursor
- cursorReset: 'default',
-
- // styles applied when using $.growlUI
- growlCSS: {
- width: '350px',
- top: '10px',
- left: '',
- right: '10px',
- border: 'none',
- padding: '5px',
- opacity: 0.6,
- cursor: 'default',
- color: '#fff',
- backgroundColor: '#000',
- '-webkit-border-radius':'10px',
- '-moz-border-radius': '10px',
- 'border-radius': '10px'
- },
-
- // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w
- // (hat tip to Jorge H. N. de Vasconcelos)
- /*jshint scripturl:true */
- iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank',
-
- // force usage of iframe in non-IE browsers (handy for blocking applets)
- forceIframe: false,
-
- // z-index for the blocking overlay
- baseZ: 1000,
-
- // set these to true to have the message automatically centered
- centerX: true, // <-- only effects element blocking (page block controlled via css above)
- centerY: true,
-
- // allow body element to be stetched in ie6; this makes blocking look better
- // on "short" pages. disable if you wish to prevent changes to the body height
- allowBodyStretch: true,
-
- // enable if you want key and mouse events to be disabled for content that is blocked
- bindEvents: true,
-
- // be default blockUI will supress tab navigation from leaving blocking content
- // (if bindEvents is true)
- constrainTabKey: true,
-
- // fadeIn time in millis; set to 0 to disable fadeIn on block
- fadeIn: 200,
-
- // fadeOut time in millis; set to 0 to disable fadeOut on unblock
- fadeOut: 400,
-
- // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock
- timeout: 0,
-
- // disable if you don't want to show the overlay
- showOverlay: true,
-
- // if true, focus will be placed in the first available input field when
- // page blocking
- focusInput: true,
-
- // elements that can receive focus
- focusableElements: ':input:enabled:visible',
-
- // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity)
- // no longer needed in 2012
- // applyPlatformOpacityRules: true,
-
- // callback method invoked when fadeIn has completed and blocking message is visible
- onBlock: null,
-
- // callback method invoked when unblocking has completed; the callback is
- // passed the element that has been unblocked (which is the window object for page
- // blocks) and the options that were passed to the unblock call:
- // onUnblock(element, options)
- onUnblock: null,
-
- // callback method invoked when the overlay area is clicked.
- // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used.
- onOverlayClick: null,
-
- // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493
- quirksmodeOffsetHack: 4,
-
- // class name of the message block
- blockMsgClass: 'blockMsg',
-
- // if it is already blocked, then ignore it (don't unblock and reblock)
- ignoreIfBlocked: false
- };
-
- // private data and functions follow...
-
- var pageBlock = null;
- var pageBlockEls = [];
-
- function install(el, opts) {
- var css, themedCSS;
- var full = (el == window);
- var msg = (opts && opts.message !== undefined ? opts.message : undefined);
- opts = $.extend({}, $.blockUI.defaults, opts || {});
-
- if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked'))
- return;
-
- opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {});
- css = $.extend({}, $.blockUI.defaults.css, opts.css || {});
- if (opts.onOverlayClick)
- opts.overlayCSS.cursor = 'pointer';
-
- themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {});
- msg = msg === undefined ? opts.message : msg;
-
- // remove the current block (if there is one)
- if (full && pageBlock)
- remove(window, {fadeOut:0});
-
- // if an existing element is being used as the blocking content then we capture
- // its current place in the DOM (and current display style) so we can restore
- // it when we unblock
- if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) {
- var node = msg.jquery ? msg[0] : msg;
- var data = {};
- $(el).data('blockUI.history', data);
- data.el = node;
- data.parent = node.parentNode;
- data.display = node.style.display;
- data.position = node.style.position;
- if (data.parent)
- data.parent.removeChild(node);
- }
-
- $(el).data('blockUI.onUnblock', opts.onUnblock);
- var z = opts.baseZ;
-
- // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform;
- // layer1 is the iframe layer which is used to supress bleed through of underlying content
- // layer2 is the overlay layer which has opacity and a wait cursor (by default)
- // layer3 is the message content that is displayed while blocking
- var lyr1, lyr2, lyr3, s;
- if (msie || opts.forceIframe)
- lyr1 = $('<iframe class="blockUI" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="'+opts.iframeSrc+'"></iframe>');
- else
- lyr1 = $('<div class="blockUI" style="display:none"></div>');
-
- if (opts.theme)
- lyr2 = $('<div class="blockUI blockOverlay ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>');
- else
- lyr2 = $('<div class="blockUI blockOverlay" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>');
-
- if (opts.theme && full) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:fixed">';
- if ( opts.title ) {
- s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>';
- }
- s += '<div class="ui-widget-content ui-dialog-content"></div>';
- s += '</div>';
- }
- else if (opts.theme) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:absolute">';
- if ( opts.title ) {
- s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>';
- }
- s += '<div class="ui-widget-content ui-dialog-content"></div>';
- s += '</div>';
- }
- else if (full) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>';
- }
- else {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement" style="z-index:'+(z+10)+';display:none;position:absolute"></div>';
- }
- lyr3 = $(s);
-
- // if we have a message, style it
- if (msg) {
- if (opts.theme) {
- lyr3.css(themedCSS);
- lyr3.addClass('ui-widget-content');
- }
- else
- lyr3.css(css);
- }
-
- // style the overlay
- if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/)
- lyr2.css(opts.overlayCSS);
- lyr2.css('position', full ? 'fixed' : 'absolute');
-
- // make iframe layer transparent in IE
- if (msie || opts.forceIframe)
- lyr1.css('opacity',0.0);
-
- //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el);
- var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el);
- $.each(layers, function() {
- this.appendTo($par);
- });
-
- if (opts.theme && opts.draggable && $.fn.draggable) {
- lyr3.draggable({
- handle: '.ui-dialog-titlebar',
- cancel: 'li'
- });
- }
-
- // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling)
- var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0);
- if (ie6 || expr) {
- // give body 100% height
- if (full && opts.allowBodyStretch && $.support.boxModel)
- $('html,body').css('height','100%');
-
- // fix ie6 issue when blocked element has a border width
- if ((ie6 || !$.support.boxModel) && !full) {
- var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth');
- var fixT = t ? '(0 - '+t+')' : 0;
- var fixL = l ? '(0 - '+l+')' : 0;
- }
-
- // simulate fixed position
- $.each(layers, function(i,o) {
- var s = o[0].style;
- s.position = 'absolute';
- if (i < 2) {
- if (full)
- s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"');
- else
- s.setExpression('height','this.parentNode.offsetHeight + "px"');
- if (full)
- s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"');
- else
- s.setExpression('width','this.parentNode.offsetWidth + "px"');
- if (fixL) s.setExpression('left', fixL);
- if (fixT) s.setExpression('top', fixT);
- }
- else if (opts.centerY) {
- if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"');
- s.marginTop = 0;
- }
- else if (!opts.centerY && full) {
- var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0;
- var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"';
- s.setExpression('top',expression);
- }
- });
- }
-
- // show the message
- if (msg) {
- if (opts.theme)
- lyr3.find('.ui-widget-content').append(msg);
- else
- lyr3.append(msg);
- if (msg.jquery || msg.nodeType)
- $(msg).show();
- }
-
- if ((msie || opts.forceIframe) && opts.showOverlay)
- lyr1.show(); // opacity is zero
- if (opts.fadeIn) {
- var cb = opts.onBlock ? opts.onBlock : noOp;
- var cb1 = (opts.showOverlay && !msg) ? cb : noOp;
- var cb2 = msg ? cb : noOp;
- if (opts.showOverlay)
- lyr2._fadeIn(opts.fadeIn, cb1);
- if (msg)
- lyr3._fadeIn(opts.fadeIn, cb2);
- }
- else {
- if (opts.showOverlay)
- lyr2.show();
- if (msg)
- lyr3.show();
- if (opts.onBlock)
- opts.onBlock();
- }
-
- // bind key and mouse events
- bind(1, el, opts);
-
- if (full) {
- pageBlock = lyr3[0];
- pageBlockEls = $(opts.focusableElements,pageBlock);
- if (opts.focusInput)
- setTimeout(focus, 20);
- }
- else
- center(lyr3[0], opts.centerX, opts.centerY);
-
- if (opts.timeout) {
- // auto-unblock
- var to = setTimeout(function() {
- if (full)
- $.unblockUI(opts);
- else
- $(el).unblock(opts);
- }, opts.timeout);
- $(el).data('blockUI.timeout', to);
- }
- }
-
- // remove the block
- function remove(el, opts) {
- var count;
- var full = (el == window);
- var $el = $(el);
- var data = $el.data('blockUI.history');
- var to = $el.data('blockUI.timeout');
- if (to) {
- clearTimeout(to);
- $el.removeData('blockUI.timeout');
- }
- opts = $.extend({}, $.blockUI.defaults, opts || {});
- bind(0, el, opts); // unbind events
-
- if (opts.onUnblock === null) {
- opts.onUnblock = $el.data('blockUI.onUnblock');
- $el.removeData('blockUI.onUnblock');
- }
-
- var els;
- if (full) // crazy selector to handle odd field errors in ie6/7
- els = $('body').children().filter('.blockUI').add('body > .blockUI');
- else
- els = $el.find('>.blockUI');
-
- // fix cursor issue
- if ( opts.cursorReset ) {
- if ( els.length > 1 )
- els[1].style.cursor = opts.cursorReset;
- if ( els.length > 2 )
- els[2].style.cursor = opts.cursorReset;
- }
-
- if (full)
- pageBlock = pageBlockEls = null;
-
- if (opts.fadeOut) {
- count = els.length;
- els.fadeOut(opts.fadeOut, function() {
- if ( --count === 0)
- reset(els,data,opts,el);
- });
- }
- else
- reset(els, data, opts, el);
- }
-
- // move blocking element back into the DOM where it started
- function reset(els,data,opts,el) {
- var $el = $(el);
- els.each(function(i,o) {
- // remove via DOM calls so we don't lose event handlers
- if (this.parentNode)
- this.parentNode.removeChild(this);
- });
-
- if (data && data.el) {
- data.el.style.display = data.display;
- data.el.style.position = data.position;
- if (data.parent)
- data.parent.appendChild(data.el);
- $el.removeData('blockUI.history');
- }
-
- if ($el.data('blockUI.static')) {
- $el.css('position', 'static'); // #22
- }
-
- if (typeof opts.onUnblock == 'function')
- opts.onUnblock(el,opts);
-
- // fix issue in Safari 6 where block artifacts remain until reflow
- var body = $(document.body), w = body.width(), cssW = body[0].style.width;
- body.width(w-1).width(w);
- body[0].style.width = cssW;
- }
-
- // bind/unbind the handler
- function bind(b, el, opts) {
- var full = el == window, $el = $(el);
-
- // don't bother unbinding if there is nothing to unbind
- if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked')))
- return;
-
- $el.data('blockUI.isBlocked', b);
-
- // don't bind events when overlay is not in use or if bindEvents is false
- if (!full || !opts.bindEvents || (b && !opts.showOverlay))
- return;
-
- // bind anchors and inputs for mouse and key events
- var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove';
- if (b)
- $(document).bind(events, opts, handler);
- else
- $(document).unbind(events, handler);
-
- // former impl...
- // var $e = $('a,:input');
- // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler);
- }
-
- // event handler to suppress keyboard/mouse events when blocking
- function handler(e) {
- // allow tab navigation (conditionally)
- if (e.keyCode && e.keyCode == 9) {
- if (pageBlock && e.data.constrainTabKey) {
- var els = pageBlockEls;
- var fwd = !e.shiftKey && e.target === els[els.length-1];
- var back = e.shiftKey && e.target === els[0];
- if (fwd || back) {
- setTimeout(function(){focus(back);},10);
- return false;
- }
- }
- }
- var opts = e.data;
- var target = $(e.target);
- if (target.hasClass('blockOverlay') && opts.onOverlayClick)
- opts.onOverlayClick();
-
- // allow events within the message content
- if (target.parents('div.' + opts.blockMsgClass).length > 0)
- return true;
-
- // allow events for content that is not being blocked
- return target.parents().children().filter('div.blockUI').length === 0;
- }
-
- function focus(back) {
- if (!pageBlockEls)
- return;
- var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0];
- if (e)
- e.focus();
- }
-
- function center(el, x, y) {
- var p = el.parentNode, s = el.style;
- var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth');
- var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth');
- if (x) s.left = l > 0 ? (l+'px') : '0';
- if (y) s.top = t > 0 ? (t+'px') : '0';
- }
-
- function sz(el, p) {
- return parseInt($.css(el,p),10)||0;
- }
-
- }
-
-
- /*global define:true */
- if (typeof define === 'function' && define.amd && define.amd.jQuery) {
- define(['jquery'], setup);
- } else {
- setup(jQuery);
- }
-
-})();
diff --git a/vendor/assets/javascripts/jquery.history.js b/vendor/assets/javascripts/jquery.history.js
deleted file mode 100644
index 8d4edcd210e..00000000000
--- a/vendor/assets/javascripts/jquery.history.js
+++ /dev/null
@@ -1 +0,0 @@
-window.JSON||(window.JSON={}),function(){function f(a){return a<10?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c<f;c+=1)h[c]=str(c,i)||"null";return e=h.length===0?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g,e}if(rep&&typeof rep=="object"){f=rep.length;for(c=0;c<f;c+=1)d=rep[c],typeof d=="string"&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e))}else for(d in i)Object.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e));return e=h.length===0?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g,e}}"use strict",typeof Date.prototype.toJSON!="function"&&(Date.prototype.toJSON=function(a){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(a){return this.valueOf()});var JSON=window.JSON,cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;typeof JSON.stringify!="function"&&(JSON.stringify=function(a,b,c){var d;gap="",indent="";if(typeof c=="number")for(d=0;d<c;d+=1)indent+=" ";else typeof c=="string"&&(indent=c);rep=b;if(!b||typeof b=="function"||typeof b=="object"&&typeof b.length=="number")return str("",{"":a});throw new Error("JSON.stringify")}),typeof JSON.parse!="function"&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&typeof e=="object")for(c in e)Object.hasOwnProperty.call(e,c)&&(d=walk(e,c),d!==undefined?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)}));if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),typeof reviver=="function"?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}(),function(a,b){"use strict";var c=a.History=a.History||{},d=a.jQuery;if(typeof c.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");c.Adapter={bind:function(a,b,c){d(a).bind(b,c)},trigger:function(a,b,c){d(a).trigger(b,c)},extractEventData:function(a,c,d){var e=c&&c.originalEvent&&c.originalEvent[a]||d&&d[a]||b;return e},onDomLoad:function(a){d(a)}},typeof c.init!="undefined"&&c.init()}(window),function(a,b){"use strict";var c=a.document,d=a.setTimeout||d,e=a.clearTimeout||e,f=a.setInterval||f,g=a.History=a.History||{};if(typeof g.initHtml4!="undefined")throw new Error("History.js HTML4 Support has already been loaded...");g.initHtml4=function(){if(typeof g.initHtml4.initialized!="undefined")return!1;g.initHtml4.initialized=!0,g.enabled=!0,g.savedHashes=[],g.isLastHash=function(a){var b=g.getHashByIndex(),c;return c=a===b,c},g.saveHash=function(a){return g.isLastHash(a)?!1:(g.savedHashes.push(a),!0)},g.getHashByIndex=function(a){var b=null;return typeof a=="undefined"?b=g.savedHashes[g.savedHashes.length-1]:a<0?b=g.savedHashes[g.savedHashes.length+a]:b=g.savedHashes[a],b},g.discardedHashes={},g.discardedStates={},g.discardState=function(a,b,c){var d=g.getHashByState(a),e;return e={discardedState:a,backState:c,forwardState:b},g.discardedStates[d]=e,!0},g.discardHash=function(a,b,c){var d={discardedHash:a,backState:c,forwardState:b};return g.discardedHashes[a]=d,!0},g.discardedState=function(a){var b=g.getHashByState(a),c;return c=g.discardedStates[b]||!1,c},g.discardedHash=function(a){var b=g.discardedHashes[a]||!1;return b},g.recycleState=function(a){var b=g.getHashByState(a);return g.discardedState(a)&&delete g.discardedStates[b],!0},g.emulated.hashChange&&(g.hashChangeInit=function(){g.checkerFunction=null;var b="",d,e,h,i;return g.isInternetExplorer()?(d="historyjs-iframe",e=c.createElement("iframe"),e.setAttribute("id",d),e.style.display="none",c.body.appendChild(e),e.contentWindow.document.open(),e.contentWindow.document.close(),h="",i=!1,g.checkerFunction=function(){if(i)return!1;i=!0;var c=g.getHash()||"",d=g.unescapeHash(e.contentWindow.document.location.hash)||"";return c!==b?(b=c,d!==c&&(h=d=c,e.contentWindow.document.open(),e.contentWindow.document.close(),e.contentWindow.document.location.hash=g.escapeHash(c)),g.Adapter.trigger(a,"hashchange")):d!==h&&(h=d,g.setHash(d,!1)),i=!1,!0}):g.checkerFunction=function(){var c=g.getHash();return c!==b&&(b=c,g.Adapter.trigger(a,"hashchange")),!0},g.intervalList.push(f(g.checkerFunction,g.options.hashChangeInterval)),!0},g.Adapter.onDomLoad(g.hashChangeInit)),g.emulated.pushState&&(g.onHashChange=function(b){var d=b&&b.newURL||c.location.href,e=g.getHashByUrl(d),f=null,h=null,i=null,j;return g.isLastHash(e)?(g.busy(!1),!1):(g.doubleCheckComplete(),g.saveHash(e),e&&g.isTraditionalAnchor(e)?(g.Adapter.trigger(a,"anchorchange"),g.busy(!1),!1):(f=g.extractState(g.getFullUrl(e||c.location.href,!1),!0),g.isLastSavedState(f)?(g.busy(!1),!1):(h=g.getHashByState(f),j=g.discardedState(f),j?(g.getHashByIndex(-2)===g.getHashByState(j.forwardState)?g.back(!1):g.forward(!1),!1):(g.pushState(f.data,f.title,f.url,!1),!0))))},g.Adapter.bind(a,"hashchange",g.onHashChange),g.pushState=function(b,d,e,f){if(g.getHashByUrl(e))throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(f!==!1&&g.busy())return g.pushQueue({scope:g,callback:g.pushState,args:arguments,queue:f}),!1;g.busy(!0);var h=g.createStateObject(b,d,e),i=g.getHashByState(h),j=g.getState(!1),k=g.getHashByState(j),l=g.getHash();return g.storeState(h),g.expectedStateId=h.id,g.recycleState(h),g.setTitle(h),i===k?(g.busy(!1),!1):i!==l&&i!==g.getShortUrl(c.location.href)?(g.setHash(i,!1),!1):(g.saveState(h),g.Adapter.trigger(a,"statechange"),g.busy(!1),!0)},g.replaceState=function(a,b,c,d){if(g.getHashByUrl(c))throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(d!==!1&&g.busy())return g.pushQueue({scope:g,callback:g.replaceState,args:arguments,queue:d}),!1;g.busy(!0);var e=g.createStateObject(a,b,c),f=g.getState(!1),h=g.getStateByIndex(-2);return g.discardState(f,e,h),g.pushState(e.data,e.title,e.url,!1),!0}),g.emulated.pushState&&g.getHash()&&!g.emulated.hashChange&&g.Adapter.onDomLoad(function(){g.Adapter.trigger(a,"hashchange")})},typeof g.init!="undefined"&&g.init()}(window),function(a,b){"use strict";var c=a.console||b,d=a.document,e=a.navigator,f=a.sessionStorage||!1,g=a.setTimeout,h=a.clearTimeout,i=a.setInterval,j=a.clearInterval,k=a.JSON,l=a.alert,m=a.History=a.History||{},n=a.history;k.stringify=k.stringify||k.encode,k.parse=k.parse||k.decode;if(typeof m.init!="undefined")throw new Error("History.js Core has already been loaded...");m.init=function(){return typeof m.Adapter=="undefined"?!1:(typeof m.initCore!="undefined"&&m.initCore(),typeof m.initHtml4!="undefined"&&m.initHtml4(),!0)},m.initCore=function(){if(typeof m.initCore.initialized!="undefined")return!1;m.initCore.initialized=!0,m.options=m.options||{},m.options.hashChangeInterval=m.options.hashChangeInterval||100,m.options.safariPollInterval=m.options.safariPollInterval||500,m.options.doubleCheckInterval=m.options.doubleCheckInterval||500,m.options.storeInterval=m.options.storeInterval||1e3,m.options.busyDelay=m.options.busyDelay||250,m.options.debug=m.options.debug||!1,m.options.initialTitle=m.options.initialTitle||d.title,m.intervalList=[],m.clearAllIntervals=function(){var a,b=m.intervalList;if(typeof b!="undefined"&&b!==null){for(a=0;a<b.length;a++)j(b[a]);m.intervalList=null}},m.debug=function(){(m.options.debug||!1)&&m.log.apply(m,arguments)},m.log=function(){var a=typeof c!="undefined"&&typeof c.log!="undefined"&&typeof c.log.apply!="undefined",b=d.getElementById("log"),e,f,g,h,i;a?(h=Array.prototype.slice.call(arguments),e=h.shift(),typeof c.debug!="undefined"?c.debug.apply(c,[e,h]):c.log.apply(c,[e,h])):e="\n"+arguments[0]+"\n";for(f=1,g=arguments.length;f<g;++f){i=arguments[f];if(typeof i=="object"&&typeof k!="undefined")try{i=k.stringify(i)}catch(j){}e+="\n"+i+"\n"}return b?(b.value+=e+"\n-----\n",b.scrollTop=b.scrollHeight-b.clientHeight):a||l(e),!0},m.getInternetExplorerMajorVersion=function(){var a=m.getInternetExplorerMajorVersion.cached=typeof m.getInternetExplorerMajorVersion.cached!="undefined"?m.getInternetExplorerMajorVersion.cached:function(){var a=3,b=d.createElement("div"),c=b.getElementsByTagName("i");while((b.innerHTML="<!--[if gt IE "+ ++a+"]><i></i><![endif]-->")&&c[0]);return a>4?a:!1}();return a},m.isInternetExplorer=function(){var a=m.isInternetExplorer.cached=typeof m.isInternetExplorer.cached!="undefined"?m.isInternetExplorer.cached:Boolean(m.getInternetExplorerMajorVersion());return a},m.emulated={pushState:!Boolean(a.history&&a.history.pushState&&a.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)),hashChange:Boolean(!("onhashchange"in a||"onhashchange"in d)||m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8)},m.enabled=!m.emulated.pushState,m.bugs={setHash:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),safariPoll:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),ieDoubleCheck:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<7)},m.isEmptyObject=function(a){for(var b in a)return!1;return!0},m.cloneObject=function(a){var b,c;return a?(b=k.stringify(a),c=k.parse(b)):c={},c},m.getRootUrl=function(){var a=d.location.protocol+"//"+(d.location.hostname||d.location.host);if(d.location.port||!1)a+=":"+d.location.port;return a+="/",a},m.getBaseHref=function(){var a=d.getElementsByTagName("base"),b=null,c="";return a.length===1&&(b=a[0],c=b.href.replace(/[^\/]+$/,"")),c=c.replace(/\/+$/,""),c&&(c+="/"),c},m.getBaseUrl=function(){var a=m.getBaseHref()||m.getBasePageUrl()||m.getRootUrl();return a},m.getPageUrl=function(){var a=m.getState(!1,!1),b=(a||{}).url||d.location.href,c;return c=b.replace(/\/+$/,"").replace(/[^\/]+$/,function(a,b,c){return/\./.test(a)?a:a+"/"}),c},m.getBasePageUrl=function(){var a=d.location.href.replace(/[#\?].*/,"").replace(/[^\/]+$/,function(a,b,c){return/[^\/]$/.test(a)?"":a}).replace(/\/+$/,"")+"/";return a},m.getFullUrl=function(a,b){var c=a,d=a.substring(0,1);return b=typeof b=="undefined"?!0:b,/[a-z]+\:\/\//.test(a)||(d==="/"?c=m.getRootUrl()+a.replace(/^\/+/,""):d==="#"?c=m.getPageUrl().replace(/#.*/,"")+a:d==="?"?c=m.getPageUrl().replace(/[\?#].*/,"")+a:b?c=m.getBaseUrl()+a.replace(/^(\.\/)+/,""):c=m.getBasePageUrl()+a.replace(/^(\.\/)+/,"")),c.replace(/\#$/,"")},m.getShortUrl=function(a){var b=a,c=m.getBaseUrl(),d=m.getRootUrl();return m.emulated.pushState&&(b=b.replace(c,"")),b=b.replace(d,"/"),m.isTraditionalAnchor(b)&&(b="./"+b),b=b.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),b},m.store={},m.idToState=m.idToState||{},m.stateToId=m.stateToId||{},m.urlToId=m.urlToId||{},m.storedStates=m.storedStates||[],m.savedStates=m.savedStates||[],m.normalizeStore=function(){m.store.idToState=m.store.idToState||{},m.store.urlToId=m.store.urlToId||{},m.store.stateToId=m.store.stateToId||{}},m.getState=function(a,b){typeof a=="undefined"&&(a=!0),typeof b=="undefined"&&(b=!0);var c=m.getLastSavedState();return!c&&b&&(c=m.createStateObject()),a&&(c=m.cloneObject(c),c.url=c.cleanUrl||c.url),c},m.getIdByState=function(a){var b=m.extractId(a.url),c;if(!b){c=m.getStateString(a);if(typeof m.stateToId[c]!="undefined")b=m.stateToId[c];else if(typeof m.store.stateToId[c]!="undefined")b=m.store.stateToId[c];else{for(;;){b=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof m.idToState[b]=="undefined"&&typeof m.store.idToState[b]=="undefined")break}m.stateToId[c]=b,m.idToState[b]=a}}return b},m.normalizeState=function(a){var b,c;if(!a||typeof a!="object")a={};if(typeof a.normalized!="undefined")return a;if(!a.data||typeof a.data!="object")a.data={};b={},b.normalized=!0,b.title=a.title||"",b.url=m.getFullUrl(m.unescapeString(a.url||d.location.href)),b.hash=m.getShortUrl(b.url),b.data=m.cloneObject(a.data),b.id=m.getIdByState(b),b.cleanUrl=b.url.replace(/\??\&_suid.*/,""),b.url=b.cleanUrl,c=!m.isEmptyObject(b.data);if(b.title||c)b.hash=m.getShortUrl(b.url).replace(/\??\&_suid.*/,""),/\?/.test(b.hash)||(b.hash+="?"),b.hash+="&_suid="+b.id;return b.hashedUrl=m.getFullUrl(b.hash),(m.emulated.pushState||m.bugs.safariPoll)&&m.hasUrlDuplicate(b)&&(b.url=b.hashedUrl),b},m.createStateObject=function(a,b,c){var d={data:a,title:b,url:c};return d=m.normalizeState(d),d},m.getStateById=function(a){a=String(a);var c=m.idToState[a]||m.store.idToState[a]||b;return c},m.getStateString=function(a){var b,c,d;return b=m.normalizeState(a),c={data:b.data,title:a.title,url:a.url},d=k.stringify(c),d},m.getStateId=function(a){var b,c;return b=m.normalizeState(a),c=b.id,c},m.getHashByState=function(a){var b,c;return b=m.normalizeState(a),c=b.hash,c},m.extractId=function(a){var b,c,d;return c=/(.*)\&_suid=([0-9]+)$/.exec(a),d=c?c[1]||a:a,b=c?String(c[2]||""):"",b||!1},m.isTraditionalAnchor=function(a){var b=!/[\/\?\.]/.test(a);return b},m.extractState=function(a,b){var c=null,d,e;return b=b||!1,d=m.extractId(a),d&&(c=m.getStateById(d)),c||(e=m.getFullUrl(a),d=m.getIdByUrl(e)||!1,d&&(c=m.getStateById(d)),!c&&b&&!m.isTraditionalAnchor(a)&&(c=m.createStateObject(null,null,e))),c},m.getIdByUrl=function(a){var c=m.urlToId[a]||m.store.urlToId[a]||b;return c},m.getLastSavedState=function(){return m.savedStates[m.savedStates.length-1]||b},m.getLastStoredState=function(){return m.storedStates[m.storedStates.length-1]||b},m.hasUrlDuplicate=function(a){var b=!1,c;return c=m.extractState(a.url),b=c&&c.id!==a.id,b},m.storeState=function(a){return m.urlToId[a.url]=a.id,m.storedStates.push(m.cloneObject(a)),a},m.isLastSavedState=function(a){var b=!1,c,d,e;return m.savedStates.length&&(c=a.id,d=m.getLastSavedState(),e=d.id,b=c===e),b},m.saveState=function(a){return m.isLastSavedState(a)?!1:(m.savedStates.push(m.cloneObject(a)),!0)},m.getStateByIndex=function(a){var b=null;return typeof a=="undefined"?b=m.savedStates[m.savedStates.length-1]:a<0?b=m.savedStates[m.savedStates.length+a]:b=m.savedStates[a],b},m.getHash=function(){var a=m.unescapeHash(d.location.hash);return a},m.unescapeString=function(b){var c=b,d;for(;;){d=a.unescape(c);if(d===c)break;c=d}return c},m.unescapeHash=function(a){var b=m.normalizeHash(a);return b=m.unescapeString(b),b},m.normalizeHash=function(a){var b=a.replace(/[^#]*#/,"").replace(/#.*/,"");return b},m.setHash=function(a,b){var c,e,f;return b!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.setHash,args:arguments,queue:b}),!1):(c=m.escapeHash(a),m.busy(!0),e=m.extractState(a,!0),e&&!m.emulated.pushState?m.pushState(e.data,e.title,e.url,!1):d.location.hash!==c&&(m.bugs.setHash?(f=m.getPageUrl(),m.pushState(null,null,f+"#"+c,!1)):d.location.hash=c),m)},m.escapeHash=function(b){var c=m.normalizeHash(b);return c=a.escape(c),m.bugs.hashEscape||(c=c.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),c},m.getHashByUrl=function(a){var b=String(a).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return b=m.unescapeHash(b),b},m.setTitle=function(a){var b=a.title,c;b||(c=m.getStateByIndex(0),c&&c.url===a.url&&(b=c.title||m.options.initialTitle));try{d.getElementsByTagName("title")[0].innerHTML=b.replace("<","&lt;").replace(">","&gt;").replace(" & "," &amp; ")}catch(e){}return d.title=b,m},m.queues=[],m.busy=function(a){typeof a!="undefined"?m.busy.flag=a:typeof m.busy.flag=="undefined"&&(m.busy.flag=!1);if(!m.busy.flag){h(m.busy.timeout);var b=function(){var a,c,d;if(m.busy.flag)return;for(a=m.queues.length-1;a>=0;--a){c=m.queues[a];if(c.length===0)continue;d=c.shift(),m.fireQueueItem(d),m.busy.timeout=g(b,m.options.busyDelay)}};m.busy.timeout=g(b,m.options.busyDelay)}return m.busy.flag},m.busy.flag=!1,m.fireQueueItem=function(a){return a.callback.apply(a.scope||m,a.args||[])},m.pushQueue=function(a){return m.queues[a.queue||0]=m.queues[a.queue||0]||[],m.queues[a.queue||0].push(a),m},m.queue=function(a,b){return typeof a=="function"&&(a={callback:a}),typeof b!="undefined"&&(a.queue=b),m.busy()?m.pushQueue(a):m.fireQueueItem(a),m},m.clearQueue=function(){return m.busy.flag=!1,m.queues=[],m},m.stateChanged=!1,m.doubleChecker=!1,m.doubleCheckComplete=function(){return m.stateChanged=!0,m.doubleCheckClear(),m},m.doubleCheckClear=function(){return m.doubleChecker&&(h(m.doubleChecker),m.doubleChecker=!1),m},m.doubleCheck=function(a){return m.stateChanged=!1,m.doubleCheckClear(),m.bugs.ieDoubleCheck&&(m.doubleChecker=g(function(){return m.doubleCheckClear(),m.stateChanged||a(),!0},m.options.doubleCheckInterval)),m},m.safariStatePoll=function(){var b=m.extractState(d.location.href),c;if(!m.isLastSavedState(b))c=b;else return;return c||(c=m.createStateObject()),m.Adapter.trigger(a,"popstate"),m},m.back=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.back,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.back(!1)}),n.go(-1),!0)},m.forward=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.forward,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.forward(!1)}),n.go(1),!0)},m.go=function(a,b){var c;if(a>0)for(c=1;c<=a;++c)m.forward(b);else{if(!(a<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(c=-1;c>=a;--c)m.back(b)}return m};if(m.emulated.pushState){var o=function(){};m.pushState=m.pushState||o,m.replaceState=m.replaceState||o}else m.onPopState=function(b,c){var e=!1,f=!1,g,h;return m.doubleCheckComplete(),g=m.getHash(),g?(h=m.extractState(g||d.location.href,!0),h?m.replaceState(h.data,h.title,h.url,!1):(m.Adapter.trigger(a,"anchorchange"),m.busy(!1)),m.expectedStateId=!1,!1):(e=m.Adapter.extractEventData("state",b,c)||!1,e?f=m.getStateById(e):m.expectedStateId?f=m.getStateById(m.expectedStateId):f=m.extractState(d.location.href),f||(f=m.createStateObject(null,null,d.location.href)),m.expectedStateId=!1,m.isLastSavedState(f)?(m.busy(!1),!1):(m.storeState(f),m.saveState(f),m.setTitle(f),m.Adapter.trigger(a,"statechange"),m.busy(!1),!0))},m.Adapter.bind(a,"popstate",m.onPopState),m.pushState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.pushState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.pushState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0},m.replaceState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.replaceState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.replaceState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0};if(f){try{m.store=k.parse(f.getItem("History.store"))||{}}catch(p){m.store={}}m.normalizeStore()}else m.store={},m.normalizeStore();m.Adapter.bind(a,"beforeunload",m.clearAllIntervals),m.Adapter.bind(a,"unload",m.clearAllIntervals),m.saveState(m.storeState(m.extractState(d.location.href,!0))),f&&(m.onUnload=function(){var a,b;try{a=k.parse(f.getItem("History.store"))||{}}catch(c){a={}}a.idToState=a.idToState||{},a.urlToId=a.urlToId||{},a.stateToId=a.stateToId||{};for(b in m.idToState){if(!m.idToState.hasOwnProperty(b))continue;a.idToState[b]=m.idToState[b]}for(b in m.urlToId){if(!m.urlToId.hasOwnProperty(b))continue;a.urlToId[b]=m.urlToId[b]}for(b in m.stateToId){if(!m.stateToId.hasOwnProperty(b))continue;a.stateToId[b]=m.stateToId[b]}m.store=a,m.normalizeStore(),f.setItem("History.store",k.stringify(a))},m.intervalList.push(i(m.onUnload,m.options.storeInterval)),m.Adapter.bind(a,"beforeunload",m.onUnload),m.Adapter.bind(a,"unload",m.onUnload));if(!m.emulated.pushState){m.bugs.safariPoll&&m.intervalList.push(i(m.safariStatePoll,m.options.safariPollInterval));if(e.vendor==="Apple Computer, Inc."||(e.appCodeName||"")==="Mozilla")m.Adapter.bind(a,"hashchange",function(){m.Adapter.trigger(a,"popstate")}),m.getHash()&&m.Adapter.onDomLoad(function(){m.Adapter.trigger(a,"hashchange")})}},m.init()}(window) \ No newline at end of file