diff options
author | Jacob Schatz <jacobschatz@Jacobs-MBP.fios-router.home> | 2016-01-11 18:06:15 -0500 |
---|---|---|
committer | Jacob Schatz <jacobschatz@Jacobs-MBP.fios-router.home> | 2016-01-11 18:06:15 -0500 |
commit | f2edb26a1e4b2587f6177af2232997bc2c6a902e (patch) | |
tree | 73af4940c9f49e125c0a4a375929c31f7b240f6a | |
parent | ab9612df8dd97d83a94a8290f1530760e5cc7d2e (diff) | |
parent | f2fab27a562dbb0e1522e388b5719d226ce66e4b (diff) | |
download | gitlab-ce-f2edb26a1e4b2587f6177af2232997bc2c6a902e.tar.gz |
fixes conflicts
422 files changed, 4659 insertions, 2997 deletions
diff --git a/.gitignore b/.gitignore index f5b6427ca03..91ea81bfc4e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ config/initializers/smtp_settings.rb config/resque.yml config/unicorn.rb config/secrets.yml +config/sidekiq.yml coverage/* db/*.sqlite3 db/*.sqlite3-journal diff --git a/CHANGELOG b/CHANGELOG index 57f0b9f30d5..0f89093d223 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,28 +1,66 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.4.0 (unreleased) - - Add support for Google reCAPTCHA in user registration to prevent spammers (Stan Hu) + - Add housekeeping function to project settings page + - The default GitLab logo now acts as a loading indicator + - Fix caching issue where build status was not updating in project dashboard (Stan Hu) + - Accept 2xx status codes for successful Web hook triggers (Stan Hu) + - Fix missing date of month in network graph when commits span a month (Stan Hu) + - 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 - Only allow group/project members to mention `@all` - - Expose Git's version in the admin area + - 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. + - Add link to merge request on build detail page + - Fix: Problem with projects ending with .keys (Jose Corcuera) - Revert back upvote and downvote button to the issue and MR pages - -v 8.3.2 (unreleased) + - 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 + - Add Open Graph and Twitter Card data to all pages + - Fix API project lookups when querying with a namespace with dots (Stan Hu) + - Enable forcing Two-Factor authentication sitewide, with optional grace period + - Import GitHub Pull Requests into GitLab + - Change single user API endpoint to return more detailed data (Michael Potthoff) + - Update version check images to use SVG + - Validate README format before displaying + - Enable Microsoft Azure OAuth2 support (Janis Meybohm) + - Properly set task-list class on single item task lists + - Add file finder feature in tree view (Kyungchul Shin) + - Ajax filter by message for commits page + - API: Add support for deleting a tag via the API (Robert Schilling) + - Allow subsequent validations in CI Linter + +v 8.3.3 + - Preserve CE behavior with JIRA integration by only calling API if URL is set + - Fix duplicated branch creation/deletion events when using Web UI (Stan Hu) + - Add configurable LDAP server query timeout + - Get "Merge when build succeeds" to work when commits were pushed to MR target branch while builds were running + - Suppress e-mails on failed builds if allow_failure is set (Stan Hu) + - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu) + - Better support for referencing and closing issues in Asana service (Mike Wyatt) + - Enable "Add key" button when user fills in a proper key (Stan Hu) + - Fix error in processing reply-by-email messages (Jason Lee) + - Fix Error 500 when visiting build page of project with nil runners_token (Stan Hu) + - Use WOFF versions of SourceSansPro fonts + - Fix regression when builds were not generated for tags created through web/api interface + +v 8.3.2 - Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu) - - Enable "Add key" button when user fills in a proper key + - Add support for Google reCAPTCHA in user registration v 8.3.1 - Fix Error 500 when global milestones have slashes (Stan Hu) - Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu) - Fix LDAP identity and user retrieval when special characters are used - Move Sidekiq-cron configuration to gitlab.yml - - Enable forcing Two-Factor authentication sitewide, with optional grace period v 8.3.0 - Bump rack-attack to 4.3.1 for security fix (Stan Hu) @@ -86,6 +124,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) @@ -144,6 +184,8 @@ v 8.2.0 - Allow to define cache in `.gitlab-ci.yml` - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page + - Use issue editor as cross reference comment author when issue is edited with a new mention. + - Add graphs of commits ahead and behind default branch (Jeff Stubler) - Improve personal snippet access workflow (Douglas Alexandre) - [API] Add ability to fetch the commit ID of the last commit that actually touched a file - Fix omniauth documentation setting for omnibus configuration (Jon Cairns) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index d48d3702aed..a04abec9149 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.9 +2.6.10 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 4b9fcbec101..be14282b7ff 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.5.1 +0.5.3 @@ -22,6 +22,7 @@ gem 'devise', '~> 3.5.3' gem 'devise-async', '~> 0.9.0' gem 'doorkeeper', '~> 2.2.0' gem 'omniauth', '~> 1.2.2' +gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-facebook', '~> 3.0.0' @@ -32,7 +33,7 @@ gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-saml', '~> 1.4.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' -gem 'omniauth_crowd' +gem 'omniauth_crowd', '~> 2.2.0' gem 'rack-oauth2', '~> 1.2.1' # reCAPTCHA protection @@ -48,7 +49,7 @@ gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.20' +gem "gitlab_git", '~> 7.2.22' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -66,10 +67,6 @@ gem 'grape', '~> 0.13.0' gem 'grape-entity', '~> 0.4.2' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' -# Format dates and times -# based on human-friendly examples -gem "stamp", '~> 0.6.0' - # Pagination gem "kaminari", "~> 0.16.3" @@ -169,10 +166,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" @@ -200,7 +197,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' diff --git a/Gemfile.lock b/Gemfile.lock index c4cadbafa26..db0104eee84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,7 +49,7 @@ GEM addressable (2.3.8) after_commit_queue (1.3.0) activerecord (>= 3.0) - allocations (1.0.1) + allocations (1.0.3) annotate (2.6.10) activerecord (>= 3.2, <= 4.3) rake (~> 10.4) @@ -66,7 +66,7 @@ GEM attr_encrypted (1.3.4) encryptor (>= 1.3.0) attr_required (1.0.0) - autoprefixer-rails (6.1.2) + autoprefixer-rails (6.2.3) execjs json awesome_print (1.2.0) @@ -82,9 +82,9 @@ 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) + bootstrap-sass (3.3.6) + autoprefixer-rails (>= 5.2.1) + sass (>= 3.3.4) brakeman (3.1.4) erubis (~> 2.6) fastercsv (~> 1.5) @@ -106,7 +106,7 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (8.2.1) - cal-heatmap-rails (0.0.1) + cal-heatmap-rails (3.5.1) capybara (2.4.4) mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -443,6 +443,10 @@ GEM omniauth (1.2.2) hashie (>= 1.2, < 4) rack (~> 1.0) + omniauth-azure-oauth2 (0.0.6) + jwt (~> 1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) omniauth-bitbucket (0.0.2) multi_json (~> 1.7) omniauth (~> 1.1) @@ -730,7 +734,6 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) - stamp (0.6.0) state_machines (0.4.0) state_machines-activemodel (0.3.0) activemodel (~> 4.1) @@ -843,13 +846,13 @@ DEPENDENCIES benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) - bootstrap-sass (~> 3.0) + 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) @@ -859,7 +862,7 @@ DEPENDENCIES 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) @@ -883,7 +886,7 @@ DEPENDENCIES github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) gitlab_emoji (~> 0.2.0) - gitlab_git (~> 7.2.20) + gitlab_git (~> 7.2.22) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.1.0) @@ -916,6 +919,7 @@ DEPENDENCIES oauth2 (~> 1.0.0) octokit (~> 3.7.0) omniauth (~> 1.2.2) + omniauth-azure-oauth2 (~> 0.0.6) omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) @@ -926,7 +930,7 @@ DEPENDENCIES omniauth-saml (~> 1.4.0) omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) - omniauth_crowd + omniauth_crowd (~> 2.2.0) org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) @@ -973,7 +977,6 @@ DEPENDENCIES spring-commands-spinach (~> 1.0.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 2.12.3) - stamp (~> 0.6.0) state_machines-activerecord (~> 0.3.0) task_list (~> 1.0.2) teaspoon (~> 1.0.0) @@ -994,4 +997,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.10.6 + 1.11.2 @@ -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 @@ -3,5 +3,5 @@ # lib/support/init.d, which call scripts in bin/ . # web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -q metrics +worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default # mail_room: bundle exec mail_room -q -c config/mail_room.yml diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt index a9b845ed1d4..df187637e18 100755 --- a/app/assets/fonts/OFL.txt +++ b/app/assets/fonts/OFL.txt @@ -1,7 +1,8 @@ -Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. +Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf Binary files differdeleted file mode 100644 index 9c9b5cb7f03..00000000000 --- a/app/assets/fonts/SourceSansPro-Black.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff b/app/assets/fonts/SourceSansPro-Black.ttf.woff Binary files differnew file mode 100755 index 00000000000..b7e86200927 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Black.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf b/app/assets/fonts/SourceSansPro-BlackIt.ttf Binary files differdeleted file mode 100644 index 294ce5abe8f..00000000000 --- a/app/assets/fonts/SourceSansPro-BlackIt.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff Binary files differnew file mode 100755 index 00000000000..c3314b1ef06 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-BlackItalic.ttf b/app/assets/fonts/SourceSansPro-BlackItalic.ttf Binary files differdeleted file mode 100755 index c719243c0d6..00000000000 --- a/app/assets/fonts/SourceSansPro-BlackItalic.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf Binary files differdeleted file mode 100644 index 5d65c93242f..00000000000 --- a/app/assets/fonts/SourceSansPro-Bold.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff b/app/assets/fonts/SourceSansPro-Bold.ttf.woff Binary files differnew file mode 100755 index 00000000000..d1d40f840f8 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Bold.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf b/app/assets/fonts/SourceSansPro-BoldIt.ttf Binary files differdeleted file mode 100644 index 3decd130070..00000000000 --- a/app/assets/fonts/SourceSansPro-BoldIt.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff Binary files differnew file mode 100755 index 00000000000..ef6ff514d3a --- /dev/null +++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-BoldItalic.ttf b/app/assets/fonts/SourceSansPro-BoldItalic.ttf Binary files differdeleted file mode 100755 index d20dd0c5eca..00000000000 --- a/app/assets/fonts/SourceSansPro-BoldItalic.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf Binary files differdeleted file mode 100644 index 253eafa3783..00000000000 --- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff Binary files differnew file mode 100755 index 00000000000..1e6c94d9eb3 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf Binary files differdeleted file mode 100644 index 00d7e9a7aa8..00000000000 --- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff Binary files differnew file mode 100755 index 00000000000..7a408b1ec73 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf b/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf Binary files differdeleted file mode 100755 index 2c34f3b8dc4..00000000000 --- a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-It.ttf b/app/assets/fonts/SourceSansPro-It.ttf Binary files differdeleted file mode 100644 index f7af5377595..00000000000 --- a/app/assets/fonts/SourceSansPro-It.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff b/app/assets/fonts/SourceSansPro-It.ttf.woff Binary files differnew file mode 100755 index 00000000000..4d54bc95718 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-It.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-Italic.ttf b/app/assets/fonts/SourceSansPro-Italic.ttf Binary files differdeleted file mode 100755 index e5a1a86e631..00000000000 --- a/app/assets/fonts/SourceSansPro-Italic.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf Binary files differdeleted file mode 100644 index 83a0a336661..00000000000 --- a/app/assets/fonts/SourceSansPro-Light.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff b/app/assets/fonts/SourceSansPro-Light.ttf.woff Binary files differnew file mode 100755 index 00000000000..1706d57d3c5 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Light.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf b/app/assets/fonts/SourceSansPro-LightIt.ttf Binary files differdeleted file mode 100644 index f18827985ef..00000000000 --- a/app/assets/fonts/SourceSansPro-LightIt.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff Binary files differnew file mode 100755 index 00000000000..87378d6c609 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-LightItalic.ttf b/app/assets/fonts/SourceSansPro-LightItalic.ttf Binary files differdeleted file mode 100755 index 88a6778d24f..00000000000 --- a/app/assets/fonts/SourceSansPro-LightItalic.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf Binary files differdeleted file mode 100644 index 44486cdc670..00000000000 --- a/app/assets/fonts/SourceSansPro-Regular.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff b/app/assets/fonts/SourceSansPro-Regular.ttf.woff Binary files differnew file mode 100755 index 00000000000..460ab12a638 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Regular.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf Binary files differdeleted file mode 100644 index 86b00c067e0..00000000000 --- a/app/assets/fonts/SourceSansPro-Semibold.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff Binary files differnew file mode 100755 index 00000000000..43379631b2d --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf Binary files differdeleted file mode 100644 index 13d66a1fc45..00000000000 --- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff Binary files differnew file mode 100755 index 00000000000..232c2048ae7 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff diff --git a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf b/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf Binary files differdeleted file mode 100755 index 2c5ad3008c3..00000000000 --- a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf +++ /dev/null diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png Binary files differnew file mode 100644 index 00000000000..a82c751e001 --- /dev/null +++ b/app/assets/images/auth_buttons/azure_64.png diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index affab5bb030..c095e5ae2b1 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,9 +38,9 @@ #= require shortcuts_dashboard_navigation #= require shortcuts_issuable #= require shortcuts_network -#= require cal-heatmap #= require jquery.nicescroll.min #= require_tree . +#= require fuzzaldrin-plus.min window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee index 917228bd276..f2fd2a775a4 100644 --- a/app/assets/javascripts/branch-graph.js.coffee +++ b/app/assets/javascripts/branch-graph.js.coffee @@ -66,7 +66,7 @@ class @BranchGraph r.rect(40, 0, 30, @barHeight).attr fill: "#444" for day, mm in @days - if cuday isnt day[0] + if cuday isnt day[0] || cumonth isnt day[1] # Dates r.text(55, @offsetY + @unitTime * mm, day[0]) .attr( 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/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index c183e78e513..ffd3627b1b0 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -1,15 +1,5 @@ class @CommitsList - @data = - ref: null - limit: 0 - offset: 0 - @disable = false - - @showProgress: -> - $('.loading').show() - - @hideProgress: -> - $('.loading').hide() + @timer = null @init: (ref, limit) -> $("body").on "click", ".day-commits-table li.commit", (event) -> @@ -18,38 +8,32 @@ class @CommitsList e.stopPropagation() return false - @data.ref = ref - @data.limit = limit - @data.offset = limit + Pager.init limit, false + + @content = $("#commits-list") + @searchField = $("#commits-search") + @initSearch() - this.initLoadMore() - this.showProgress() + @initSearch: -> + @timer = null + @searchField.keyup => + clearTimeout(@timer) + @timer = setTimeout(@filterResults, 500) + + @filterResults: => + form = $(".commits-search-form") + search = @searchField.val() + commitsUrl = form.attr("action") + '?' + form.serialize() + @content.fadeTo('fast', 0.5) - @getOld: -> - this.showProgress() $.ajax type: "GET" - url: location.href - data: @data - complete: this.hideProgress - success: (data) -> - CommitsList.append(data.count, data.html) + url: form.attr("action") + data: form.serialize() + complete: => + @content.fadeTo('fast', 1.0) + success: (data) => + @content.html(data.html) + # Change url so if user reload a page - search results are saved + history.replaceState {page: commitsUrl}, document.title, commitsUrl dataType: "json" - - @append: (count, html) -> - $("#commits-list").append(html) - if count > 0 - @data.offset += count - else - @disable = true - - @initLoadMore: -> - $(document).unbind('scroll') - $(document).endlessScroll - bottomPixels: 400 - fireDelay: 1000 - fireOnce: true - ceaseFire: => - @disable - callback: => - this.getOld() diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 69e061ce6e9..58d6b9d4060 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -87,7 +87,9 @@ class Dispatcher new GroupAvatar() when 'projects:tree:show' new TreeView() - shortcut_handler = new ShortcutsNavigation() + shortcut_handler = new ShortcutsTree() + when 'projects:find_file:show' + shortcut_handler = true when 'projects:blob:show' new LineHighlighter() shortcut_handler = new ShortcutsNavigation() diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index 30a35a04339..c714c0fa939 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -66,7 +66,7 @@ class @DropzoneInput success: (header, response) -> child = $(dropzone[0]).children("textarea") - $(child).val $(child).val() + formatLink(response.link) + "\n" + $(child).val $(child).val() + response.link.markdown + "\n" return error: (temp, errorMessage) -> @@ -99,11 +99,6 @@ class @DropzoneInput child = $(dropzone[0]).children("textarea") - formatLink = (link) -> - text = "[#{link.alt}](#{link.url})" - text = "!#{text}" if link.is_image - text - handlePaste = (event) -> pasteEvent = event.originalEvent if pasteEvent.clipboardData and pasteEvent.clipboardData.items @@ -162,7 +157,7 @@ class @DropzoneInput closeAlertMessage() success: (e, textStatus, response) -> - insertToTextArea(filename, formatLink(response.responseJSON.link)) + insertToTextArea(filename, response.responseJSON.link.markdown) error: (response) -> showError(response.responseJSON.message) @@ -202,8 +197,3 @@ class @DropzoneInput e.preventDefault() $(@).closest('.gfm-form').find('.div-dropzone').click() return - - formatLink: (link) -> - text = "[#{link.alt}](#{link.url})" - text = "!#{text}" if link.is_image - text diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 7967892f856..4718bcf7a1e 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -34,7 +34,7 @@ GitLab.GfmAutoComplete = searchKey: 'search' callbacks: beforeSave: (members) -> - $.map members, (m) -> + $.map members, (m) -> title = m.name title += " (#{m.count})" if m.count @@ -50,7 +50,7 @@ GitLab.GfmAutoComplete = insertTpl: '${atwho-at}${id}' callbacks: beforeSave: (issues) -> - $.map issues, (i) -> + $.map issues, (i) -> id: i.iid title: sanitize(i.title) search: "#{i.iid} #{i.title}" @@ -63,12 +63,12 @@ GitLab.GfmAutoComplete = insertTpl: '${atwho-at}${id}' callbacks: beforeSave: (merges) -> - $.map merges, (m) -> + $.map merges, (m) -> id: m.iid title: sanitize(m.title) search: "#{m.iid} #{m.title}" - input.one 'focus', => + if @dataSource $.getJSON(@dataSource).done (data) -> # load members input.atwho 'load', '@', data.members 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/project_find_file.js.coffee b/app/assets/javascripts/project_find_file.js.coffee new file mode 100644 index 00000000000..0dd32352c34 --- /dev/null +++ b/app/assets/javascripts/project_find_file.js.coffee @@ -0,0 +1,125 @@ +class @ProjectFindFile
+ constructor: (@element, @options)->
+ @filePaths = {}
+ @inputElement = @element.find(".file-finder-input")
+
+ # init event
+ @initEvent()
+
+ # focus text input box
+ @inputElement.focus()
+
+ # load file list
+ @load(@options.url)
+
+ # init event
+ initEvent: ->
+ @inputElement.off "keyup"
+ @inputElement.on "keyup", (event) =>
+ target = $(event.target)
+ value = target.val()
+ oldValue = target.data("oldValue") ? ""
+
+ if value != oldValue
+ target.data("oldValue", value)
+ @findFile()
+ @element.find("tr.tree-item").eq(0).addClass("selected").focus()
+
+ @element.find(".tree-content-holder .tree-table").on "click", (event) ->
+ if (event.target.nodeName != "A")
+ path = @element.find(".tree-item-file-name a", this).attr("href")
+ location.href = path if path
+
+ # find file
+ findFile: ->
+ searchText = @inputElement.val()
+ result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths
+ @renderList result, searchText
+
+ # files pathes load
+ load: (url) ->
+ $.ajax
+ url: url
+ method: "get"
+ dataType: "json"
+ success: (data) =>
+ @element.find(".loading").hide()
+ @filePaths = data
+ @findFile()
+ @element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus()
+
+ # render result
+ renderList: (filePaths, searchText) ->
+ @element.find(".tree-table > tbody").empty()
+
+ for filePath, i in filePaths
+ break if i == 20
+
+ if searchText
+ matches = fuzzaldrinPlus.match(filePath, searchText)
+
+ blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}"
+
+ html = @makeHtml filePath, matches, blobItemUrl
+ @element.find(".tree-table > tbody").append(html)
+
+ # highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
+ highlighter = (element, text, matches) ->
+ lastIndex = 0
+ highlightText = ""
+ matchedChars = []
+
+ for matchIndex in matches
+ unmatched = text.substring(lastIndex, matchIndex)
+
+ if unmatched
+ element.append(matchedChars.join("").bold()) if matchedChars.length
+ matchedChars = []
+ element.append(document.createTextNode(unmatched))
+
+ matchedChars.push(text[matchIndex])
+ lastIndex = matchIndex + 1
+
+ element.append(matchedChars.join("").bold()) if matchedChars.length
+ element.append(document.createTextNode(text.substring(lastIndex)))
+
+ # make tbody row html
+ makeHtml: (filePath, matches, blobItemUrl) ->
+ $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>")
+ if matches
+ $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl))
+ else
+ $tr.find("a").attr("href", blobItemUrl).text(filePath)
+
+ return $tr
+
+ selectRow: (type) ->
+ rows = @element.find(".files-slider tr.tree-item")
+ selectedRow = @element.find(".files-slider tr.tree-item.selected")
+
+ if rows && rows.length > 0
+ if selectedRow && selectedRow.length > 0
+ if type == "UP"
+ next = selectedRow.prev()
+ else if type == "DOWN"
+ next = selectedRow.next()
+
+ if next.length > 0
+ selectedRow.removeClass "selected"
+ selectedRow = next
+ else
+ selectedRow = rows.eq(0)
+ selectedRow.addClass("selected").focus()
+
+ selectRowUp: =>
+ @selectRow "UP"
+
+ selectRowDown: =>
+ @selectRow "DOWN"
+
+ goToTree: =>
+ location.href = @options.treeUrl
+
+ goToBlob: =>
+ path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href")
+ location.href = path if path
diff --git a/app/assets/javascripts/shortcuts_find_file.js.coffee b/app/assets/javascripts/shortcuts_find_file.js.coffee new file mode 100644 index 00000000000..311e80bae19 --- /dev/null +++ b/app/assets/javascripts/shortcuts_find_file.js.coffee @@ -0,0 +1,19 @@ +#= require shortcuts_navigation + +class @ShortcutsFindFile extends ShortcutsNavigation + constructor: (@projectFindFile) -> + super() + _oldStopCallback = Mousetrap.stopCallback + # override to fire shortcuts action when focus in textbox + Mousetrap.stopCallback = (event, element, combo) => + if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter') + # when press up/down key in textbox, cusor prevent to move to home/end + event.preventDefault() + return false + + return _oldStopCallback(event, element, combo) + + Mousetrap.bind('up', @projectFindFile.selectRowUp) + Mousetrap.bind('down', @projectFindFile.selectRowDown) + Mousetrap.bind('esc', @projectFindFile.goToTree) + Mousetrap.bind('enter', @projectFindFile.goToBlob) diff --git a/app/assets/javascripts/shortcuts_tree.coffee b/app/assets/javascripts/shortcuts_tree.coffee new file mode 100644 index 00000000000..ba0839c9fc0 --- /dev/null +++ b/app/assets/javascripts/shortcuts_tree.coffee @@ -0,0 +1,4 @@ +class @ShortcutsTree extends ShortcutsNavigation + constructor: -> + super() + Mousetrap.bind('t', -> ShortcutsTree.findAndFollowLink('.shortcuts-find-file')) 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/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee index a1462cf3cae..e1c5446eaac 100644 --- a/app/assets/javascripts/zen_mode.js.coffee +++ b/app/assets/javascripts/zen_mode.js.coffee @@ -1,56 +1,80 @@ +# Zen Mode (full screen) textarea +# +#= provides zen_mode:enter +#= provides zen_mode:leave +# +#= require jquery.scrollTo #= require dropzone #= require mousetrap #= require mousetrap/pause - +# +# ### Events +# +# `zen_mode:enter` +# +# Fired when the "Edit in fullscreen" link is clicked. +# +# **Synchronicity** Sync +# **Bubbles** Yes +# **Cancelable** No +# **Target** a.js-zen-enter +# +# `zen_mode:leave` +# +# Fired when the "Leave Fullscreen" link is clicked. +# +# **Synchronicity** Sync +# **Bubbles** Yes +# **Cancelable** No +# **Target** a.js-zen-leave +# class @ZenMode constructor: -> - @active_zen_area = null - @active_checkbox = null - @scroll_position = 0 - - $(window).scroll => - if not @active_checkbox - @scroll_position = window.pageYOffset + @active_backdrop = null + @active_textarea = null - $('body').on 'click', '.zen-enter-link', (e) => + $(document).on 'click', '.js-zen-enter', (e) -> e.preventDefault() - $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true).change() + $(e.currentTarget).trigger('zen_mode:enter') - $('body').on 'click', '.zen-leave-link', (e) => + $(document).on 'click', '.js-zen-leave', (e) -> e.preventDefault() - $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false).change() - - $('body').on 'change', '.zen-toggle-comment', (e) => - checkbox = e.currentTarget - if checkbox.checked - # Disable other keyboard shortcuts in ZEN mode - Mousetrap.pause() - @updateActiveZenArea(checkbox) - else - @exitZenMode() - - $(document).on 'keydown', (e) => - if e.keyCode is 27 # Esc - @exitZenMode() + $(e.currentTarget).trigger('zen_mode:leave') + + $(document).on 'zen_mode:enter', (e) => + @enter(e.target.parentNode) + $(document).on 'zen_mode:leave', (e) => + @exit() + + $(document).on 'keydown', (e) -> + if e.keyCode == 27 # Esc e.preventDefault() + $(document).trigger('zen_mode:leave') + + enter: (backdrop) -> + Mousetrap.pause() + + @active_backdrop = $(backdrop) + @active_backdrop.addClass('fullscreen') + + @active_textarea = @active_backdrop.find('textarea') - updateActiveZenArea: (checkbox) => - @active_checkbox = $(checkbox) - @active_checkbox.prop('checked', true) - @active_zen_area = @active_checkbox.parent().find('textarea') # Prevent a user-resized textarea from persisting to fullscreen - @active_zen_area.removeAttr('style') - @active_zen_area.focus() + @active_textarea.removeAttr('style') + @active_textarea.focus() - exitZenMode: => - if @active_zen_area isnt null + exit: -> + if @active_textarea Mousetrap.unpause() - @active_checkbox.prop('checked', false) - @active_zen_area = null - @active_checkbox = null - @restoreScroll(@scroll_position) - # Enable dropzone when leaving ZEN mode + + @active_textarea.closest('.zen-backdrop').removeClass('fullscreen') + + @scrollTo(@active_textarea) + + @active_textarea = null + @active_backdrop = null + Dropzone.forElement('.div-dropzone').enable() - restoreScroll: (y) -> - window.scrollTo(window.pageXOffset, y) + scrollTo: (zen_area) -> + $.scrollTo(zen_area, 0, offset: -150) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 206d39cc9b3..fa0e70847f3 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -72,6 +72,15 @@ > p:last-child { margin-bottom: 0; } + + .block-controls { + float: right; + + .control { + float: left; + margin-left: 10px; + } + } } .cover-block { 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/fonts.scss b/app/assets/stylesheets/framework/fonts.scss index e214567eca1..20988f7b430 100644 --- a/app/assets/stylesheets/framework/fonts.scss +++ b/app/assets/stylesheets/framework/fonts.scss @@ -3,23 +3,23 @@ font-family: 'Source Sans Pro'; font-style: normal; font-weight: 300; - src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf'); + src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf.woff'); } @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 400; - src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf'); + src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf.woff'); } @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 600; - src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf'); + src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf.woff'); } @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 700; - src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf'); + src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf.woff'); } 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/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 32e2c020e06..002bd7e8ca5 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -1,9 +1,5 @@ .zennable { - .zen-toggle-comment { - display: none; - } - - .zen-enter-link { + a.js-zen-enter { color: $gl-gray; position: absolute; top: 0px; @@ -11,7 +7,7 @@ line-height: 40px; } - .zen-leave-link { + a.js-zen-leave { display: none; color: $gl-text-color; position: absolute; @@ -25,62 +21,41 @@ } } - // Hide the Enter link when we're in Zen mode - input:checked ~ .zen-backdrop .zen-enter-link { - display: none; - } - - // Show the Leave link when we're in Zen mode - input:checked ~ .zen-backdrop .zen-leave-link { - display: block; - position: absolute; - top: 0; - } - - input:checked ~ .zen-backdrop { - background-color: white; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 1031; - - textarea { - border: none; - box-shadow: none; - border-radius: 0; - color: #000; - font-size: 20px; - line-height: 26px; - padding: 30px; - display: block; - outline: none; - resize: none; - height: 100vh; - max-width: 900px; - margin: 0 auto; + .zen-backdrop { + &.fullscreen { + background-color: white; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 1031; + + textarea { + border: none; + box-shadow: none; + border-radius: 0; + color: #000; + font-size: 20px; + line-height: 26px; + padding: 30px; + display: block; + outline: none; + resize: none; + height: 100vh; + max-width: 900px; + margin: 0 auto; + } + + a.js-zen-enter { + display: none; + } + + a.js-zen-leave { + display: block; + position: absolute; + top: 0; + } } } - - // Make the color of the placeholder text in the Zenned-out textarea darker, - // so it becomes visible - - input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder { - color: #A8A8A8; - } - - input:checked ~ .zen-backdrop textarea:-moz-placeholder { - color: #A8A8A8; - opacity: 1; - } - - input:checked ~ .zen-backdrop textarea::-moz-placeholder { - color: #A8A8A8; - opacity: 1; - } - - input:checked ~ .zen-backdrop textarea:-ms-input-placeholder { - color: #A8A8A8; - } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index c9dfcff6290..800df95cff3 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -28,10 +28,6 @@ } } -.commits-feed-holder { - float: right; -} - li.commit { list-style: none; @@ -122,3 +118,59 @@ li.commit { color: $gl-gray; } } + +.divergence-graph { + padding: 12px 12px 0 0; + float: right; + + .graph-side { + position: relative; + width: 80px; + height: 22px; + padding: 5px 0 13px; + float: left; + + .bar { + position: absolute; + height: 4px; + background-color: #ccc; + } + + .bar-behind { + right: 0; + border-radius: 3px 0 0 3px; + } + + .bar-ahead { + left: 0; + border-radius: 0 3px 3px 0; + } + + .count { + padding-top: 6px; + padding-bottom: 0px; + font-size: 12px; + color: #333; + display: block; + } + + .count-behind { + padding-right: 4px; + text-align: right; + } + + .count-ahead { + padding-left: 4px; + text-align: left; + } + } + + .graph-separator { + position: relative; + width: 1px; + height: 18px; + margin: 5px 0 0; + float: left; + background-color: #ccc; + } +} diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 282aaf2219b..984b4b91216 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -138,6 +138,7 @@ */ .event-last-push { overflow: auto; + width: 100%; .event-last-push-text { @include str-truncated(100%); padding: 5px 0; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 9da273a0b6b..d4b44004f4f 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -94,8 +94,16 @@ } .cross-project-reference { - font-weight: bold; color: $gl-link-color; + + span { + white-space: nowrap; + width: 85%; + overflow: hidden; + position: relative; + display: inline-block; + text-overflow: ellipsis; + } button { float: right; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index cff3edb7ed2..d32509b7d49 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; @@ -404,10 +411,15 @@ ul.nav.nav-projects-tabs { } } +.last-push-widget { + margin-top: -1px; +} + .top-area { border-bottom: 1px solid #EEE; margin: 0 -16px; padding: 0 $gl-padding; + height: 42px; ul.left-top-menu { display: inline-block; @@ -514,6 +526,7 @@ pre.light-well { .projects-search-form { margin: -$gl-padding; padding: $gl-padding; + padding-bottom: 0; margin-bottom: 0px; input { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index d4ab6967ccd..97505edeabf 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,5 +1,13 @@ .tree-holder { + .file-finder { + width: 50%; + .file-finder-input { + width: 95%; + display: inline-block; + } + } + .tree-table { margin-bottom: 0; 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 3c332adf1fa..44d06b6a647 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -69,12 +69,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :max_artifacts_size, :metrics_enabled, :metrics_host, - :metrics_database, - :metrics_username, - :metrics_password, + :metrics_port, :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/application_controller.rb b/app/controllers/application_controller.rb index d9a37a4d45f..81cb1367e2c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -286,7 +286,7 @@ class ApplicationController < ActionController::Base end def set_filters_params - params[:sort] ||= 'created_desc' + params[:sort] ||= 'id_desc' params[:scope] = 'all' if params[:scope].blank? params[:state] = 'opened' if params[:state].blank? diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index e782a51e7eb..a7af3cb8345 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -6,11 +6,13 @@ module Ci end def create - if params[:content].blank? + @content = params[:content] + + if @content.blank? @status = false @error = "Please provide content of .gitlab-ci.yml" else - @config_processor = Ci::GitlabCiYamlProcessor.new params[:content] + @config_processor = Ci::GitlabCiYamlProcessor.new(@content) @stages = @config_processor.stages @builds = @config_processor.builds @status = true 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/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 3c2849a7601..4db3b3bf23d 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -9,6 +9,11 @@ class Projects::BranchesController < Projects::ApplicationController @sort = params[:sort] || 'name' @branches = @repository.branches_sorted_by(@sort) @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE) + + @max_commits = @branches.reduce(0) do |memo, branch| + diverging_commit_counts = repository.diverging_commit_counts(branch) + [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max + end end def recent 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 04a88990bf4..bf5b54c8cb7 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -8,10 +8,16 @@ class Projects::CommitsController < Projects::ApplicationController before_action :authorize_download_code! def show - @repo = @project.repository @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i + search = params[:search] + + @commits = + if search.present? + @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact + else + @repository.commits(@ref, @path, @limit, @offset) + end - @commits = @repo.commits(@ref, @path, @limit, @offset) @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb new file mode 100644 index 00000000000..54a0c447aee --- /dev/null +++ b/app/controllers/projects/find_file_controller.rb @@ -0,0 +1,26 @@ +# Controller for viewing a repository's file structure
+class Projects::FindFileController < Projects::ApplicationController
+ include ExtractsPath
+ include ActionView::Helpers::SanitizeHelper
+ include TreeHelper
+
+ before_action :require_non_empty_project
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
+
+ def show
+ return render_404 unless @repository.commit(@ref)
+
+ respond_to do |format|
+ format.html
+ end
+ end
+
+ def list
+ file_paths = @repo.ls_files(@ref)
+
+ respond_to do |format|
+ format.json { render json: file_paths }
+ end
+ end
+end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e3c4aa4873a..d85ef229254 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -160,18 +160,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_check - @merge_request.check_if_can_be_merged if @merge_request.unchecked? - puts @merge_request.merge_status - respond_to do |format| - format.json do - render json: { - can_be_merged: @merge_request.merge_status == :can_be_merged - } - end - format.html do - render partial: "projects/merge_requests/widget/show.html.haml", layout: false - end - end + @merge_request.check_if_can_be_merged + + render partial: "projects/merge_requests/widget/show.html.haml", layout: false end def cancel_merge_when_build_succeeds diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index c4e18c17077..a8f091819ca 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -20,6 +20,8 @@ class Projects::RefsController < Projects::ApplicationController namespace_project_network_path(@project.namespace, @project, @id, @options) when "graphs" namespace_project_graph_path(@project.namespace, @project, @id) + when "find_file" + namespace_project_find_file_path(@project.namespace, @project, @id) when "graphs_commits" commits_namespace_project_graph_path(@project.namespace, @project, @id) else diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3004722bce0..935f7d75c6a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -8,7 +8,7 @@ class ProjectsController < ApplicationController before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? # Authorize - before_action :authorize_admin_project!, only: [:edit, :update] + before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping] before_action :event_filter, only: [:show, :activity] layout :determine_layout @@ -166,6 +166,15 @@ class ProjectsController < ApplicationController end end + def housekeeping + ::Projects::HousekeepingService.new(@project).execute + + respond_to do |format| + flash[:notice] = "Housekeeping successfully started." + format.html { redirect_to project_path(@project) } + end + end + def toggle_star current_user.toggle_star(@project) @project.reload diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index ee1006dea49..c48175a4c5a 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -7,7 +7,7 @@ class RegistrationsController < Devise::RegistrationsController end def create - if !Gitlab.config.recaptcha.enabled || verify_recaptcha + 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." diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index da4b35d322b..825f85199be 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -5,6 +5,7 @@ class SessionsController < Devise::SessionsController 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 @@ -108,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/issuable_finder.rb b/app/finders/issuable_finder.rb index 3d5e8b6fbe7..4d56b48e3f8 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -79,9 +79,9 @@ class IssuableFinder if project? @projects = project elsif current_user && params[:authorized_only].presence && !current_user_related? - @projects = current_user.authorized_projects + @projects = current_user.authorized_projects.reorder(nil) else - @projects = ProjectsFinder.new.execute(current_user) + @projects = ProjectsFinder.new.execute(current_user).reorder(nil) 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..2b9bad9c9ea 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 @@ -206,7 +206,7 @@ module ApplicationHelper element = content_tag :time, time.to_s, class: "#{html_class} js-timeago js-timeago-pending", datetime: time.getutc.iso8601, - title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), + title: time.in_time_zone.to_s(:medium), data: { toggle: 'tooltip', placement: placement, container: 'body' } unless skip_js diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 0cfc0565e84..de669e529a7 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,5 +1,5 @@ module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 80e2741b09a..a7080ddfefb 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -80,7 +80,7 @@ module IssuesHelper xml.link href: namespace_project_issue_url(issue.project.namespace, issue.project, issue) xml.title truncate(issue.title, length: 80) - xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") + xml.updated issue.created_at.xmlschema xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) xml.author do |author| xml.name issue.author_name @@ -99,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}", diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 791cb9e50bd..82f805fa444 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -27,35 +27,20 @@ module PageLayoutHelper # # Returns an HTML-safe String. def page_description(description = nil) - @page_description ||= page_description_default - if description.present? @page_description = description.squish - else + elsif @page_description.present? 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 + subject = @project || @user || @group + + image = subject.avatar_url if subject.present? + image || default end # Define or get attributes to be used as Twitter card metadata 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/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index bb12d43f397..241179b0212 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -19,7 +19,7 @@ module SortingHelper end def sort_title_recently_updated - 'Recently updated' + 'Last updated' end def sort_title_oldest_created @@ -27,7 +27,7 @@ module SortingHelper end def sort_title_recently_created - 'Recently created' + 'Last created' end def sort_title_milestone_soon @@ -63,11 +63,11 @@ module SortingHelper end def sort_value_oldest_created - 'created_asc' + 'id_asc' end def sort_value_recently_created - 'created_desc' + 'id_desc' end def sort_value_milestone_soon 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..6c6c2468374 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -27,9 +27,20 @@ # admin_notification_email :string(255) # shared_runners_enabled :boolean default(TRUE), not null # max_artifacts_size :integer default(100), not null -# runners_registration_token :string(255) -# require_two_factor_authentication :boolean default(TRUE) +# runners_registration_token :string +# require_two_factor_authentication :boolean default(FALSE) # two_factor_grace_period :integer default(48) +# metrics_enabled :boolean default(FALSE) +# metrics_host :string default("localhost") +# metrics_username :string +# metrics_password :string +# metrics_pool_size :integer default(16) +# metrics_timeout :integer default(10) +# metrics_method_call_threshold :integer default(10) +# recaptcha_enabled :boolean default(FALSE) +# recaptcha_site_key :string +# recaptcha_private_key :string +# metrics_port :integer default(8089) # class ApplicationSetting < ActiveRecord::Base @@ -44,24 +55,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..a4779d06de8 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # module Ci @@ -54,6 +55,8 @@ module Ci # To prevent db load megabytes of data from trace default_scope -> { select(Ci::Build.columns_without_lazy) } + before_destroy { project } + class << self def columns_without_lazy (column_names - LAZY_ATTRIBUTES).map do |column_name| @@ -145,10 +148,6 @@ module Ci end end - def project - commit.project - end - def project_id commit.project.id end @@ -194,8 +193,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 @@ -204,7 +206,7 @@ module Ci def trace trace = raw_trace - if project && trace.present? + if project && trace.present? && project.runners_token.present? trace.gsub(project.runners_token, 'xxxxxx') else trace @@ -212,8 +214,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 +233,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/ci/runner_project.rb b/app/models/ci/runner_project.rb index 93d9be144e8..7b16f207a26 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -2,11 +2,12 @@ # # Table name: ci_runner_projects # -# id :integer not null, primary key -# runner_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# runner_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # module Ci diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 23516709a41..bb98cd5c7da 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -2,12 +2,13 @@ # # Table name: ci_triggers # -# id :integer not null, primary key -# token :string(255) -# project_id :integer not null -# deleted_at :datetime -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# token :string(255) +# project_id :integer +# deleted_at :datetime +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # module Ci diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 56759d3e50f..7f6f497f325 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -3,12 +3,13 @@ # Table name: ci_variables # # id :integer not null, primary key -# project_id :integer not null +# project_id :integer # key :string(255) # value :text # encrypted_value :text # encrypted_value_salt :string(255) # encrypted_value_iv :string(255) +# gl_project_id :integer # module Ci diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 21c5c87bc3d..ff479493474 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,30 +1,35 @@ # == Schema Information # -# project_id integer -# status string -# finished_at datetime -# trace text -# created_at datetime -# updated_at datetime -# started_at datetime -# runner_id integer -# coverage float -# commit_id integer -# commands text -# job_id integer -# name string -# deploy boolean default: false -# options text -# allow_failure boolean default: false, null: false -# stage string -# trigger_request_id integer -# stage_idx integer -# tag boolean -# ref string -# user_id integer -# type string -# target_url string -# description string +# Table name: ci_builds +# +# id :integer not null, primary key +# project_id :integer +# status :string(255) +# finished_at :datetime +# trace :text +# created_at :datetime +# updated_at :datetime +# started_at :datetime +# runner_id :integer +# coverage :float +# commit_id :integer +# commands :text +# job_id :integer +# name :string(255) +# deploy :boolean default(FALSE) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# trigger_request_id :integer +# stage_idx :integer +# tag :boolean +# ref :string(255) +# user_id :integer +# type :string(255) +# target_url :string(255) +# description :string(255) +# artifacts_file :text +# gl_project_id :integer # class CommitStatus < ActiveRecord::Base diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 919833f6df5..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) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 6316ee208b5..98f71ae8cb0 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -51,8 +51,11 @@ module Mentionable else self.class.mentionable_attrs.each do |attr, options| text = send(attr) - options[:cache_key] = [self, attr] if options.delete(:cache) && self.persisted? - ext.analyze(text, options) + + context = options.dup + context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted? + + ext.analyze(text, context) end end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 7391a77383c..8b47b9e0abd 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -11,6 +11,7 @@ module Sortable default_scope { order_id_desc } scope :order_id_desc, -> { reorder(id: :desc) } + scope :order_id_asc, -> { reorder(id: :asc) } scope :order_created_desc, -> { reorder(created_at: :desc) } scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) } @@ -28,6 +29,8 @@ module Sortable when 'updated_desc' then order_updated_desc when 'created_asc' then order_created_asc when 'created_desc' then order_created_desc + when 'id_desc' then order_id_desc + when 'id_asc' then order_id_asc else all end diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index 12c934e2494..97f4f03a9a5 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # class GenericCommitStatus < CommitStatus diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index af1d7562ebe..7ee276255a0 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -121,9 +121,9 @@ class GlobalMilestone def expires_at if due_date if due_date.past? - "expired at #{due_date.stamp("Aug 21, 2011")}" + "expired on #{due_date.to_s(:medium)}" else - "expires at #{due_date.stamp("Aug 21, 2011")}" + "expires on #{due_date.to_s(:medium)}" end end end diff --git a/app/models/group.rb b/app/models/group.rb index 1b5b875a19e..5a31b46920c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # require 'carrierwave/orm/activerecord' @@ -50,10 +49,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 +120,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/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 22638057773..fa18ba5dbbe 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -15,6 +15,7 @@ # tag_push_events :boolean default(FALSE) # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) +# build_events :boolean default(FALSE), not null # class ProjectHook < WebHook diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 09bb3ee52a2..b333a337347 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -15,6 +15,7 @@ # tag_push_events :boolean default(FALSE) # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) +# build_events :boolean default(FALSE), not null # class ServiceHook < WebHook diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index 2f63c59b07e..d81512fae5d 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -15,6 +15,7 @@ # tag_push_events :boolean default(FALSE) # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) +# build_events :boolean default(FALSE), not null # class SystemHook < WebHook diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 40eb0e20b4b..d0aadfc330a 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -15,6 +15,7 @@ # tag_push_events :boolean default(FALSE) # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) +# build_events :boolean default(FALSE), not null # class WebHook < ActiveRecord::Base @@ -60,7 +61,7 @@ class WebHook < ActiveRecord::Base basic_auth: auth) end - [response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)] + [(response.code >= 200 && response.code < 300), ActionView::Base.full_sanitizer.sanitize(response.to_s)] rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e logger.error("WebHook Error => #{e}") [false, e.to_s] diff --git a/app/models/issue.rb b/app/models/issue.rb index 80ecd15077f..f52e47f3e62 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -33,7 +33,9 @@ class Issue < ActiveRecord::Base belongs_to :project validates :project, presence: true - scope :of_group, ->(group) { where(project_id: group.project_ids) } + scope :of_group, + ->(group) { where(project_id: group.projects.select(:id).reorder(nil)) } + scope :cared, ->(user) { where(assignee_id: user) } scope :open_for, ->(user) { opened.assigned_to(user) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f013018ec08..b2cb86dc1e5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -2,28 +2,28 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) -# merge_params :text (serialized to hash) -# merge_when_build_succeeds :boolean default(false), not null -# merge_user_id :integer +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime +# updated_at :datetime +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer +# description :text +# position :integer default(0) +# locked_at :datetime +# updated_by_id :integer +# merge_error :string(255) +# merge_params :text +# merge_when_build_succeeds :boolean default(FALSE), not null +# merge_user_id :integer # require Rails.root.join("app/models/commit") @@ -131,7 +131,7 @@ class MergeRequest < ActiveRecord::Base validate :validate_branches validate :validate_fork - scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) } + scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } @@ -229,6 +229,8 @@ class MergeRequest < ActiveRecord::Base end def check_if_can_be_merged + return unless unchecked? + can_be_merged = project.repository.can_be_merged?(source_sha, target_branch) @@ -252,7 +254,11 @@ class MergeRequest < ActiveRecord::Base end def mergeable? - open? && !work_in_progress? && can_be_merged? + return false unless open? && !work_in_progress? + + check_if_can_be_merged + + can_be_merged? end def gitlab_merge_status @@ -452,6 +458,10 @@ class MergeRequest < ActiveRecord::Base !source_branch_exists? || !target_branch_exists? end + def broken? + self.commits.blank? || branch_missing? || cannot_be_merged? + end + def can_be_merged_by?(user) ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) end @@ -497,8 +507,4 @@ class MergeRequest < ActiveRecord::Base def ci_commit @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project end - - def broken? - self.commits.blank? || branch_missing? || cannot_be_merged? - end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d8c7536cd31..c9a0ad8b9b6 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -22,6 +22,7 @@ class Milestone < ActiveRecord::Base include InternalId include Sortable + include Referable include StripAttribute belongs_to :project @@ -61,6 +62,27 @@ class Milestone < ActiveRecord::Base end end + def self.reference_pattern + nil + end + + def self.link_reference_pattern + super("milestones", /(?<milestone>\d+)/) + end + + def to_reference(from_project = nil) + escaped_title = self.title.gsub("]", "\\]") + + h = Gitlab::Application.routes.url_helpers + url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) + + "[#{escaped_title}](#{url})" + end + + def reference_link_text(from_project = nil) + self.title + end + def expired? if due_date due_date.past? @@ -90,9 +112,9 @@ class Milestone < ActiveRecord::Base def expires_at if due_date if due_date.past? - "expired at #{due_date.stamp("Aug 21, 2011")}" + "expired on #{due_date.to_s(:medium)}" else - "expires at #{due_date.stamp("Aug 21, 2011")}" + "expires on #{due_date.to_s(:medium)}" end end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index adafabbec07..bdb33f37495 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # class Namespace < ActiveRecord::Base diff --git a/app/models/note.rb b/app/models/note.rb index 1222d99cf1f..3d5b663c99f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -346,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..31990485f7d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -29,6 +29,13 @@ # import_source :string(255) # commit_count :integer default(0) # import_error :text +# ci_id :integer +# builds_enabled :boolean default(TRUE), not null +# shared_runners_enabled :boolean default(TRUE), not null +# runners_token :string +# build_coverage_regex :string +# build_allow_git_fetch :boolean default(TRUE), not null +# build_timeout :integer default(3600), not null # require 'carrierwave/orm/activerecord' @@ -43,6 +50,7 @@ class Project < ActiveRecord::Base include Sortable include AfterCommitQueue include CaseSensitivity + include TokenAuthenticatable extend Gitlab::ConfigHelper @@ -81,6 +89,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' @@ -185,10 +194,8 @@ class Project < ActiveRecord::Base if: ->(project) { project.avatar.present? && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - before_validation :set_runners_token_token - def set_runners_token_token - self.runners_token = SecureRandom.hex(15) if self.runners_token.blank? - end + add_authentication_token_field :runners_token + before_save :ensure_runners_token mount_uploader :avatar, AvatarUploader @@ -555,7 +562,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 +708,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 @@ -767,6 +781,8 @@ class Project < ActiveRecord::Base end def change_head(branch) + # Cached divergent commit counts are based on repository head + repository.expire_branch_cache gitlab_shell.update_repository_head(self.path_with_namespace, branch) reload_default_branch end @@ -883,4 +899,8 @@ class Project < ActiveRecord::Base return true unless forked? Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i) end + + def runners_token + ensure_runners_token! + end end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index e6e16058d41..792ad804575 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -16,7 +16,9 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # + require 'asana' class AsanaService < Service @@ -40,8 +42,8 @@ get the commit comment added to it. You can also close a task with a message containing: `fix #123456`. -You can find your Api Keys here: -http://developer.asana.com/documentation/#api_keys' +You can create a Personal Access Token here: +http://app.asana.com/-/account_api' end def to_param @@ -53,14 +55,12 @@ http://developer.asana.com/documentation/#api_keys' { type: 'text', name: 'api_key', - placeholder: 'User API token. User must have access to task, -all comments will be attributed to this user.' + placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.' }, { type: 'text', name: 'restrict_to_branch', - placeholder: 'Comma-separated list of branches which will be -automatically inspected. Leave blank to include all branches.' + placeholder: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' } ] end @@ -69,58 +69,58 @@ automatically inspected. Leave blank to include all branches.' %w(push) end + def client + @_client ||= begin + Asana::Client.new do |c| + c.authentication :access_token, api_key + end + end + end + def execute(data) return unless supported_events.include?(data[:object_kind]) - Asana.configure do |client| - client.api_key = api_key - end - - user = data[:user_name] + # check the branch restriction is poplulated and branch is not included branch = Gitlab::Git.ref_name(data[:ref]) - branch_restriction = restrict_to_branch.to_s - - # check the branch restriction is poplulated and branch is not included if branch_restriction.length > 0 && branch_restriction.index(branch).nil? return end + user = data[:user_name] project_name = project.name_with_namespace - push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name data[:commits].each do |commit| - check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg) + push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):" + check_commit(commit[:message], push_msg) end end def check_commit(message, push_msg) - task_list = [] - close_list = [] - - message.split("\n").each do |line| - # look for a task ID or a full Asana url - task_list.concat(line.scan(/#(\d+)/)) - task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/)) - # look for a word starting with 'fix' followed by a task ID - close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i)) - end - - # post commit to every taskid found - task_list.each do |taskid| - task = Asana::Task.find(taskid[0]) - - if task - task.create_story(text: push_msg + ' ' + message) - end - end - - # close all tasks that had 'fix(ed/es/ing) #:id' in them - close_list.each do |taskid| - task = Asana::Task.find(taskid.last) - - if task - task.modify(completed: true) + # matches either: + # - #1234 + # - https://app.asana.com/0/0/1234 + # optionally preceded with: + # - fix/ed/es/ing + # - close/s/d + # - closing + issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i + + message.scan(issue_finder).each do |tuple| + # tuple will be + # [ 'fix', 'id_from_url', 'id_from_pound' ] + taskid = tuple[2] || tuple[1] + + begin + task = Asana::Task.find_by_id(client, taskid) + task.add_comment(text: "#{push_msg} #{message}") + + if tuple[0] + task.update(completed: true) + end + rescue => e + Rails.logger.error(e.message) + next end end end diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index fb7e0c0fb0d..29d841faed8 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class AssemblaService < Service diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index aa8746beb80..9e7f642180e 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class BambooService < CiService diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 199ee3a9d0d..3efbfd2eec3 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require "addressable/uri" diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 8247c79fc33..f6313255cbb 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class BuildsEmailService < Service @@ -72,12 +73,16 @@ class BuildsEmailService < Service when 'success' !notify_only_broken_builds? when 'failed' - true + !allow_failure?(data) else false end end + def allow_failure?(data) + data[:build_allow_failure] == true + end + def all_recipients(data) all_recipients = recipients.split(',') diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index e591afdda64..6e8f0842524 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class CampfireService < Service diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 88186113c68..c3f70d1f972 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # # Base class for CI services diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 7c2027c18e6..88a3e9218cb 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class CustomIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 08e5ccb3855..b4724bb647e 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class DroneCiService < CiService diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 8f5d8b086eb..b831577cd97 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class EmailsOnPushService < Service diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index 74c57949b4d..b402b68665a 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class ExternalWikiService < Service diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 15c7c907f7e..8605ce66e48 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require "flowdock-git-hook" diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 202fee042e3..61babe9cfe5 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require "gemnasium/gitlab_service" diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index b64d97ce75d..33f0d7ea01a 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # # TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 9558292fea3..7aa04309f54 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class GitlabIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 1e1686a11c6..32a81808930 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class HipchatService < Service diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index d24aa317cf3..bd9b580038f 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require 'uri' diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 936e574cccd..ed201979d39 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class IssueTrackerService < Service diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index e216f406e1c..f6571fc063e 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class JiraService < IssueTrackerService @@ -39,15 +40,10 @@ class JiraService < IssueTrackerService end def help - line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\ + 'Setting `project_url`, `issues_url` and `new_issue_url` will '\ 'allow a user to easily navigate to the Jira issue tracker. See the '\ '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\ 'for details.' - - line2 = 'Support for referencing commits and automatic closing of Jira issues directly '\ - 'from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)' - - [line1, line2].join("\n\n") end def title @@ -120,6 +116,7 @@ class JiraService < IssueTrackerService end def test_settings + return unless api_url.present? result = JiraService.get( jira_api_test_url, headers: { @@ -217,6 +214,7 @@ class JiraService < IssueTrackerService end def send_message(url, message) + return unless api_url.present? result = JiraService.post( url, body: message, @@ -242,6 +240,7 @@ class JiraService < IssueTrackerService end def existing_comment?(issue_name, new_comment) + return unless api_url.present? result = JiraService.get( comment_url(issue_name), headers: { diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index ade9ee97873..c9a890c7e3f 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class PivotaltrackerService < Service diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 53edf522e9a..3d7e8bbee61 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class PushoverService < Service diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index dd9ba97ee1f..de974354c77 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class RedmineService < IssueTrackerService diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 375b4534d07..d89cf6d17b2 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class SlackService < Service diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index a63700693d7..b8e9416131a 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # class TeamcityService < CiService diff --git a/app/models/repository.rb b/app/models/repository.rb index a9bf4eb4033..d9ff71c01ed 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -92,9 +92,12 @@ class Repository commits end - def find_commits_by_message(query) + def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) + ref ||= root_ref + # Limited to 1000 commits for now, could be parameterized? - args = %W(#{Gitlab.config.git.bin_path} log --pretty=%H --max-count 1000 --grep=#{query}) + args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query}) + args = args.concat(%W(-- #{path})) if path.present? git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) commits = git_log_results.map { |c| commit(c) } @@ -176,17 +179,41 @@ class Repository cache.fetch(:size) { raw_repository.size } end + def diverging_commit_counts(branch) + root_ref_hash = raw_repository.rev_parse_target(root_ref).oid + cache.fetch(:"diverging_commit_counts_#{branch.name}") do + # Rugged seems to throw a `ReferenceError` when given branch_names rather + # than SHA-1 hashes + number_commits_behind = commits_between(branch.target, root_ref_hash).size + number_commits_ahead = commits_between(root_ref_hash, branch.target).size + + { behind: number_commits_behind, ahead: number_commits_ahead } + end + end + def cache_keys %i(size branch_names tag_names commit_count readme version contribution_guide changelog license) end + def branch_cache_keys + branches.map do |branch| + :"diverging_commit_counts_#{branch.name}" + end + end + def build_cache cache_keys.each do |key| unless cache.exist?(key) send(key) end end + + branches.each do |branch| + unless cache.exist?(:"diverging_commit_counts_#{branch.name}") + send(:diverging_commit_counts, branch) + end + end end def expire_tags_cache @@ -203,6 +230,14 @@ class Repository cache_keys.each do |key| cache.expire(key) end + + expire_branch_cache + end + + def expire_branch_cache + branches.each do |branch| + cache.expire(:"diverging_commit_counts_#{branch.name}") + end end def rebuild_cache @@ -210,6 +245,11 @@ class Repository cache.expire(key) send(key) end + + branches.each do |branch| + cache.expire(:"diverging_commit_counts_#{branch.name}") + diverging_commit_counts(branch) + end end def lookup_cache @@ -644,6 +684,11 @@ class Repository end end + def ls_files(ref) + actual_ref = ref || root_ref + raw_repository.ls_files(actual_ref) + end + private def cache diff --git a/app/models/service.rb b/app/models/service.rb index d3bf7f0ebd1..24f4bf7646e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # # To add new service you should build a class inherited from Service diff --git a/app/models/tree.rb b/app/models/tree.rb index 93b3246a668..e0e04d8859f 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -17,18 +17,16 @@ class Tree def readme return @readme if defined?(@readme) - available_readmes = blobs.select(&:readme?) + # Take the first previewable readme, or return nil if none is available or + # we can't preview any of them + readme_tree = blobs.find do |blob| + blob.readme? && (previewable?(blob.name) || plain?(blob.name)) + end - if available_readmes.count == 0 + if readme_tree.nil? return @readme = nil end - # Take the first previewable readme, or the first available readme, if we - # can't preview any of them - readme_tree = available_readmes.find do |readme| - previewable?(readme.name) - end || available_readmes.first - readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) git_repo = repository.raw_repository diff --git a/app/models/user.rb b/app/models/user.rb index df87f3b79bd..46b36c605b0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,62 +2,63 @@ # # Table name: users # -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# bio :string(255) -# failed_attempts :integer default(0) -# locked_at :datetime -# unlock_token :string(255) -# username :string(255) -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# last_credential_check_at :datetime -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string(255) default(""), not null -# notification_email :string(255) -# hide_no_password :boolean default(FALSE) -# password_automatically_set :boolean default(FALSE) -# location :string(255) -# encrypted_otp_secret :string(255) -# encrypted_otp_secret_iv :string(255) -# encrypted_otp_secret_salt :string(255) -# otp_required_for_login :boolean default(FALSE), not null -# otp_backup_codes :text -# public_email :string(255) default(""), not null -# dashboard :integer default(0) -# project_view :integer default(0) -# consumed_timestep :integer -# layout :integer default(0) -# hide_project_limit :boolean default(FALSE) +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# name :string(255) +# admin :boolean default(FALSE), not null +# projects_limit :integer default(10) +# skype :string(255) default(""), not null +# linkedin :string(255) default(""), not null +# twitter :string(255) default(""), not null +# authentication_token :string(255) +# theme_id :integer default(1), not null +# bio :string(255) +# failed_attempts :integer default(0) +# locked_at :datetime +# username :string(255) +# can_create_group :boolean default(TRUE), not null +# can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer +# last_credential_check_at :datetime +# avatar :string(255) +# confirmation_token :string(255) +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string(255) +# hide_no_ssh_key :boolean default(FALSE) +# website_url :string(255) default(""), not null +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# location :string(255) +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean default(FALSE), not null +# otp_backup_codes :text +# public_email :string(255) default(""), not null +# dashboard :integer default(0) +# project_view :integer default(0) +# consumed_timestep :integer +# layout :integer default(0) +# hide_project_limit :boolean default(FALSE) +# unlock_token :string +# otp_grace_period_started_at :datetime # require 'carrierwave/orm/activerecord' @@ -352,10 +353,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/create_branch_service.rb b/app/services/create_branch_service.rb index f139872c728..c0e08a151f2 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -31,7 +31,6 @@ class CreateBranchService < BaseService if new_branch push_data = build_push_data(project, current_user, new_branch) - EventCreateService.new.push(project, current_user, push_data) project.execute_hooks(push_data.dup, :push_hooks) project.execute_services(push_data.dup, :push_hooks) diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 2452999382a..55985380d31 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -23,6 +23,7 @@ class CreateTagService < BaseService EventCreateService.new.push(project, current_user, push_data) project.execute_hooks(push_data.dup, :tag_push_hooks) project.execute_services(push_data.dup, :tag_push_hooks) + CreateCommitBuildsService.new.execute(project, current_user, push_data) if release_description CreateReleaseService.new(@project, @current_user). diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 22bf9dd935e..004b3ce7286 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -27,7 +27,6 @@ class DeleteBranchService < BaseService if repository.rm_branch(current_user, branch_name) push_data = build_push_data(branch) - EventCreateService.new.push(project, current_user, push_data) project.execute_hooks(push_data.dup, :push_hooks) project.execute_services(push_data.dup, :push_hooks) 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/download_service.rb b/app/services/projects/download_service.rb index 99f22293d0d..6386f57fb0d 100644 --- a/app/services/projects/download_service.rb +++ b/app/services/projects/download_service.rb @@ -16,13 +16,7 @@ module Projects uploader.download!(@url) uploader.store! - filename = uploader.image? ? uploader.file.basename : uploader.file.filename - - { - 'alt' => filename, - 'url' => uploader.secure_url, - 'is_image' => uploader.image? - } + uploader.to_h end private diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb new file mode 100644 index 00000000000..0db85ac2142 --- /dev/null +++ b/app/services/projects/housekeeping_service.rb @@ -0,0 +1,20 @@ +# Projects::HousekeepingService class +# +# Used for git housekeeping +# +# Ex. +# Projects::HousekeepingService.new(project).execute +# +module Projects + class HousekeepingService < BaseService + include Gitlab::ShellAdapter + + def initialize(project) + @project = project + end + + def execute + GitlabShellWorker.perform_async(:gc, @project.path_with_namespace) + end + end +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/projects/upload_service.rb b/app/services/projects/upload_service.rb index 279550d6f4a..012e82a7704 100644 --- a/app/services/projects/upload_service.rb +++ b/app/services/projects/upload_service.rb @@ -10,13 +10,7 @@ module Projects uploader = FileUploader.new(@project) uploader.store!(@file) - filename = uploader.image? ? uploader.file.basename : uploader.file.filename - - { - alt: filename, - url: uploader.secure_url, - is_image: uploader.image? - } + uploader.to_h end private 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/services/system_note_service.rb b/app/services/system_note_service.rb index 98a71cbf1ad..1083bcec054 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -41,7 +41,7 @@ class SystemNoteService # # Returns the created Note object def self.change_assignee(noteable, project, author, assignee) - body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}" + body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -66,7 +66,7 @@ class SystemNoteService def self.change_label(noteable, project, author, added_labels, removed_labels) labels_count = added_labels.count + removed_labels.count - references = ->(label) { "~#{label.id}" } + references = ->(label) { label.to_reference(:id) } added_labels = added_labels.map(&references).join(' ') removed_labels = removed_labels.map(&references).join(' ') @@ -103,7 +103,7 @@ class SystemNoteService # Returns the created Note object def self.change_milestone(noteable, project, author, milestone) body = 'Milestone ' - body += milestone.nil? ? 'removed' : "changed to #{milestone.title}" + body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}" create_note(noteable: noteable, project: project, author: author, note: body) end 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/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index ac920119a85..86d24469e05 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -30,4 +30,19 @@ class FileUploader < CarrierWave::Uploader::Base def secure_url File.join("/uploads", @secret, file.filename) end + + def to_h + filename = image? ? self.file.basename : self.file.filename + escaped_filename = filename.gsub("]", "\\]") + + markdown = "[#{escaped_filename}](#{self.secure_url})" + markdown.prepend("!") if image? + + { + alt: filename, + url: self.secure_url, + is_image: image?, + markdown: markdown + } + end end diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index 10e35ce665a..7a35958cc5f 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -17,6 +17,7 @@ class NamespaceValidator < ActiveModel::EachValidator hooks issues merge_requests + new notes profile projects diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml index 619533e09a7..2741eb44357 100644 --- a/app/views/abuse_report_mailer/notify.html.haml +++ b/app/views/abuse_report_mailer/notify.html.haml @@ -8,4 +8,4 @@ = @abuse_report.message %p - = link_to "View details", abuse_reports_url + = link_to "View details", admin_abuse_reports_url 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..2ab01704b77 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -2,25 +2,30 @@ - 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), data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr" %td - - if user + - if user && !user.blocked? = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" + - else + .btn.btn-xs.disabled + Already Blocked = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" 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 3cada08c2ba..81337432ab7 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -149,7 +149,7 @@ .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' @@ -171,20 +171,14 @@ .col-sm-10 = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com' .form-group - = f.label :metrics_database, 'InfluxDB database', class: 'control-label col-sm-2' + = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2' .col-sm-10 - = f.text_field :metrics_database, class: 'form-control', placeholder: 'gitlab' + = f.text_field :metrics_port, class: 'form-control', placeholder: '8089' .help-block - The name of the InfluxDB database to store data in. Users will have to - create this database manually, GitLab does not do so automatically. - .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' + 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_pool_size, 'Connection pool size', class: 'control-label col-sm-2' .col-sm-10 @@ -209,5 +203,27 @@ 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/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 296497a4cd4..f7fd156b84a 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -30,7 +30,7 @@ %li %span.light Created on: %strong - = @group.created_at.stamp("March 1, 1999") + = @group.created_at.to_s(:medium) .panel.panel-default .panel-heading diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 5260eadf95b..0c986af4a95 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -38,7 +38,7 @@ %li %span.light Created on: %strong - = @project.created_at.stamp("March 1, 1999") + = @project.created_at.to_s(:medium) %li %span.light http: diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml index 7d11edc79e2..6bc217f84cc 100644 --- a/app/views/admin/users/_profile.html.haml +++ b/app/views/admin/users/_profile.html.haml @@ -4,7 +4,7 @@ %ul.well-list %li %span.light Member since - %strong= user.created_at.stamp("Aug 21, 2011") + %strong= user.created_at.to_s(:medium) - unless user.public_email.blank? %li %span.light E-mail: 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/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 0848504b7a6..2c2450d4117 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -58,12 +58,12 @@ %li %span.light Member since: %strong - = @user.created_at.stamp("Nov 12, 2031") + = @user.created_at.to_s(:medium) - if @user.confirmed_at %li %span.light Confirmed at: %strong - = @user.confirmed_at.stamp("Nov 12, 2031") + = @user.confirmed_at.to_s(:medium) - else %li %span.light Confirmed: @@ -74,7 +74,7 @@ %span.light Current sign-in at: %strong - if @user.current_sign_in_at - = @user.current_sign_in_at.stamp("Nov 12, 2031") + = @user.current_sign_in_at.to_s(:medium) - else never @@ -82,7 +82,7 @@ %span.light Last sign-in at: %strong - if @user.last_sign_in_at - = @user.last_sign_in_at.stamp("Nov 12, 2031") + = @user.last_sign_in_at.to_s(:medium) - else never diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index a144c43be47..0044d779c31 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -4,12 +4,12 @@ .row = form_tag ci_lint_path, method: :post do .form-group - = label_tag :content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap' + = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap') .col-sm-12 - = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true + = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true) .col-sm-12 .pull-left.prepend-top-10 - = submit_tag 'Validate', class: 'btn btn-success submit-yml' + = submit_tag('Validate', class: 'btn btn-success submit-yml') .row.prepend-top-20 .col-sm-12 diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 07bda1c77f8..0d7b1b30dc3 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" xml.id issues_dashboard_url - xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? + xml.updated @issues.first.created_at.xmlschema if @issues.any? @issues.each do |issue| issue_to_atom(xml, issue) diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index c8c219f4cca..2e2712c5146 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.id dashboard_projects_url - xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.xmlschema if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 49fab016bfa..cb93ff2465e 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -19,7 +19,7 @@ .form-group.append-bottom-20#password-strength = f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true %div - - if Gitlab.config.recaptcha.enabled + - 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/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index ffc37ad6178..abea86b026a 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -1,5 +1,5 @@ - if show_last_push_widget?(event) - .gray-content-block.clear-block + .gray-content-block.clear-block.last-push-widget .event-last-push .event-last-push-text %span You pushed to 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/issues.atom.builder b/app/views/groups/issues.atom.builder index 66fe7e25871..486d1d8587a 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml" xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" xml.id issues_dashboard_url - xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? + xml.updated @issues.first.created_at.xmlschema if @issues.any? @issues.each do |issue| issue_to_atom(xml, issue) diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index 7ea574434c3..5cc0f5e1d2e 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.id group_url(@group) - xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.xmlschema if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index c2c7c581b3e..48a544fc834 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -6,6 +6,12 @@ = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") .cover-block + .cover-controls + - if @group && can?(current_user, :admin_group, @group) + = link_to icon('pencil'), edit_group_path(@group), class: 'btn' + - if current_user + = link_to icon('rss'), group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' + .avatar-holder = link_to group_icon(@group), target: '_blank' do = image_tag group_icon(@group), class: "avatar group-avatar s90" @@ -34,9 +40,6 @@ .gray-content-block.activity-filter-block - if current_user = render "events/event_last_push", event: @last_push - .pull-right - = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do - %i.fa.fa-rss = render 'shared/event_filter' @@ -47,5 +50,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 e8e331dd109..9ee6f07b26b 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -40,6 +40,32 @@ %td.shortcut .key enter %td Open Selection + %tr + %td.shortcut + .key t + %td Go to finding file + %tbody + %tr + %th + %th Finding Project File + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + %td Move selection up + %tr + %td.shortcut + .key + %i.fa.fa-arrow-down + %td Move selection down + %tr + %td.shortcut + .key enter + %td Open Selection + %tr + %td.shortcut + .key esc + %td Go back .col-lg-4 %table.shortcut-mappings diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 2e0bd2007a3..38ca4f91c4d 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,13 +1,13 @@ +- page_description brand_title unless page_description + +- site_name = "GitLab" %head{prefix: "og: http://ogp.me/ns#"} %meta{charset: "utf-8"} %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} - %meta{name: 'referrer', content: 'origin-when-cross-origin'} - - %meta{name: "description", content: page_description} -# Open Graph - http://ogp.me/ %meta{property: 'og:type', content: "object"} - %meta{property: 'og:site_name', content: "GitLab"} + %meta{property: 'og:site_name', content: site_name} %meta{property: 'og:title', content: page_title} %meta{property: 'og:description', content: page_description} %meta{property: 'og:image', content: page_image} @@ -20,8 +20,8 @@ %meta{property: 'twitter:image', content: page_image} = page_card_meta_tags - - page_title "GitLab" - %title= page_title + %title= page_title(site_name) + %meta{name: "description", content: page_description} = favicon_link_tag 'favicon.ico' @@ -34,6 +34,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/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 035fe0056d3..96b38485425 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -1,4 +1,6 @@ - project = @target_project || @project -:javascript - GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}" - GitLab.GfmAutoComplete.setup(); + +- if @noteable + :javascript + GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}" + GitLab.GfmAutoComplete.setup(); diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 31888c5580e..2e483b7148d 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,5 +1,6 @@ -- page_title @group.name -- header_title group_title(@group) unless header_title -- sidebar "group" unless sidebar +- page_title @group.name +- page_description @group.description unless page_description +- header_title group_title(@group) unless header_title +- sidebar "group" unless sidebar = render template: "layouts/application" 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..270ccfd387f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -25,7 +25,7 @@ %span Activity - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do = icon('files-o fw') %span @@ -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 @@ -117,4 +117,3 @@ %li.hidden = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do Network - diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index abf73bcc709..ab527e8e438 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,6 +1,7 @@ -- page_title @project.name_with_namespace -- header_title project_title(@project) unless header_title -- sidebar "project" unless sidebar +- page_title @project.name_with_namespace +- page_description @project.description unless page_description +- header_title project_title(@project) unless header_title +- sidebar "project" unless sidebar - content_for :scripts_body_top do - project = @target_project || @project diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 4361f67a74d..3dd2595f1ad 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -17,7 +17,7 @@ %strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))} %div %span by #{commit.author_name} - %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} + %i at #{commit.committed_date.to_s(:iso8601)} %pre.commit-message = commit.safe_message diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index aa0e263b6df..53869e36b28 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -8,7 +8,7 @@ \ = @message.reverse_compare? ? "Deleted commits:" : "Commits:" - @message.commits.each do |commit| - #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} + #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.to_s(:iso8601)} #{commit.safe_message} \- - - - - \ diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 0ca8bd95157..3bd1f1af162 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -10,7 +10,7 @@ %strong= @key.title %li %span.light Created on: - %strong= @key.created_at.stamp("Aug 21, 2011") + %strong= @key.created_at.to_s(:medium) .col-md-8 %p diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml new file mode 100644 index 00000000000..08e2fc48be7 --- /dev/null +++ b/app/views/projects/_find_file_link.html.haml @@ -0,0 +1,3 @@ += link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do
+ = icon('search')
+ %span Find File
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index e92115b9b98..53eec76129b 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 - = 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 @@ -39,5 +52,5 @@ = render 'projects/buttons/notifications' -:coffeescript - new Star()
\ No newline at end of file +:javascript + new Star(); diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index 7e6301abde8..d5829568275 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,13 +1,12 @@ .zennable - %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox") .zen-backdrop - classes << ' js-gfm-input markdown-area' - if defined?(f) && f - = f.text_area attr, class: classes, placeholder: '' + = f.text_area attr, class: classes - else - = text_area_tag attr, nil, class: classes, placeholder: '' - %a.zen-enter-link(tabindex="-1" href="#") + = text_area_tag attr, nil, class: classes + %a.js-zen-enter(tabindex="-1" href="#") = icon('expand') Edit in fullscreen - %a.zen-leave-link(href="#") + %a.js-zen-leave(tabindex="-1" href="#") = icon('compress') diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 5081bae6801..a234536723e 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,4 +1,8 @@ - commit = @repository.commit(branch.target) +- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 +- diverging_commit_counts = @repository.diverging_commit_counts(branch) +- number_commits_behind = diverging_commit_counts[:behind] +- number_commits_ahead = diverging_commit_counts[:ahead] %li(class="js-branch-#{branch.name}") %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do @@ -29,6 +33,17 @@ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") + - if branch.name != @repository.root_ref + .divergence-graph{ title: "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" } + .graph-side + .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } + %span.count.count-behind= number_commits_behind + .graph-separator + .graph-side + .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } + %span.count.count-ahead= number_commits_ahead + + - if commit = render 'projects/branches/commit', commit: commit, project: @project - else diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 1a26908ab11..3bbfdb1e3b0 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 @@ -40,7 +40,7 @@ %thead %tr %th Status - %th Runner + %th Build ID %th Commit %th Ref %th Stage 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/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 069b8b1f169..58aa45e8d2c 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,4 +1,6 @@ -- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" +- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" +- page_description @commit.description + = render "projects/commits/header_title" = render "commit_box" - if @ci_commit 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/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 0cd9ce1f371..6c631228002 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -6,7 +6,7 @@ .col-md-2.hidden-xs.hidden-sm %h5.commits-row-date %i.fa.fa-calendar - %span= day.stamp("28 Aug, 2010") + %span= day.strftime('%d %b, %Y') .light = pluralize(commits.count, 'commit') .col-md-10.col-sm-12 diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 7ffa7317196..e310fafd82c 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -4,14 +4,14 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html" xml.id namespace_project_commits_url(@project.namespace, @project, @ref) - xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any? + xml.updated @commits.first.committed_date.xmlschema if @commits.any? @commits.each do |commit| xml.entry do xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id) xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) xml.title truncate(commit.title, length: 80) - xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") + xml.updated commit.committed_date.xmlschema xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) xml.author do |author| xml.name commit.author_name diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 2dd99cc8215..034057da42e 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -10,26 +10,30 @@ .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' - .commits-feed-holder.hidden-xs.hidden-sm + .block-controls.hidden-xs.hidden-sm - if create_mr_button?(@repository.root_ref, @ref) - = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do - = icon('plus') - Create Merge Request + .control + = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do + = icon('plus') + Create Merge Request + + .control + = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do + = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - if current_user && current_user.private_token - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'prepend-left-10 btn' do - = icon("rss") + .control + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do + = icon("rss") %ul.breadcrumb.repo-breadcrumb = commits_breadcrumbs %div{id: dom_id(@project)} - #commits-list= render "commits", project: @project + #commits-list.content_list= render "commits", project: @project .clear = spinner -- if @commits.count == @limit - :javascript - CommitsList.init("#{@ref}", #{@limit}); - +:javascript + CommitsList.init("#{@ref}", #{@limit}); diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 650629ef1b9..31e752c6649 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -174,6 +174,19 @@ .danger-settings + .panel.panel-default + .panel-heading Housekeeping + .errors-holder + .panel-body + %p + Runs a number of housekeeping tasks within the current repository, + such as compressing file revisions and removing unreachable objects. + %br + + .form-actions + = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), + method: :post, class: "btn btn-default" + - if can? current_user, :archive_project, @project - if @project.archived? .panel.panel-success diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml new file mode 100644 index 00000000000..40a2a61d8a1 --- /dev/null +++ b/app/views/projects/find_file/show.html.haml @@ -0,0 +1,27 @@ +- page_title "Find File", @ref +- header_title project_title(@project, "Files", project_files_path(@project)) + +.file-finder-holder.tree-holder.clearfix + .gray-content-block.top-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'find_file', path: @path + %ul.breadcrumb.repo-breadcrumb + %li + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + %li.file-finder + %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path'} + + %div.tree-content-holder + .table-holder + %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" } + %tbody + = spinner nil, true + +:javascript + var projectFindFile = new ProjectFindFile($(".file-finder-holder"), { + url: "#{escape_javascript(namespace_project_files_path(@project.namespace, @project, @ref, @options.merge(format: :json)))}", + treeUrl: "#{escape_javascript(namespace_project_tree_path(@project.namespace, @project, @ref))}", + blobUrlTemplate: "#{escape_javascript(namespace_project_blob_path(@project.namespace, @project, @id || @commit.id))}" + }); + new ShortcutsFindFile(projectFindFile); diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index b18d9197d0b..a0511819c9f 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -47,14 +47,14 @@ = f.label :issues_events, class: 'list-label' do %strong Issues events %p.light - This url will be triggered when an issue is created + This url will be triggered when an issue is created/updated/merged %div = f.check_box :merge_requests_events, class: 'pull-left' .prepend-left-20 = f.label :merge_requests_events, class: 'list-label' do %strong Merge Request events %p.light - This url will be triggered when a merge request is created + This url will be triggered when a merge request is created/updated/merged %div = f.check_box :build_events, class: 'pull-left' .prepend-left-20 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/index.atom.builder b/app/views/projects/issues/index.atom.builder index dc8e477185b..ee8a9414657 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_issues_url(@project.namespace, @project) - xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? + xml.updated @issues.first.created_at.xmlschema if @issues.any? @issues.each do |issue| issue_to_atom(xml, issue) 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 ba7c2c01e93..095876450a0 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -38,7 +38,7 @@ = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/widget/show.html.haml" - - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user) + - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) .light.prepend-top-default You can also accept this merge request manually using the = succeed '.' do 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/show.atom.builder b/app/views/projects/show.atom.builder index 15c49767556..d6762219108 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_url(@project.namespace, @project) - xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.xmlschema if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 7466a098e24..8436be433b1 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -69,14 +69,3 @@ %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 diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index ec14bd7f65a..c57570afa09 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -3,12 +3,12 @@ = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") - = render 'projects/last_push' -- if can? current_user, :download_code, @project - .tree-download-holder - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true +.pull-right + = render 'projects/find_file_link' + - if can? current_user, :download_code, @project + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true #tree-holder.tree-holder.clearfix .gray-content-block.top-block 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/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 28d6f421fea..5a60ff5a5da 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -50,7 +50,7 @@ = form.label :issues_events, class: 'list-label' do %strong Issues events %p.light - This url will be triggered when an issue is created + This url will be triggered when an issue is created/updated/merged - if @service.supported_events.include?("merge_request") %div = form.check_box :merge_requests_events, class: 'pull-left' @@ -58,7 +58,7 @@ = form.label :merge_requests_events, class: 'list-label' do %strong Merge Request events %p.light - This url will be triggered when a merge request is created + This url will be triggered when a merge request is created/updated/merged - if @service.supported_events.include?("build") %div = form.check_box :build_events, class: 'pull-left' 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/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 79c5cc7f40a..2299112bec7 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -54,14 +54,6 @@ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } - .block - .title - Cross-project reference - .cross-project-reference - %span#cross-project-reference - = cross_project_reference(@project, issuable) - = clipboard_button(clipboard_target: 'span#cross-project-reference') - = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user @@ -77,7 +69,16 @@ You're not receiving notifications from this thread. .subscribed{class: ( 'hidden' unless subscribed )} You're receiving notifications because you're subscribed to this thread. + - project_ref = cross_project_reference(@project, issuable) + .block + .title + .cross-project-reference + %span#cross-project-reference + Reference: + %a{href: '#', title:project_ref} + = project_ref + = clipboard_button(clipboard_target: 'span#cross-project-reference') :javascript new Subscription("#{toggle_subscription_path(issuable)}"); - new IssuableContext(); + new IssuableContext();
\ No newline at end of file diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index c36995b94d7..5db8056b77c 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -4,8 +4,12 @@ - skip_namespace = false unless local_assigns[:skip_namespace] == true - css_class = '' unless local_assigns[:css_class] - css_class += " no-description" unless project.description.present? +- ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit +- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] +- cache_key.push(ci_commit.status) if ci_commit + %li.project-row{ class: css_class } - = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2.2'] do + = cache(cache_key) do = link_to project_path(project), class: dom_class(project) do - if avatar .dash-project-avatar @@ -19,10 +23,9 @@ = project.name .project-controls - - if ci && !project.empty_repo? && project.commit - - if ci_commit = project.ci_commit(project.commit.sha) - = render_ci_status(ci_commit) - + - if ci_commit + = render_ci_status(ci_commit) + - if stars %span %i.fa.fa-star diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index 2fe5b7fac83..114d1e7a379 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" xml.link href: user_url(@user), rel: "alternate", type: "text/html" xml.id user_url(@user) - xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.xmlschema if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0bca8177e14..ce17fc7bca1 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -21,7 +21,7 @@ %span #{@user.bio}. %span - Member since #{@user.created_at.stamp("Aug 21, 2011")} + Member since #{@user.created_at.to_s(:medium)} .cover-desc - unless @user.public_email.blank? diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index ce0a0113403..b1f8645eea0 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -20,27 +20,29 @@ = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) - if current_user - :coffeescript - post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}" - noteable_type = "#{votable.class.name.underscore}" - noteable_id = "#{votable.id}" - aliases = #{AwardEmoji.aliases.to_json} + :javascript + var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; + var noteable_type = "#{votable.class.name.underscore}"; + var noteable_id = "#{votable.id}"; + var aliases = #{AwardEmoji.aliases.to_json}; window.awards_handler = new AwardsHandler( post_emoji_url, noteable_type, noteable_id, aliases - ) + ); - $(".awards").on "click", ".emoji-menu-content li", (e) -> - emoji = $(this).find(".emoji-icon").data("emoji") - awards_handler.addAward(emoji) + $(".awards").on("click", ".emoji-menu-content li", function(e) { + var emoji = $(this).find(".emoji-icon").data("emoji"); + awards_handler.addAward(emoji); + }); - $(".awards").on "click", ".award", (e) -> - emoji = $(this).find(".icon").data("emoji") - awards_handler.addAward(emoji) + $(".awards").on("click", ".award", function(e) { + var emoji = $(this).find(".icon").data("emoji"); + awards_handler.addAward(emoji); + }); - $(".award").tooltip() + $(".award").tooltip(); - $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false}) + $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false}); diff --git a/app/workers/metrics_worker.rb b/app/workers/metrics_worker.rb deleted file mode 100644 index b15dc819c5c..00000000000 --- a/app/workers/metrics_worker.rb +++ /dev/null @@ -1,33 +0,0 @@ -class MetricsWorker - include Sidekiq::Worker - - sidekiq_options queue: :metrics - - def perform(metrics) - prepared = prepare_metrics(metrics) - - Gitlab::Metrics.pool.with do |connection| - connection.write_points(prepared) - end - end - - def 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 escape_value(value) - value.to_s.gsub('=', '\\=') - end -end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 84f0dfb64c8..d6e2c9380a5 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -204,6 +204,11 @@ production: &base bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' + # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking + # a request if the LDAP server becomes unresponsive. + # A value of 0 means there is no timeout. + timeout: 10 + # This setting specifies if LDAP server is Active Directory LDAP server. # For non AD servers it skips the AD specific queries. # If your LDAP server is not AD, set this to false. @@ -346,12 +351,6 @@ production: &base # cas3: # session_duration: 28800 - # reCAPTCHA settings. See: http://www.google.com/recaptcha - recaptcha: - enabled: false - public_key: 'YOUR_PUBLIC_KEY' - private_key: 'YOUR_PRIVATE_KEY' - # Shared file storage settings shared: # path: /mnt/gitlab # Default: shared diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 045bab739ea..a9c5b2caf0a 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -108,6 +108,7 @@ if Settings.ldap['enabled'] || Rails.env.test? Settings.ldap['servers'].each do |key, server| server['label'] ||= 'LDAP' + server['timeout'] ||= 10.seconds server['block_auto_created_users'] = false if server['block_auto_created_users'].nil? server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? server['active_directory'] = true if server['active_directory'].nil? @@ -131,12 +132,6 @@ Settings.omniauth.cas3['session_duration'] ||= 8.hours Settings.omniauth['session_tickets'] ||= Settingslogic.new({}) Settings.omniauth.session_tickets['cas3'] = 'ticket' -# ReCAPTCHA settings -Settings['recaptcha'] ||= Settingslogic.new({}) -Settings.recaptcha['enabled'] = false if Settings.recaptcha['enabled'].nil? -Settings.recaptcha['public_key'] ||= Settings.recaptcha['public_key'] -Settings.recaptcha['private_key'] ||= Settings.recaptcha['private_key'] - Settings['shared'] ||= Settingslogic.new({}) Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) @@ -158,9 +153,9 @@ 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/date_time_formats.rb b/config/initializers/date_time_formats.rb new file mode 100644 index 00000000000..57568203cab --- /dev/null +++ b/config/initializers/date_time_formats.rb @@ -0,0 +1,9 @@ +# :short - 10 Nov +# :medium - Nov 10, 2007 +# :long - November 10, 2007 +Date::DATE_FORMATS[:medium] = '%b %-d, %Y' + +# :short - 18 Jan 06:10 +# :medium - Jan 18, 2007 6:10am +# :long - January 18, 2007 06:10 +Time::DATE_FORMATS[:medium] = '%b %-d, %Y %-I:%M%P' diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index 2e4908192a1..52ace27b7ae 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -1,6 +1,5 @@ if Gitlab::Metrics.enabled? require 'influxdb' - require 'socket' require 'connection_pool' require 'method_source' diff --git a/config/initializers/recaptcha.rb b/config/initializers/recaptcha.rb deleted file mode 100644 index 7509e327ae1..00000000000 --- a/config/initializers/recaptcha.rb +++ /dev/null @@ -1,6 +0,0 @@ -if Gitlab.config.recaptcha.enabled - Recaptcha.configure do |config| - config.public_key = Gitlab.config.recaptcha['public_key'] - config.private_key = Gitlab.config.recaptcha['private_key'] - end -end diff --git a/config/routes.rb b/config/routes.rb index 3e7d9f78710..3d5c70987c8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,9 +52,6 @@ Rails.application.routes.draw do API::API.logger Rails.logger mount API::API => '/api' - # Get all keys of user - get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ } - constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? } constraints constraint do mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq @@ -378,6 +375,7 @@ Rails.application.routes.draw do delete :remove_fork post :archive post :unarchive + post :housekeeping post :toggle_star post :markdown_preview get :autocomplete_sources @@ -441,6 +439,24 @@ Rails.application.routes.draw do end scope do + get( + '/find_file/*id', + to: 'find_file#show', + constraints: { id: /.+/, format: /html/ }, + as: :find_file + ) + end + + scope do + get( + '/files/*id', + to: 'find_file#list', + constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, + as: :files + ) + end + + scope do post( '/create_dir/*id', to: 'tree#create_dir', @@ -668,5 +684,8 @@ Rails.application.routes.draw do end end + # Get all keys of user + get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ } + get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } 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/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/migrate/20160106162223_add_index_milestones_title.rb b/db/migrate/20160106162223_add_index_milestones_title.rb new file mode 100644 index 00000000000..767885e2aac --- /dev/null +++ b/db/migrate/20160106162223_add_index_milestones_title.rb @@ -0,0 +1,5 @@ +class AddIndexMilestonesTitle < ActiveRecord::Migration + def change + add_index :milestones, :title + end +end diff --git a/db/migrate/20160106164438_remove_influxdb_credentials.rb b/db/migrate/20160106164438_remove_influxdb_credentials.rb new file mode 100644 index 00000000000..47e74400b97 --- /dev/null +++ b/db/migrate/20160106164438_remove_influxdb_credentials.rb @@ -0,0 +1,6 @@ +class RemoveInfluxdbCredentials < ActiveRecord::Migration + def change + remove_column :application_settings, :metrics_username, :string + remove_column :application_settings, :metrics_password, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index dc9ba36d0c7..2ded8a45e18 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: 20151228150906) do +ActiveRecord::Schema.define(version: 20160106164438) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -32,41 +32,42 @@ ActiveRecord::Schema.define(version: 20151228150906) 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_database", default: "gitlab" - 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 "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.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" @@ -77,14 +78,14 @@ ActiveRecord::Schema.define(version: 20151228150906) 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| @@ -96,7 +97,7 @@ ActiveRecord::Schema.define(version: 20151228150906) 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" @@ -107,19 +108,19 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -138,13 +139,13 @@ ActiveRecord::Schema.define(version: 20151228150906) 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" @@ -171,16 +172,16 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -188,25 +189,25 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -225,34 +226,34 @@ ActiveRecord::Schema.define(version: 20151228150906) 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" @@ -264,9 +265,9 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -275,8 +276,8 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -290,7 +291,7 @@ ActiveRecord::Schema.define(version: 20151228150906) 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" @@ -303,19 +304,19 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -330,8 +331,8 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -340,9 +341,9 @@ ActiveRecord::Schema.define(version: 20151228150906) 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" @@ -368,8 +369,8 @@ ActiveRecord::Schema.define(version: 20151228150906) 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" @@ -379,17 +380,17 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -409,10 +410,10 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -421,7 +422,7 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -430,22 +431,22 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -460,17 +461,17 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -482,10 +483,10 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -493,26 +494,26 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -528,13 +529,13 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -544,39 +545,37 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -592,14 +591,14 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -607,12 +606,12 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -620,15 +619,15 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -640,39 +639,39 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -688,17 +687,17 @@ ActiveRecord::Schema.define(version: 20151228150906) 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" @@ -711,30 +710,30 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -742,16 +741,16 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -764,7 +763,7 @@ ActiveRecord::Schema.define(version: 20151228150906) 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" @@ -775,10 +774,10 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -786,67 +785,67 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 @@ -873,19 +872,19 @@ ActiveRecord::Schema.define(version: 20151228150906) 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 d82ff8b908b..25fe3abcb9a 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. @@ -19,6 +19,7 @@ ## CI Documentation - [Quick Start](ci/quick_start/README.md) +- [Enable or disable GitLab CI](ci/enable_or_disable_ci.md) - [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md) - [Configuring runner](ci/runners/README.md) - [Configuring deployment](ci/deployment/README.md) @@ -56,7 +57,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. -- [Environmental Variables](administration/environmental_variables.md) to configure GitLab. +- [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/enviroment_variables.md b/doc/administration/environment_variables.md index d7f5cb7c21f..1eb3a74d304 100644 --- a/doc/administration/enviroment_variables.md +++ b/doc/administration/environment_variables.md @@ -9,11 +9,14 @@ 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 +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 @@ -39,7 +42,12 @@ GITLAB_DATABASE_PASSWORD | GITLAB_DATABASE_HOST | localhost GITLAB_DATABASE_PORT | 5432 -## Other variables +## 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/api/projects.md b/doc/api/projects.md index 0ca81ffd49e..37d74216c1b 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -482,6 +482,34 @@ Parameters: - `id` (required) - The ID of a project +## Uploads + +### Upload a file + +Uploads a file to the specified project to be used in an issue or merge request description, or a comment. + +``` +POST /projects/:id/uploads +``` + +Parameters: + +- `id` (required) - The ID of the project +- `file` (required) - The file to be uploaded + +```json +{ + "alt": "dk", + "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", + "is_image": true, + "markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)" +} +``` + +**Note**: The returned `url` is relative to the project path. +In Markdown contexts, the link is automatically expanded when the format in `markdown` is used. + + ## Team members ### List project team members diff --git a/doc/api/tags.md b/doc/api/tags.md index 085d387e824..17d12e9cc62 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -83,6 +83,26 @@ it will contain the annotation. It returns 200 if the operation succeed. In case of an error, 405 with an explaining error message is returned. +## Delete a tag + +Deletes a tag of a repository with given name. On success, this API method +returns 200 with the name of the deleted tag. If the tag does not exist, the +API returns 404. + +``` +DELETE /projects/:id/repository/tags/:tag_name +``` + +Parameters: + +- `id` (required) - The ID of a project +- `tag_name` (required) - The name of a tag + +```json +{ + "tag_name": "v4.3.0" +} +``` ## Create a new release diff --git a/doc/api/users.md b/doc/api/users.md index 66d2fd52526..773fe36d277 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -123,6 +123,13 @@ Parameters: "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", + "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, + "bio": null, + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "" } ``` diff --git a/doc/ci/README.md b/doc/ci/README.md index a1f5513d88e..4cdd2e1ad33 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -3,6 +3,7 @@ ### User documentation * [Quick Start](quick_start/README.md) +* [Enable or disable GitLab CI](enable_or_disable_ci.md) * [Configuring project (.gitlab-ci.yml)](yaml/README.md) * [Configuring runner](runners/README.md) * [Configuring deployment](deployment/README.md) diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md new file mode 100644 index 00000000000..9bd2f5aff22 --- /dev/null +++ b/doc/ci/enable_or_disable_ci.md @@ -0,0 +1,70 @@ +## Enable or disable GitLab CI + +_To effectively use GitLab CI, you need a valid [`.gitlab-ci.yml`](yaml/README.md) +file present at the root directory of your project and a +[runner](runners/README.md) properly set up. You can read our +[quick start guide](quick_start/README.md) to get you started._ + +If you are using an external CI server like Jenkins or Drone CI, it is advised +to disable GitLab CI in order to not have any conflicts with the commits status +API. + +--- + +As of GitLab 8.2, GitLab CI is mainly exposed via the `/builds` page of a +project. Disabling GitLab CI in a project does not delete any previous builds. +In fact, the `/builds` page can still be accessed, although it's hidden from +the left sidebar menu. + +GitLab CI is enabled by default on new installations and can be disabled either +individually under each project's settings, or site-wide by modifying the +settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations +respectively. + +### Per-project user setting + +The setting to enable or disable GitLab CI can be found with the name **Builds** +under the **Features** area of a project's settings along with **Issues**, +**Merge Requests**, **Wiki** and **Snippets**. Select or deselect the checkbox +and hit **Save** for the settings to take effect. + +![Features settings](img/features_settings.png) + +--- + +### Site-wide administrator setting + +You can disable GitLab CI site-wide, by modifying the settings in `gitlab.yml` +and `gitlab.rb` for source and Omnibus installations respectively. + +Two things to note: + +1. Disabling GitLab CI, will affect only newly-created projects. Projects that + had it enabled prior to this modification, will work as before. +1. Even if you disable GitLab CI, users will still be able to enable it in the + project's settings. + +--- + +For installations from source, open `gitlab.yml` with your editor and set +`builds` to `false`: + +```yaml +## Default project features settings +default_projects_features: + issues: true + merge_requests: true + wiki: true + snippets: false + builds: false +``` + +Save the file and restart GitLab: `sudo service gitlab restart`. + +For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line: + +``` +gitlab-rails['gitlab_default_projects_features_builds'] = false +``` + +Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`. diff --git a/doc/ci/img/features_settings.png b/doc/ci/img/features_settings.png Binary files differnew file mode 100644 index 00000000000..17aba5d14d8 --- /dev/null +++ b/doc/ci/img/features_settings.png diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 68dcfe23ffb..295d953db11 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -62,8 +62,9 @@ Now simply register the runner as any runner: sudo gitlab-runner register ``` -Note that you will have to enable `Allows shared runners` for each project -that you want to make use of a shared runner. This is by default `off`. +Shared runners are enabled by default as of GitLab 8.2, but can be disabled with the +`DISABLE SHARED RUNNERS` button. Previous versions of GitLab defaulted shared runners to +disabled. ## Registering a Specific Runner diff --git a/doc/install/installation.md b/doc/install/installation.md index 81edd8da2b8..8c4e092c636 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -348,7 +348,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout 0.5.1 + sudo -u git -H git checkout 0.5.3 sudo -u git -H make ### Initialize Database and Activate Advanced Features @@ -552,6 +552,6 @@ Apart from the always supported markdown style there are other rich text files t If you see this message when attempting to clone a repository hosted by GitLab, this is likely due to an outdated Nginx or Apache configuration, or a missing or -misconfigured `gitlab-git-http-server` instance. Double-check that you've -[installed Go](#3-go), [installed gitlab-git-http-server](#install-gitlab-git-http-server), +misconfigured gitlab-workhorse instance. Double-check that you've +[installed Go](#3-go), [installed gitlab-workhorse](#install-gitlab-workhorse), and correctly [configured Nginx](#site-configuration). diff --git a/doc/integration/README.md b/doc/integration/README.md index 2a9f76533b7..5edac746c7b 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -1,6 +1,7 @@ # GitLab Integration -GitLab integrates with multiple third-party services to allow external issue trackers and external authentication. +GitLab integrates with multiple third-party services to allow external issue +trackers and external authentication. See the documentation below for details on how to configure these services. @@ -15,13 +16,25 @@ See the documentation below for details on how to configure these services. - [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). +GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. ## Project services -Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a Project Service. -You can find these within GitLab in the Services page under Project Settings if you are at least a master on the project. -Project Services are a bit like plugins in that they allow a lot of freedom in adding functionality to GitLab, for example there is also a service that can send an email every time someone pushes new commits. -Because GitLab is open source we can ship with the code and tests for all plugins. -This allows the community to keep the plugins up to date so that they always work in newer GitLab versions. -For an overview of what projects services are available without logging in please see the [project_services directory](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services). +Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, +Pivotal Tracker, and Slack are available in the form of a [Project Service][]. +You can find these within GitLab in the Services page under Project Settings if +you are at least a master on the project. +Project Services are a bit like plugins in that they allow a lot of freedom in +adding functionality to GitLab. For example there is also a service that can +send an email every time someone pushes new commits. + +Because GitLab is open source we can ship with the code and tests for all +plugins. This allows the community to keep the plugins up to date so that they +always work in newer GitLab versions. + +For an overview of what projects services are available without logging in, +please see the [project_services directory][projects-code]. + +[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html +[Project Service]: ../project_services/project_services.md +[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services diff --git a/doc/integration/azure.md b/doc/integration/azure.md new file mode 100644 index 00000000000..48dddf7df44 --- /dev/null +++ b/doc/integration/azure.md @@ -0,0 +1,83 @@ +# Microsoft Azure OAuth2 OmniAuth Provider + +To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your application with Azure. Azure will generate a client ID and secret key for you to use. + +1. Sign in to the [Azure Management Portal](https://manage.windowsazure.com>). + +1. Select "Active Directory" on the left and choose the directory you want to use to register GitLab. + +1. Select "Applications" at the top bar and click the "Add" button the bottom. + +1. Select "Add an application my organization is developing". + +1. Provide the project information and click the "Next" button. + - Name: 'GitLab' works just fine here. + - Type: 'WEB APPLICATION AND/OR WEB API' + +1. On the "App properties" page enter the needed URI's and click the "Complete" button. + - SIGN-IN URL: Enter the URL of your GitLab installation (e.g 'https://gitlab.mycompany.com/') + - APP ID URI: Enter the endpoint URL for Microsoft to use, just has to be unique (e.g 'https://mycompany.onmicrosoft.com/gitlab') + +1. Select "Configure" in the top menu. + +1. Add a "Reply URL" pointing to the Azure OAuth callback of your GitLab installation (e.g. https://gitlab.mycompany.com/users/auth/azure_oauth2/callback). + +1. Create a "Client secret" by selecting a duration, the secret will be generated as soon as you click the "Save" button in the bottom menu.. + +1. Note the "CLIENT ID" and the "CLIENT SECRET". + +1. Select "View endpoints" from the bottom menu. + +1. You will see lots of endpoint URLs in the form 'https://login.microsoftonline.com/TENANT ID/...', note down the TENANT ID part of one of those endpoints. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "azure_oauth2", + "args" => { + "client_id" => "CLIENT ID", + "client_secret" => "CLIENT SECRET", + "tenant_id" => "TENANT ID", + } + } + ] + ``` + + For installations from source: + + ``` + - { name: 'azure_oauth2', + args: { client_id: "CLIENT ID", + client_secret: "CLIENT SECRET", + tenant_id: "TENANT ID" } } + ``` + +1. Replace 'CLIENT ID', 'CLIENT SECRET' and 'TENANT ID' with the values you got above. + +1. Save the configuration file. + +1. Restart GitLab for the changes to take effect. + +On the sign in page there should now be a Microsoft icon below the regular sign in form. Click the icon to begin the authentication process. Microsoft will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 3e660cfba1e..3543a67dd49 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,44 +1,30 @@ # External issue tracker -GitLab has a great issue tracker but you can also use an external issue tracker such as Jira, Bugzilla or Redmine. You can configure issue trackers per GitLab project. For instance, if you configure Jira it allows you to do the following: +GitLab has a great issue tracker but you can also use an external one such as +Jira or Redmine. Issue trackers are configurable per GitLab project and allow +you to do the following: -- the 'Issues' link on the GitLab project pages takes you to the appropriate Jira issue index; -- clicking 'New issue' on the project dashboard creates a new Jira issue; -- To reference Jira issue PROJECT-1234 in comments, use syntax PROJECT-1234. Commit messages get turned into HTML links to the corresponding Jira issue. - -![Jira screenshot](jira-integration-points.png) - -GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html). +- the **Issues** link on the GitLab project pages takes you to the appropriate + issue index of the external tracker +- clicking **New issue** on the project dashboard creates a new issue on the + external tracker ## Configuration -### Project Service +The configuration is done via a project's **Services**. -You can enable an external issue tracker per project. As an example, we will configure `Redmine` for project named gitlab-ci. - -Fill in the required details on the page: +### Project Service -![redmine configuration](redmine_configuration.png) +To enable an external issue tracker you must configure the appropriate **Service**. +Visit the links below for details: -* `description` A name for the issue tracker (to differentiate between instances, for example). -* `project_url` The URL to the project in Redmine which is being linked to this GitLab project. -* `issues_url` The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the url. This id is used by GitLab as a placeholder to replace the issue number. -* `new_issue_url` This is the URL to create a new issue in Redmine for the project linked to this GitLab project. +- [Redmine](../project_services/redmine.md) +- [Jira](jira.md) ### Service Template -It is necessary to configure the external issue tracker per project, because project specific details are needed for the integration with GitLab. -The admin can add a service template that sets a default for each project. This makes it much easier to configure individual projects. - -In GitLab Admin section, navigate to `Service Templates` and choose the service template you want to create: - -![redmine service template](redmine_service_template.png) - -After the template is created, the template details will be pre-filled on the project service page. - -NOTE: For each project, you will still need to configure the issue tracking URLs by replacing `:issues_tracker_id` in the above screenshot -with the ID used by your external issue tracker. Prior to GitLab v7.8, this ID was configured in the project settings, and GitLab would automatically -update the URL configured in `gitlab.yml`. This behavior is now depecated, and all issue tracker URLs must be configured directly -within the project's Services settings. +To save you the hassle from configuring each project's service individually, +GitLab provides the ability to set Service Templates which can then be +overridden in each project's settings. -Support to add your commits to the Jira ticket automatically is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jira.html). +Read more on [Services Templates](../project_services/services_templates.md). diff --git a/doc/integration/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png Binary files differindex 15739a22dc7..15739a22dc7 100644 --- a/doc/integration/jira_issue_reference.png +++ b/doc/integration/img/jira_issue_reference.png diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/integration/img/jira_merge_request_close.png Binary files differnew file mode 100644 index 00000000000..1e78daf105f --- /dev/null +++ b/doc/integration/img/jira_merge_request_close.png diff --git a/doc/integration/jira_project_name.png b/doc/integration/img/jira_project_name.png Binary files differindex 5986fdb63fb..5986fdb63fb 100644 --- a/doc/integration/jira_project_name.png +++ b/doc/integration/img/jira_project_name.png diff --git a/doc/integration/jira_service.png b/doc/integration/img/jira_service.png Binary files differindex 1f6628c4371..1f6628c4371 100644 --- a/doc/integration/jira_service.png +++ b/doc/integration/img/jira_service.png diff --git a/doc/integration/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png Binary files differindex 67dfc6144c4..67dfc6144c4 100644 --- a/doc/integration/jira_service_close_issue.png +++ b/doc/integration/img/jira_service_close_issue.png diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png Binary files differnew file mode 100644 index 00000000000..2b37eda3520 --- /dev/null +++ b/doc/integration/img/jira_service_page.png diff --git a/doc/integration/jira_workflow_screenshot.png b/doc/integration/img/jira_workflow_screenshot.png Binary files differindex 8635a32eb68..8635a32eb68 100644 --- a/doc/integration/jira_workflow_screenshot.png +++ b/doc/integration/img/jira_workflow_screenshot.png diff --git a/doc/integration/jira.md b/doc/integration/jira.md index 624601d0fac..de574d53410 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -1,14 +1,15 @@ # GitLab Jira integration -GitLab can be configured to interact with Jira. -Configuration happens via username and password. -Connecting to a Jira server via CAS is not possible. +GitLab can be configured to interact with Jira. Configuration happens via +username and password. Connecting to a Jira server via CAS is not possible. -Each project can be configured to connect to a different Jira instance, configuration is explained [here](#configuration). -If you have one Jira instance you can pre-fill the settings page with a default template. To configure the template [see external issue tracker document](external-issue-tracker.md#service-template)). - -Once the project is connected to Jira, you can reference and close the issues in Jira directly from GitLab. +Each project can be configured to connect to a different Jira instance, see the +[configuration](#configuration) section. If you have one Jira instance you can +pre-fill the settings page with a default template. To configure the template +see the [Services Templates][services-templates] document. +Once the project is connected to Jira, you can reference and close the issues +in Jira directly from GitLab. ## Table of Contents @@ -18,8 +19,11 @@ Once the project is connected to Jira, you can reference and close the issues in ### Referencing Jira Issues -When GitLab project has Jira issue tracker configured and enabled, mentioning Jira issue in GitLab will automatically add a comment in Jira issue with the link back to GitLab. This means that in comments in merge requests and commits referencing an issue, eg. `PROJECT-7`, will add a comment in Jira issue in the format: - +When GitLab project has Jira issue tracker configured and enabled, mentioning +Jira issue in GitLab will automatically add a comment in Jira issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, eg. `PROJECT-7`, will add a comment in Jira issue in the +format: ``` USER mentioned this issue in LINK_TO_THE_MENTION @@ -29,85 +33,117 @@ When GitLab project has Jira issue tracker configured and enabled, mentioning Ji * `LINK_TO_THE_MENTION` Link to the origin of mention with a name of the entity where Jira issue was mentioned. Can be commit or merge request. +![example of mentioning or closing the Jira issue](img/jira_issue_reference.png) -![example of mentioning or closing the Jira issue](jira_issue_reference.png) - +--- ### Closing Jira Issues -Jira issues can be closed directly from GitLab by using trigger words, eg. `Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and merge requests. -When a commit which contains the trigger word in the commit message is pushed, GitLab will add a comment in the mentioned Jira issue. +Jira issues can be closed directly from GitLab by using trigger words, eg. +`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and +merge requests. When a commit which contains the trigger word in the commit +message is pushed, GitLab will add a comment in the mentioned Jira issue. -For example, for project named PROJECT in Jira, we implemented a new feature and created a merge request in GitLab. +For example, for project named `PROJECT` in Jira, we implemented a new feature +and created a merge request in GitLab. -This feature was requested in Jira issue PROJECT-7. Merge request in GitLab contains the improvement and in merge request description we say that this merge request `Closes PROJECT-7` issue. +This feature was requested in Jira issue `PROJECT-7`. Merge request in GitLab +contains the improvement and in merge request description we say that this +merge request `Closes PROJECT-7` issue. -Once this merge request is merged, Jira issue will be automatically closed with a link to the commit that resolved the issue. +Once this merge request is merged, the Jira issue will be automatically closed +with a link to the commit that resolved the issue. -![A Git commit that causes the Jira issue to be closed](merge_request_close_jira.png) +![A Git commit that causes the Jira issue to be closed](img/jira_merge_request_close.png) +--- -![The GitLab integration user leaves a comment on Jira](jira_service_close_issue.png) +![The GitLab integration user leaves a comment on Jira](img/jira_service_close_issue.png) +--- ## Configuration ### Configuring JIRA -We need to create a user in JIRA which will have access to all projects that need to integrate with GitLab. -Login to your JIRA instance as admin and under Administration go to User Management and create a new user. -As an example, we'll create a user named `gitlab` and add it to `jira-developers` group. +We need to create a user in JIRA which will have access to all projects that +need to integrate with GitLab. Login to your JIRA instance as admin and under +Administration go to User Management and create a new user. + +As an example, we'll create a user named `gitlab` and add it to `jira-developers` +group. **It is important that the user `gitlab` has write-access to projects in JIRA** ### Configuring GitLab -### GitLab 7.8 EE and up with JIRA v6.x +JIRA configuration in GitLab is done via a project's **Services**. -To enable JIRA integration in a project, navigate to the project Settings page and go to Services. Here you will find JIRA. +#### GitLab 7.8 and up with JIRA v6.x -Fill in the required details on the page: +See next section. -![Jira service page](jira_service_page.png) +#### GitLab 7.8 and up -* `description` A name for the issue tracker (to differentiate between instances, for instance). -* `project url` The URL to the JIRA project which is being linked to this GitLab project. -* `issues url` The URL to the JIRA project issues overview for the project that is linked to this GitLab project. -* `new issue url` This is the URL to create a new issue in JIRA for the project linked to this GitLab project. -* `api url` The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`. -* `username` The username of the user created in [configuring JIRA step](#configuring-jira). -* `password` The password of the user created in [configuring JIRA step](#configuring-jira). -* `Jira issue transition` This is the id of a transition that moves issues to a closed state. You can find this number under [JIRA workflow administration, see screenshot](jira_workflow_screenshot.png). By default, this id is `2`. (In the example image, this is `2` as well) +_The currently supported JIRA versions are v6.x and v7.x._ -After saving the configuration, your GitLab project will be able to interact with the linked JIRA project. +To enable JIRA integration in a project, navigate to the project's +**Settings > Services > JIRA**. +Fill in the required details on the page as described in the table below. -### GitLab 6.x-7.7 with JIRA v6.x +| Field | Description | +| ----- | ----------- | +| `description` | A name for the issue tracker (to differentiate between instances, for instance). | +| `project url` | The URL to the JIRA project which is being linked to this GitLab project. | +| `issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. | +| `new issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project. | +| `api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`. | +| `username` | The username of the user created in [configuring JIRA step](#configuring-jira). | +| `password` |The password of the user created in [configuring JIRA step](#configuring-jira). | +| `Jira issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). By default, this ID is `2` (in the example image, this is `2` as well) | -**Note: GitLab 7.8 and up contain various integration improvements. We strongly recommend upgrading.** +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. +![Jira service page](img/jira_service_page.png) -In `gitlab.yml` enable [JIRA issue tracker section by uncommenting the lines](https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115). -This will make sure that all issues within GitLab are pointing to the JIRA issue tracker. +--- -We can also enable JIRA service that will allow us to interact with JIRA issues. +#### GitLab 6.x-7.7 with JIRA v6.x -For example, we can close issues in JIRA by a commit in GitLab. +_**Note:** GitLab versions 7.8 and up contain various integration improvements. +We strongly recommend upgrading._ -Go to project settings page and fill in the project name for the JIRA project: +In `gitlab.yml` enable the JIRA issue tracker section by +[uncommenting these lines][jira-gitlab-yml]. This will make sure that all +issues within GitLab are pointing to the JIRA issue tracker. -![Set the JIRA project name in GitLab to 'NEW'](jira_project_name.png) +After you set this, you will be able to close issues in JIRA by a commit in +GitLab. -Next, go to the services page and find JIRA. +Go to your project's **Settings** page and fill in the project name for the +JIRA project: -![Jira services page](jira_service.png) +![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) -1. Tick the active check box to enable the service. -1. Supply the url to JIRA server, for example http://jira.sample -1. Supply the username of a user we created under `Configuring JIRA` section, for example `gitlab` +--- + +You can also enable the JIRA service that will allow you to interact with JIRA +issues. Go to the **Settings > Services > JIRA** and: + +1. Tick the active check box to enable the service +1. Supply the URL to JIRA server, for example http://jira.example.com +1. Supply the username of a user we created under `Configuring JIRA` section, + for example `gitlab` 1. Supply the password of the user -1. Optional: supply the JIRA api version, default is version -1. Optional: supply the JIRA issue transition ID (issue transition to closed). This is dependant on JIRA settings, default is 2 -1. Save +1. Optional: supply the JIRA API version, default is version `2` +1. Optional: supply the JIRA issue transition ID (issue transition to closed). + This is dependent on JIRA settings, default is `2` +1. Hit save + + +![Jira services page](img/jira_service.png) -Now we should be able to interact with JIRA issues. +[services-templates]: ../project_services/services_templates.md +[jira-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 diff --git a/doc/integration/jira_service_page.png b/doc/integration/jira_service_page.png Binary files differdeleted file mode 100644 index 69ec44e826f..00000000000 --- a/doc/integration/jira_service_page.png +++ /dev/null diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 845f588f913..f256477196b 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -48,6 +48,11 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' + # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking + # a request if the LDAP server becomes unresponsive. + # A value of 0 means there is no timeout. + timeout: 10 + # This setting specifies if LDAP server is Active Directory LDAP server. # For non AD servers it skips the AD specific queries. # If your LDAP server is not AD, set this to false. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index f2b1721fc03..e9e17eb4165 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -78,6 +78,7 @@ Now we can choose one or more of the Supported Providers below to continue confi - [Shibboleth](shibboleth.md) - [SAML](saml.md) - [Crowd](crowd.md) +- [Azure](azure.md) ## Enable OmniAuth for an Existing User diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md index 7e6f7e7e30a..a301d1a613c 100644 --- a/doc/integration/recaptcha.md +++ b/doc/integration/recaptcha.md @@ -6,51 +6,18 @@ to confirm that a real user, not a bot, is attempting to create an account. ## Configuration -To use reCAPTCHA, first you must create a public and private key. +To use reCAPTCHA, first you must create a site and private key. 1. Go to the URL: https://www.google.com/recaptcha/admin -1. Fill out the form necessary to obtain reCAPTCHA keys. +2. Fill out the form necessary to obtain reCAPTCHA keys. -1. On your GitLab server, open the configuration file. +3. Login to your GitLab server, with administrator credentials. - For omnibus package: +4. Go to Applications Settings on Admin Area (`admin/application_settings`) - ```sh - sudo editor /etc/gitlab/gitlab.rb - ``` +5. Fill all recaptcha fields with keys from previous steps - For installations from source: +6. Check the `Enable reCAPTCHA` checkbox - ```sh - cd /home/git/gitlab - - sudo -u git -H editor config/gitlab.yml - ``` - -1. Enable reCAPTCHA and add the settings: - - For omnibus package: - - ```ruby - gitlab_rails['recaptcha_enabled'] = true - gitlab_rails['recaptcha_public_key'] = 'YOUR_PUBLIC_KEY' - gitlab_rails['recaptcha_private_key'] = 'YOUR_PUBLIC_KEY' - ``` - - For installation from source: - - ``` - recaptcha: - enabled: true - public_key: 'YOUR_PUBLIC_KEY' - private_key: 'YOUR_PRIVATE_KEY' - ``` - -1. Change 'YOUR_PUBLIC_KEY' to the public key from step 2. - -1. Change 'YOUR_PRIVATE_KEY' to the private key from step 2. - -1. Save the configuration file. - -1. Restart GitLab. +7. Save the configuration. diff --git a/doc/integration/redmine_configuration.png b/doc/integration/redmine_configuration.png Binary files differdeleted file mode 100644 index 6b145363229..00000000000 --- a/doc/integration/redmine_configuration.png +++ /dev/null diff --git a/doc/integration/redmine_service_template.png b/doc/integration/redmine_service_template.png Binary files differdeleted file mode 100644 index 1159eb5b964..00000000000 --- a/doc/integration/redmine_service_template.png +++ /dev/null diff --git a/doc/project_services/img/redmine_configuration.png b/doc/project_services/img/redmine_configuration.png Binary files differnew file mode 100644 index 00000000000..d14e526ad33 --- /dev/null +++ b/doc/project_services/img/redmine_configuration.png diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/project_services/img/services_templates_redmine_example.png Binary files differnew file mode 100644 index 00000000000..384d057fc8e --- /dev/null +++ b/doc/project_services/img/services_templates_redmine_example.png diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 03937d20728..e3403127723 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -1,20 +1,37 @@ # Project Services - -__Project integrations with external services for continuous integration and more.__ + +Project services allow you to integrate GitLab with other applications. Below +is list of the currently supported ones. Click on the service links to see +further configuration instructions and details. Contributions are welcome. ## Services -- Assembla -- [Atlassian Bamboo CI](bamboo.md) An Atlassian product for continuous integration. -- Build box -- Campfire -- Emails on push -- Flowdock -- Gemnasium -- GitLab CI -- [HipChat](hipchat.md) An Atlassian product for private group chat and instant messaging. -- [Irker](irker.md) An IRC gateway to receive messages on repository updates. -- Pivotal Tracker -- Pushover -- Slack -- TeamCity +| Service | Description | +| ------- | ----------- | +| Asana | Asana - Teamwork without email | +| Assembla | Project Management Software (Source Commits Endpoint) | +| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server | +| Buildkite | Continuous integration and deployments | +| Builds emails | Email the builds status to a list of recipients | +| Campfire | Simple web-based real-time group chat | +| Custom Issue Tracker | Custom issue tracker | +| Drone CI | Continuous Integration platform built on Docker, written in Go | +| Emails on push | Email the commits and diff of each push to a list of recipients | +| External Wiki | Replaces the link to the internal wiki with a link to an external wiki | +| Flowdock | Flowdock is a collaboration web app for technical teams | +| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | +| [HipChat](hipchat.md) | Private group chat and IM | +| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | +| JIRA | Jira issue tracker | +| JetBrains TeamCity CI | A continuous integration and build server | +| PivotalTracker | Project Management Software (Source Commits Endpoint) | +| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | +| [Redmine](redmine.md) | Redmine issue tracker | +| Slack | A team communication tool for the 21st century | + +## Services Templates + +Services templates is a way to set some predefined values in the Service of +your liking which will then be pre-filled on each project's Service. + +Read more about [Services Templates in this document](services_templates.md). diff --git a/doc/project_services/redmine.md b/doc/project_services/redmine.md new file mode 100644 index 00000000000..b9830ea7c38 --- /dev/null +++ b/doc/project_services/redmine.md @@ -0,0 +1,21 @@ +# Redmine Service + +Go to your project's **Settings > Services > Redmine** and fill in the required +details as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `description` | A name for the issue tracker (to differentiate between instances, for example) | +| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project | +| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | +| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project | + +Once you have configured and enabled Redmine: + +- the **Issues** link on the GitLab project pages takes you to the appropriate + Redmine issue index +- clicking **New issue** on the project dashboard creates a new Redmine issue + +As an example, below is a configuration for a project named gitlab-ci. + +![Redmine configuration](img/redmine_configuration.png) diff --git a/doc/project_services/services_templates.md b/doc/project_services/services_templates.md new file mode 100644 index 00000000000..be6d13b6d2b --- /dev/null +++ b/doc/project_services/services_templates.md @@ -0,0 +1,25 @@ +# Services Templates + +A GitLab administrator can add a service template that sets a default for each +project. This makes it much easier to configure individual projects. + +After the template is created, the template details will be pre-filled on a +project's Service page. + +## Enable a Service template + +In GitLab's Admin area, navigate to **Service Templates** and choose the +service template you wish to create. + +For example, in the image below you can see Redmine. + +![Redmine service template](img/services_templates_redmine_example.png) + +--- + +**NOTE:** For each project, you will still need to configure the issue tracking +URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used +by your external issue tracker. Prior to GitLab v7.8, this ID was configured in +the project settings, and GitLab would automatically update the URL configured +in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs +must be configured directly within the project's **Services** settings. 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/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 3748941b781..a3950df5fef 100644 --- a/doc/update/8.2-to-8.3.md +++ b/doc/update/8.2-to-8.3.md @@ -78,7 +78,7 @@ which should already be on your system from GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout 0.5.1 +sudo -u git -H git checkout 0.5.3 sudo -u git -H make ``` diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md index 8c9b4f72631..fffa0aba57f 100644 --- a/doc/workflow/add-user/add-user.md +++ b/doc/workflow/add-user/add-user.md @@ -1,25 +1,89 @@ # Project users -You can manage the groups and users and their access levels in all of your projects. You can also personalize the access level you give each user, per project. +You can manage the groups and users and their access levels in all of your +projects. You can also personalize the access level you give each user, +per-project. -Here's how to add or import users to your projects. - -You should have 'master' or 'owner' permissions to add or import a new user +You should have `master` or `owner` permissions to add or import a new user to your project. -To add or import a user, go to your project and click on "Members" on the left side of your screen: +The first step to add or import a user, go to your project and click on +**Members** on the left side of your screen. + +![Members](img/add_user_members_menu.png) + +--- + +## Add a user + +Right next to **People**, start typing the name or username of the user you +want to add. + +![Search for people](img/add_user_search_people.png) + +--- + +Select the user and the [permission level](../../permissions/permissions.md) +that you'd like to give the user. Note that you can select more than one user. + +![Give user permissions](img/add_user_give_permissions.png) + +--- + +Once done, hit **Add users to project** and they will be immediately added to +your project with the permissions you gave them above. + +![List members](img/add_user_list_members.png) + +--- + +From there on, you can either remove an existing user or change their access +level to the project. + +## Import users from another project + +You can import another project's users in your own project by hitting the +**Import members** button on the upper right corner of the **Members** menu. + +In the dropdown menu, you can see only the projects you are Master on. + +![Import members from another project](img/add_user_import_members_from_another_project.png) + +--- + +Select the one you want and hit **Import project members**. A flash message +notifying you that the import was successful will appear, and the new members +are now in the project's members list. Notice that the permissions that they +had on the project you imported from are retained. + +![Members list of new members](img/add_user_imported_members.png) + +--- + +## Invite people using their e-mail address + +If a user you want to give access to doesn't have an account on your GitLab +instance, you can invite them just by typing their e-mail address in the +user search field. + +![Invite user by mail](img/add_user_email_search.png) + +--- -![Members](images/members.png) +As you can imagine, you can mix inviting multiple people and adding existing +GitLab users to the project. -Select "Add members" or "Import members" on the right side of your screen: +![Invite user by mail ready to submit](img/add_user_email_ready.png) -![Add or Import](images/add-members.png) +--- -If you are adding a user, select the user and the [permission level](doc/permissions/permissions.md) that you'd like to -give the user: +Once done, hit **Add users to project** and watch that there is a new member +with the e-mail address we used above. From there on, you can resend the +invitation, change their access level or even delete them. -![Add or Import](images/new-member.png) +![Invite user members list](img/add_user_email_accept.png) -If you are importing a user, follow the steps to select the project where you'd like to import the user from: +--- -![Add or Import](images/select-project.png) +Once the user accepts the invitation, they will be prompted to create a new +GitLab account using the same e-mail address the invitation was sent to. diff --git a/doc/workflow/add-user/images/add-members.png b/doc/workflow/add-user/images/add-members.png Binary files differdeleted file mode 100644 index 2805c5764a5..00000000000 --- a/doc/workflow/add-user/images/add-members.png +++ /dev/null diff --git a/doc/workflow/add-user/images/new-member.png b/doc/workflow/add-user/images/new-member.png Binary files differdeleted file mode 100644 index d500daea56e..00000000000 --- a/doc/workflow/add-user/images/new-member.png +++ /dev/null diff --git a/doc/workflow/add-user/images/select-project.png b/doc/workflow/add-user/images/select-project.png Binary files differdeleted file mode 100644 index dd3844edff8..00000000000 --- a/doc/workflow/add-user/images/select-project.png +++ /dev/null diff --git a/doc/workflow/add-user/img/add_new_user_to_project_settings.png b/doc/workflow/add-user/img/add_new_user_to_project_settings.png Binary files differnew file mode 100644 index 00000000000..3da18cdae53 --- /dev/null +++ b/doc/workflow/add-user/img/add_new_user_to_project_settings.png diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/workflow/add-user/img/add_user_email_accept.png Binary files differnew file mode 100644 index 00000000000..910affc9659 --- /dev/null +++ b/doc/workflow/add-user/img/add_user_email_accept.png diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/workflow/add-user/img/add_user_email_ready.png Binary files differnew file mode 100644 index 00000000000..5f02ce89b3e --- /dev/null +++ b/doc/workflow/add-user/img/add_user_email_ready.png diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/workflow/add-user/img/add_user_email_search.png Binary files differnew file mode 100644 index 00000000000..140979fbe13 --- /dev/null +++ b/doc/workflow/add-user/img/add_user_email_search.png diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/workflow/add-user/img/add_user_give_permissions.png Binary files differnew file mode 100644 index 00000000000..8ef9156c8d5 --- /dev/null +++ b/doc/workflow/add-user/img/add_user_give_permissions.png diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png Binary files differnew file mode 100644 index 00000000000..5770d5cf0c4 --- /dev/null +++ b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/workflow/add-user/img/add_user_imported_members.png Binary files differnew file mode 100644 index 00000000000..dea4b3f40ad --- /dev/null +++ b/doc/workflow/add-user/img/add_user_imported_members.png diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/workflow/add-user/img/add_user_list_members.png Binary files differnew file mode 100644 index 00000000000..7daa6ca7d9e --- /dev/null +++ b/doc/workflow/add-user/img/add_user_list_members.png diff --git a/doc/workflow/add-user/images/members.png b/doc/workflow/add-user/img/add_user_members_menu.png Binary files differindex f1797b95f67..f1797b95f67 100644 --- a/doc/workflow/add-user/images/members.png +++ b/doc/workflow/add-user/img/add_user_members_menu.png diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/workflow/add-user/img/add_user_search_people.png Binary files differnew file mode 100644 index 00000000000..5ac10ce80d4 --- /dev/null +++ b/doc/workflow/add-user/img/add_user_search_people.png diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index 2d77c6d1172..2027a055c37 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -14,7 +14,7 @@ If you want to import from a GitHub Enterprise instance, you need to use GitLab ![Importer page](github_importer/importer.png)
-* To import a project, you can simple click "Add". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data.
+* To import a project, you can simple click "Add". The importer will import your repository, issues, and pull requests. Once the importer is done, a new GitLab project will be created with your imported data.
### Note
-When you import your projects from GitHub, it is not possible to keep your labels and milestones. We are working on improving this in the near future.
+When you import your projects from GitHub, it is not possible to keep your labels, milestones, and cross-repository pull requests. We are working on improving this in the near future.
diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png Binary files differindex 68756ed1f98..e5914aa8e67 100644 --- a/doc/workflow/shortcuts.png +++ b/doc/workflow/shortcuts.png 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/commits/commits.feature b/features/project/commits/commits.feature index 5bb2d0e976b..01c10721312 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -55,3 +55,8 @@ Feature: Project Commits Scenario: I browse a commit with an image Given I visit a commit with an image that changed Then The diff links to both the previous and current image + + @javascript + Scenario: I filter commits by message + When I search "submodules" commits + Then I should see only "submodules" commits diff --git a/features/project/find_file.feature b/features/project/find_file.feature new file mode 100644 index 00000000000..ae8fa245923 --- /dev/null +++ b/features/project/find_file.feature @@ -0,0 +1,42 @@ +@dashboard +Feature: Project Find File + Background: + Given I sign in as a user + And I own a project + And I visit my project's files page + + @javascript + Scenario: Navigate to find file by shortcut + Given I press "t" + Then I should see "find file" page + + Scenario: Navigate to find file + Given I click Find File button + Then I should see "find file" page + + @javascript + Scenario: I search file + Given I visit project find file page + And I fill in file find with "change" + Then I should not see ".gitignore" in files + And I should not see ".gitmodules" in files + And I should see "CHANGELOG" in files + And I should not see "VERSION" in files + + @javascript + Scenario: I search file that not exist + Given I visit project find file page + And I fill in file find with "asdfghjklqwertyuizxcvbnm" + Then I should not see ".gitignore" in files + And I should not see ".gitmodules" in files + And I should not see "CHANGELOG" in files + And I should not see "VERSION" in files + + @javascript + Scenario: I search file that partially matches + Given I visit project find file page + And I fill in file find with "git" + Then I should see ".gitignore" in files + And I should see ".gitmodules" in files + And I should not see "CHANGELOG" in files + And I should not see "VERSION" in files 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/group/milestones.rb b/features/steps/group/milestones.rb index 6e57b16ccb6..2363ad797fa 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -28,7 +28,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end step 'I should see group milestone with descriptions and expiry date' do - expect(page).to have_content('expires at Aug 20, 2114') + expect(page).to have_content('expires on Aug 20, 2114') end step 'I should see group milestone with all issues and MRs assigned to that milestone' do diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index a3141fe3be1..daf6cdaaac8 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -124,4 +124,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content "build: pending" expect(page).to have_content "1 build" end + + step 'I search "submodules" commits' do + fill_in 'commits-search', with: 'submodules' + end + + step 'I should see only "submodules" commits' do + expect(page).to have_content "More submodules" + expect(page).not_to have_content "Change some files" + end end 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 1404f34cfe0..2c2ed08655e 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -24,7 +24,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps page.within '.awards' do expect do page.find('.award.active').click - sleep 0.1 + sleep 0.3 end.to change{ page.all(".award").size }.from(3).to(2) 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/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb new file mode 100644 index 00000000000..8c1d09d6cc6 --- /dev/null +++ b/features/steps/project/project_find_file.rb @@ -0,0 +1,73 @@ +class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + include SharedProjectTab + + step 'I press "t"' do + find('body').native.send_key('t') + end + + step 'I click Find File button' do + click_link 'Find File' + end + + step 'I should see "find file" page' do + ensure_active_main_tab('Files') + expect(page).to have_selector('.file-finder-holder', count: 1) + end + + step 'I fill in Find by path with "git"' do + ensure_active_main_tab('Files') + expect(page).to have_selector('.file-finder-holder', count: 1) + end + + step 'I fill in file find with "git"' do + find_file "git" + end + + step 'I fill in file find with "change"' do + find_file "change" + end + + step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do + find_file "asdfghjklqwertyuizxcvbnm" + end + + step 'I should see "VERSION" in files' do + expect(page).to have_content("VERSION") + end + + step 'I should not see "VERSION" in files' do + expect(page).not_to have_content("VERSION") + end + + step 'I should see "CHANGELOG" in files' do + expect(page).to have_content("CHANGELOG") + end + + step 'I should not see "CHANGELOG" in files' do + expect(page).not_to have_content("CHANGELOG") + end + + step 'I should see ".gitmodules" in files' do + expect(page).to have_content(".gitmodules") + end + + step 'I should not see ".gitmodules" in files' do + expect(page).not_to have_content(".gitmodules") + end + + step 'I should see ".gitignore" in files' do + expect(page).to have_content(".gitignore") + end + + step 'I should not see ".gitignore" in files' do + expect(page).not_to have_content(".gitignore") + end + + + def find_file(text) + fill_in 'file_find', with: text + end +end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index b33bd332655..4264c9c6f1a 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -259,6 +259,10 @@ module SharedPaths visit namespace_project_deploy_keys_path(@project.namespace, @project) end + step 'I visit project find file page' do + visit namespace_project_find_file_path(@project.namespace, @project, root_ref) + end + # ---------------------------------------- # "Shop" Project # ---------------------------------------- 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/merge_requests.rb b/lib/api/merge_requests.rb index 3c1c6bda260..5c97fe1c88c 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -211,7 +211,7 @@ module API unauthorized! unless merge_request.can_be_merged_by?(current_user) not_allowed! if !merge_request.open? || merge_request.work_in_progress? - merge_request.check_if_can_be_merged if merge_request.unchecked? + merge_request.check_if_can_be_merged render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? diff --git a/lib/api/projects.rb b/lib/api/projects.rb index a9e0960872a..8b1390e3289 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) @@ -269,7 +269,7 @@ module API # Remove a forked_from relationship # # Parameters: - # id: (required) - The ID of the project being marked as a fork + # id: (required) - The ID of the project being marked as a fork # Example Request: # DELETE /projects/:id/fork delete ":id/fork" do @@ -278,6 +278,16 @@ module API user_project.forked_project_link.destroy end end + + # Upload a file + # + # Parameters: + # id: (required) - The ID of the project + # file: (required) - The file to be uploaded + post ":id/uploads" do + ::Projects::UploadService.new(user_project, params[:file]).execute + end + # search for projects current_user has access to # # Parameters: diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 47621f443e6..2d8a9e51bb9 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -40,6 +40,27 @@ module API end end + # Delete tag + # + # Parameters: + # id (required) - The ID of a project + # tag_name (required) - The name of the tag + # Example Request: + # DELETE /projects/:id/repository/tags/:tag + delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.*/ } do + authorize_push_project + result = DeleteTagService.new(user_project, current_user). + execute(params[:tag_name]) + + if result[:status] == :success + { + tag_name: params[:tag_name] + } + else + render_api_error!(result[:message], result[:return_code]) + end + end + # Add release notes to tag # # Parameters: diff --git a/lib/api/users.rb b/lib/api/users.rb index 3400f0713ef..0d7813428e2 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -39,7 +39,7 @@ module API if current_user.is_admin? present @user, with: Entities::UserFull else - present @user, with: Entities::UserBasic + present @user, with: Entities::User end end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 63ad8910c0f..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 @@ -60,27 +70,31 @@ module Banzai end def call - # `#123` - replace_text_nodes_matching(object_class.reference_pattern) do |content| - object_link_filter(content, object_class.reference_pattern) - end + if object_class.reference_pattern + # `#123` + replace_text_nodes_matching(object_class.reference_pattern) do |content| + object_link_filter(content, object_class.reference_pattern) + end - # `[Issue](#123)`, which is turned into - # `<a href="#123">Issue</a>` - replace_link_nodes_with_href(object_class.reference_pattern) do |link, text| - object_link_filter(link, object_class.reference_pattern, link_text: text) + # `[Issue](#123)`, which is turned into + # `<a href="#123">Issue</a>` + replace_link_nodes_with_href(object_class.reference_pattern) do |link, text| + object_link_filter(link, object_class.reference_pattern, link_text: text) + end end - # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into - # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>` - replace_link_nodes_with_text(object_class.link_reference_pattern) do |text| - object_link_filter(text, object_class.link_reference_pattern) - end + if object_class.link_reference_pattern + # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into + # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>` + replace_link_nodes_with_text(object_class.link_reference_pattern) do |text| + object_link_filter(text, object_class.link_reference_pattern) + end - # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into - # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>` - replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text| - object_link_filter(link, object_class.link_reference_pattern, link_text: text) + # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into + # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>` + replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text| + object_link_filter(link, object_class.link_reference_pattern, link_text: text) + end end end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb new file mode 100644 index 00000000000..e88b27c1fae --- /dev/null +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -0,0 +1,22 @@ +require 'banzai' + +module Banzai + module Filter + # HTML filter that replaces milestone references with links. + class MilestoneReferenceFilter < AbstractReferenceFilter + def self.object_class + Milestone + end + + def find_object(project, id) + project.milestones.find_by(iid: id) + end + + def url_for_object(issue, project) + h = Gitlab::Application.routes.url_helpers + h.namespace_project_milestone_url(project.namespace, project, milestone, + only_path: context[:only_path]) + end + end + end +end diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb index f01a32b5ae5..66f77902319 100644 --- a/lib/banzai/filter/redactor_filter.rb +++ b/lib/banzai/filter/redactor_filter.rb @@ -10,7 +10,7 @@ module Banzai # class RedactorFilter < HTML::Pipeline::Filter def call - doc.css('a.gfm').each do |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. diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 8ca05ace88c..7198a8b03e2 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -124,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') @@ -162,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 12412ff7ea9..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 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/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb index bdf7c2ebdfc..d0ce13003a5 100644 --- a/lib/banzai/filter/task_list_filter.rb +++ b/lib/banzai/filter/task_list_filter.rb @@ -12,13 +12,18 @@ module Banzai # # See https://github.com/github/task_list/pull/60 class TaskListFilter < TaskList::Filter - def add_css_class(node, *new_class_names) + def add_css_class_with_fix(node, *new_class_names) if new_class_names.include?('task-list') - super if node.children.any? { |c| c['class'] == 'task-list-item' } - else - super + # Don't add class to all lists + return + elsif new_class_names.include?('task-list-item') + add_css_class_without_fix(node.parent, 'task-list') end + + add_css_class_without_fix(node, *new_class_names) end + + alias_method_chain :add_css_class, :fix end end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 38750b55ec7..838155e8831 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -22,6 +22,7 @@ module Banzai Filter::CommitRangeReferenceFilter, Filter::CommitReferenceFilter, Filter::LabelReferenceFilter, + Filter::MilestoneReferenceFilter, Filter::TaskListFilter ] diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb index 6725c9039a9..a3c9d4f43aa 100644 --- a/lib/banzai/pipeline/single_line_pipeline.rb +++ b/lib/banzai/pipeline/single_line_pipeline.rb @@ -3,7 +3,23 @@ require 'banzai' module Banzai module Pipeline class SingleLinePipeline < GfmPipeline + def self.filters + @filters ||= [ + Filter::SanitizationFilter, + Filter::EmojiFilter, + Filter::AutolinkFilter, + Filter::ExternalLinkFilter, + + Filter::UserReferenceFilter, + Filter::IssueReferenceFilter, + Filter::ExternalIssueReferenceFilter, + Filter::MergeRequestReferenceFilter, + Filter::SnippetReferenceFilter, + Filter::CommitRangeReferenceFilter, + Filter::CommitReferenceFilter, + ] + end end end end 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..891c0fd7749 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,7 +18,7 @@ module Banzai cache_key = context.delete(:cache_key) cache_key = full_cache_key(cache_key, context[:pipeline]) - if cache_key && CACHE_ENABLED + if cache_key Rails.cache.fetch(cache_key) do cacheless_render(text, context) end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 459e3d6bcdb..4c15d58d680 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -150,6 +150,18 @@ module Gitlab "#{path}.git", tag_name]) end + # Gc repository + # + # path - project path with namespace + # + # Ex. + # gc("gitlab/gitlab-ci") + # + def gc(path) + Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'gc', + "#{path}.git"]) + end + # Add new key to gitlab-shell # # Ex. diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb index 86bfa0a4378..34e949130da 100644 --- a/lib/gitlab/build_data_builder.rb +++ b/lib/gitlab/build_data_builder.rb @@ -23,6 +23,7 @@ module Gitlab build_started_at: build.started_at, build_finished_at: build.finished_at, build_duration: build.duration, + build_allow_failure: build.allow_failure, # TODO: do we still need it? project_id: project.id, 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..7f938780ab1 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -38,7 +38,12 @@ 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') + + rescue ActiveRecord::NoDatabaseError + false end end end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 2b252b32887..2ca21af5bc8 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -74,7 +74,7 @@ module Gitlab def sent_notification return nil unless reply_key - + SentNotification.for(reply_key) end @@ -82,10 +82,7 @@ module Gitlab attachments = Email::AttachmentUploader.new(message).execute(sent_notification.project) attachments.each do |link| - text = "[#{link[:alt]}](#{link[:url]})" - text.prepend("!") if link[:is_image] - - reply << "\n\n#{text}" + reply << "\n\n#{link[:markdown]}" end reply diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 403ebeec474..db580b5e578 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -232,9 +232,7 @@ module Gitlab return nil if res.nil? - text = "[#{res['alt']}](#{res['url']})" - text = "!#{text}" if res['is_image'] - text + res[:markdown] end def build_attachment_url(rel_url) diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb new file mode 100644 index 00000000000..202263c6742 --- /dev/null +++ b/lib/gitlab/github_import/base_formatter.rb @@ -0,0 +1,21 @@ +module Gitlab + module GithubImport + class BaseFormatter + attr_reader :formatter, :project, :raw_data + + def initialize(project, raw_data) + @project = project + @raw_data = raw_data + @formatter = Gitlab::ImportFormatter.new + end + + private + + def gl_user_id(github_id) + User.joins(:identities). + find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). + try(:id) + end + end + end +end diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb new file mode 100644 index 00000000000..7d58e53991a --- /dev/null +++ b/lib/gitlab/github_import/comment_formatter.rb @@ -0,0 +1,45 @@ +module Gitlab + module GithubImport + class CommentFormatter < BaseFormatter + def attributes + { + project: project, + note: note, + commit_id: raw_data.commit_id, + line_code: line_code, + author_id: author_id, + created_at: raw_data.created_at, + updated_at: raw_data.updated_at + } + end + + private + + def author + raw_data.user.login + end + + def author_id + gl_user_id(raw_data.user.id) || project.creator_id + end + + def body + raw_data.body || "" + end + + def line_code + if on_diff? + Gitlab::Diff::LineCode.generate(raw_data.path, raw_data.position, 0) + end + end + + def on_diff? + raw_data.path && raw_data.position + end + + def note + formatter.author_line(author) + body + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index b5720f6e2cb..2b0afbc7b39 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -12,39 +12,59 @@ module Gitlab end def execute - #Issues && Comments + import_issues + import_pull_requests + + true + end + + private + + def import_issues client.list_issues(project.import_source, state: :all, sort: :created, - direction: :asc).each do |issue| - if issue.pull_request.nil? - - body = @formatter.author_line(issue.user.login) - body += issue.body || "" + direction: :asc).each do |raw_data| + gh_issue = IssueFormatter.new(project, raw_data) - if issue.comments > 0 - body += @formatter.comments_header + if gh_issue.valid? + issue = Issue.create!(gh_issue.attributes) - client.issue_comments(project.import_source, issue.number).each do |c| - body += @formatter.comment(c.user.login, c.created_at, c.body) - end + if gh_issue.has_comments? + import_comments(gh_issue.number, issue) end + end + end + end + + def import_pull_requests + client.pull_requests(project.import_source, state: :all, + sort: :created, + direction: :asc).each do |raw_data| + pull_request = PullRequestFormatter.new(project, raw_data) - project.issues.create!( - description: body, - title: issue.title, - state: issue.state == 'closed' ? 'closed' : 'opened', - author_id: gl_user_id(project, issue.user.id) - ) + if !pull_request.cross_project? && pull_request.valid? + merge_request = MergeRequest.create!(pull_request.attributes) + import_comments(pull_request.number, merge_request) + import_comments_on_diff(pull_request.number, merge_request) end end end - private + def import_comments(issue_number, noteable) + comments = client.issue_comments(project.import_source, issue_number) + create_comments(comments, noteable) + end - def gl_user_id(project, github_id) - user = User.joins(:identities). - find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s) - (user && user.id) || project.creator_id + def import_comments_on_diff(pull_request_number, merge_request) + comments = client.pull_request_comments(project.import_source, pull_request_number) + create_comments(comments, merge_request) + end + + def create_comments(comments, noteable) + comments.each do |raw_data| + comment = CommentFormatter.new(project, raw_data) + noteable.notes.create!(comment.attributes) + end end end end diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb new file mode 100644 index 00000000000..1e3ba44f27c --- /dev/null +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -0,0 +1,66 @@ +module Gitlab + module GithubImport + class IssueFormatter < BaseFormatter + def attributes + { + project: project, + title: raw_data.title, + description: description, + state: state, + author_id: author_id, + assignee_id: assignee_id, + created_at: raw_data.created_at, + updated_at: updated_at + } + end + + def has_comments? + raw_data.comments > 0 + end + + def number + raw_data.number + end + + def valid? + raw_data.pull_request.nil? + end + + private + + def assigned? + raw_data.assignee.present? + end + + def assignee_id + if assigned? + gl_user_id(raw_data.assignee.id) + end + end + + def author + raw_data.user.login + end + + def author_id + gl_user_id(raw_data.user.id) || project.creator_id + end + + def body + raw_data.body || "" + end + + def description + @formatter.author_line(author) + body + end + + def state + raw_data.state == 'closed' ? 'closed' : 'opened' + end + + def updated_at + state == 'closed' ? raw_data.closed_at : raw_data.updated_at + end + end + end +end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb new file mode 100644 index 00000000000..b7c47958cc7 --- /dev/null +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -0,0 +1,101 @@ +module Gitlab + module GithubImport + class PullRequestFormatter < BaseFormatter + def attributes + { + title: raw_data.title, + description: description, + source_project: source_project, + source_branch: source_branch.name, + target_project: target_project, + target_branch: target_branch.name, + state: state, + author_id: author_id, + assignee_id: assignee_id, + created_at: raw_data.created_at, + updated_at: updated_at + } + end + + def cross_project? + source_repo.fork == true + end + + def number + raw_data.number + end + + def valid? + source_branch.present? && target_branch.present? + end + + private + + def assigned? + raw_data.assignee.present? + end + + def assignee_id + if assigned? + gl_user_id(raw_data.assignee.id) + end + end + + def author + raw_data.user.login + end + + def author_id + gl_user_id(raw_data.user.id) || project.creator_id + end + + def body + raw_data.body || "" + end + + def description + formatter.author_line(author) + body + end + + def source_project + project + end + + def source_repo + raw_data.head.repo + end + + def source_branch + source_project.repository.find_branch(raw_data.head.ref) + end + + def target_project + project + end + + def target_branch + target_project.repository.find_branch(raw_data.base.ref) + end + + def state + @state ||= case true + when raw_data.state == 'closed' && raw_data.merged_at.present? + 'merged' + when raw_data.state == 'closed' + 'closed' + else + 'opened' + end + end + + def updated_at + case state + when 'merged' then raw_data.merged_at + when 'closed' then raw_data.closed_at + else + raw_data.updated_at + end + end + end + end +end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index c438a3d167b..b2bdbc10d7f 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -5,7 +5,7 @@ module Gitlab module LDAP class Access - attr_reader :adapter, :provider, :user + attr_reader :provider, :user def self.open(user, &block) Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter| @@ -32,7 +32,7 @@ module Gitlab end def allowed? - if Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) + if ldap_user return true unless ldap_config.active_directory # Block user in GitLab if he/she was blocked in AD @@ -59,6 +59,10 @@ module Gitlab def ldap_config Gitlab::LDAP::Config.new(provider) end + + def ldap_user + @ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) + end end end end diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 577a890a7d9..df65179bfea 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -70,19 +70,25 @@ module Gitlab end def ldap_search(*args) - results = ldap.search(*args) + # Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead. + Timeout.timeout(config.timeout) do + results = ldap.search(*args) - if results.nil? - response = ldap.get_operation_result + if results.nil? + response = ldap.get_operation_result - unless response.code.zero? - Rails.logger.warn("LDAP search error: #{response.message}") - end + unless response.code.zero? + Rails.logger.warn("LDAP search error: #{response.message}") + end - [] - else - results + [] + else + results + end end + rescue Timeout::Error + Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds") + [] end end end diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 101a3285f4b..aff7ccb157f 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -88,6 +88,10 @@ module Gitlab options['attributes'] end + def timeout + options['timeout'].to_i + end + protected def base_config Gitlab.config.ldap diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 9470633b065..44356a0e42c 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -1,36 +1,24 @@ 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}\/?/ - # Returns the current settings, ensuring we _always_ have a default set of - # metrics settings (even during tests, when the migrations are lacking, - # etc). This ensures the application is able to boot up even when the - # migrations have not been executed. def self.settings - if ApplicationSetting.table_exists? and curr = ApplicationSetting.current - curr - else - { - metrics_pool_size: 16, - metrics_timeout: 10, - metrics_enabled: false, - metrics_method_call_threshold: 10 - } - end - end - - def self.pool_size - settings[:metrics_pool_size] - end - - def self.timeout - settings[:metrics_timeout] + @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], + port: current_application_settings[:metrics_port] + } end def self.enabled? - settings[:metrics_enabled] + settings[:enabled] || false end def self.mri? @@ -41,17 +29,13 @@ module Gitlab # 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[:metrics_method_call_threshold] + @method_call_threshold ||= settings[:method_call_threshold] end def self.pool @pool end - def self.hostname - @hostname - end - # Returns a relative path and line number based on the last application call # frame. def self.last_relative_application_frame @@ -66,18 +50,48 @@ module Gitlab end end - @hostname = Socket.gethostname + 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: pool_size, timeout: timeout) do - host = settings[:metrics_host] - db = settings[:metrics_database] - user = settings[:metrics_username] - pw = settings[:metrics_password] + @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do + host = settings[:host] + port = settings[:port] - InfluxDB::Client.new(db, host: host, username: user, password: pw) + InfluxDB::Client. + new(udp: { host: host, port: port }) end end end diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index 06fc2f25948..d9fce2e6758 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -123,6 +123,8 @@ module Gitlab duration = (Time.now - start) * 1000.0 if duration >= Gitlab::Metrics.method_call_threshold + trans.increment(:method_duration, duration) + trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, { duration: duration }, method: #{label.inspect}) diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb index f592f4e571f..7ea9555cc8c 100644 --- a/lib/gitlab/metrics/metric.rb +++ b/lib/gitlab/metrics/metric.rb @@ -17,16 +17,10 @@ module Gitlab # Returns a Hash in a format that can be directly written to InfluxDB. def to_hash { - series: @series, - tags: @tags.merge( - hostname: Metrics.hostname, - ruby_engine: RUBY_ENGINE, - ruby_version: RUBY_VERSION, - gitlab_version: Gitlab::VERSION, - process_type: Sidekiq.server? ? 'sidekiq' : 'rails' - ), + series: @series, + tags: @tags, values: @values, - timestamp: @created_at.to_i + timestamp: @created_at.to_i * 1_000_000_000 } end end diff --git a/lib/gitlab/metrics/obfuscated_sql.rb b/lib/gitlab/metrics/obfuscated_sql.rb deleted file mode 100644 index 481aca56efb..00000000000 --- a/lib/gitlab/metrics/obfuscated_sql.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Gitlab - module Metrics - # Class for producing SQL queries with sensitive data stripped out. - class ObfuscatedSQL - REPLACEMENT = / - \d+(\.\d+)? # integers, floats - | '.+?' # single quoted strings - | \/.+?(?<!\\)\/ # regexps (including escaped slashes) - /x - - MYSQL_REPLACEMENTS = / - ".+?" # double quoted strings - /x - - # Regex to replace consecutive placeholders with a single one indicating - # the length. This can be useful when a "IN" statement uses thousands of - # IDs (storing this would just be a waste of space). - CONSECUTIVE = /(\?(\s*,\s*)?){2,}/ - - # sql - The raw SQL query as a String. - def initialize(sql) - @sql = sql - end - - # Returns a new, obfuscated SQL query. - def to_s - regex = REPLACEMENT - - if Gitlab::Database.mysql? - regex = Regexp.union(regex, MYSQL_REPLACEMENTS) - end - - sql = @sql.gsub(regex, '?').gsub(CONSECUTIVE) do |match| - "#{match.count(',') + 1} values" - end - - # InfluxDB escapes double quotes upon output, so lets get rid of them - # whenever we can. - if Gitlab::Database.postgresql? - sql = sql.delete('"') - end - - sql - end - end - end -end diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb index 5c0587c4c51..6f179789d3e 100644 --- a/lib/gitlab/metrics/rack_middleware.rb +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -32,17 +32,15 @@ module Gitlab 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.set(:request_uri, env['REQUEST_URI']) + trans.set(:request_method, env['REQUEST_METHOD']) trans end def tag_controller(trans, env) - controller = env[CONTROLLER_KEY] - label = "#{controller.class.name}##{controller.action_name}" - - trans.add_tag(:action, label) + controller = env[CONTROLLER_KEY] + trans.action = "#{controller.class.name}##{controller.action_name}" end end end diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/sampler.rb index 828ee1f8c62..1ea425bc904 100644 --- a/lib/gitlab/metrics/sampler.rb +++ b/lib/gitlab/metrics/sampler.rb @@ -46,16 +46,15 @@ module Gitlab end def flush - MetricsWorker.perform_async(@metrics.map(&:to_hash)) + Metrics.submit_metrics(@metrics.map(&:to_hash)) end def sample_memory_usage - @metrics << Metric.new('memory_usage', value: System.memory_usage) + add_metric('memory_usage', value: System.memory_usage) end def sample_file_descriptors - @metrics << Metric. - new('file_descriptors', value: System.file_descriptor_count) + add_metric('file_descriptors', value: System.file_descriptor_count) end if Metrics.mri? @@ -69,7 +68,7 @@ module Gitlab counts['Symbol'] = Symbol.all_symbols.length counts.each do |name, count| - @metrics << Metric.new('object_counts', { count: count }, type: name) + add_metric('object_counts', { count: count }, type: name) end end else @@ -91,7 +90,17 @@ module Gitlab stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count] - @metrics << Metric.new('gc_statistics', stats) + 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 diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb index ec10707d1fb..fd98aa3412e 100644 --- a/lib/gitlab/metrics/sidekiq_middleware.rb +++ b/lib/gitlab/metrics/sidekiq_middleware.rb @@ -5,26 +5,14 @@ module Gitlab # This middleware is intended to be used as a server-side middleware. class SidekiqMiddleware def call(worker, message, queue) - # We don't want to track the MetricsWorker itself as otherwise we'll end - # up in an infinite loop. - if worker.class == MetricsWorker - yield - return - end - - trans = Transaction.new + trans = Transaction.new("#{worker.class.name}#perform") 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 index 7e0dcf99d92..7c0105d543a 100644 --- a/lib/gitlab/metrics/subscribers/action_view.rb +++ b/lib/gitlab/metrics/subscribers/action_view.rb @@ -19,6 +19,7 @@ module Gitlab values = values_for(event) tags = tags_for(event) + current_transaction.increment(:view_duration, event.duration) current_transaction.add_metric(SERIES, values, tags) end diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index d947c128ce2..8008b3bc895 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -1,44 +1,18 @@ module Gitlab module Metrics module Subscribers - # Class for tracking raw SQL queries. - # - # Queries are obfuscated before being logged to ensure no private data is - # exposed via InfluxDB/Grafana. + # Class for tracking the total query duration of a transaction. class ActiveRecord < ActiveSupport::Subscriber attach_to :active_record - SERIES = 'sql_queries' - def sql(event) return unless current_transaction - values = values_for(event) - tags = tags_for(event) - - current_transaction.add_metric(SERIES, values, tags) + current_transaction.increment(:sql_duration, event.duration) end private - def values_for(event) - { duration: event.duration } - end - - def tags_for(event) - sql = ObfuscatedSQL.new(event.payload[:sql]).to_s - tags = { sql: sql } - - 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 diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index 568f9d6ae0c..86606b1c6d6 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -4,23 +4,25 @@ module Gitlab class Transaction THREAD_KEY = :_gitlab_metrics_transaction - SERIES = 'transactions' + attr_reader :tags, :values - attr_reader :uuid, :tags + attr_accessor :action def self.current Thread.current[THREAD_KEY] end - # name - The name of this transaction as a String. - def initialize + # action - A String describing the action performed, usually the class + # plus method name. + def initialize(action = nil) @metrics = [] - @uuid = SecureRandom.uuid @started_at = nil @finished_at = nil - @tags = {} + @values = Hash.new(0) + @tags = {} + @action = action end def duration @@ -40,9 +42,17 @@ module Gitlab end def add_metric(series, values, tags = {}) - tags = tags.merge(transaction_id: @uuid) + prefix = sidekiq? ? 'sidekiq_' : 'rails_' - @metrics << Metric.new(series, values, tags) + @metrics << Metric.new("#{prefix}#{series}", values, tags) + end + + def increment(name, value) + @values[name] += value + end + + def set(name, value) + @values[name] = value end def add_tag(key, value) @@ -55,11 +65,29 @@ module Gitlab end def track_self - add_metric(SERIES, { duration: duration }, @tags) + values = { duration: duration } + + @values.each do |name, value| + values[name] = value + end + + add_metric('transactions', values, @tags) end def submit - MetricsWorker.perform_async(@metrics.map(&:to_hash)) + metrics = @metrics.map do |metric| + hash = metric.to_hash + + hash[:tags][:action] ||= @action if @action + + hash + end + + Metrics.submit_metrics(metrics) + end + + def sidekiq? + Sidekiq.server? 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 be795649e59..4164e998dd1 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -19,7 +19,7 @@ module Gitlab super(text, context.merge(project: project)) end - %i(user label merge_request snippet commit commit_range).each do |type| + %i(user label milestone merge_request snippet commit commit_range).each do |type| define_method("#{type}s") do @references[type] ||= references(type, reference_context) end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 0469c5a61c3..2dc2953e328 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -431,7 +431,7 @@ namespace :gitlab do try_fixing_it( "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}", "sudo chmod -R ug-s #{repo_base_path}", - "find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s" + "sudo find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s" ) for_more_information( see_installation_guide_section "GitLab Shell" diff --git a/lib/version_check.rb b/lib/version_check.rb index ea23344948c..91ad07feee5 100644 --- a/lib/version_check.rb +++ b/lib/version_check.rb @@ -13,6 +13,6 @@ class VersionCheck end def host - 'https://version.gitlab.com/check.png' + 'https://version.gitlab.com/check.svg' 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/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb new file mode 100644 index 00000000000..038dfeb8466 --- /dev/null +++ b/spec/controllers/projects/find_file_controller_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Projects::FindFileController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + + project.team << [user, :master] + controller.instance_variable_set(:@project, project) + end + + describe "GET #show" do + # Make sure any errors accessing the tree in our views bubble up to this spec + render_views + + before do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id) + end + + context "valid branch" do + let(:id) { 'master' } + it { is_expected.to respond_with(:success) } + end + + context "invalid branch" do + let(:id) { 'invalid-branch' } + it { is_expected.to respond_with(:not_found) } + end + end + + describe "GET #list" do + def go(format: 'json') + get :list, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id, + format: format + end + + context "valid branch" do + let(:id) { 'master' } + it 'returns an array of file path list' do + go + + json = JSON.parse(response.body) + is_expected.to respond_with(:success) + expect(json).not_to eq(nil) + expect(json.length).to be >= 0 + end + end + + context "invalid branch" do + let(:id) { 'invalid-branch' } + + it 'responds with status 404' do + go + is_expected.to respond_with(:not_found) + end + end + end +end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 5b4d7f41bc4..0c6a881f868 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -2,25 +2,28 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime +# updated_at :datetime +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer +# description :text +# position :integer default(0) +# locked_at :datetime +# updated_by_id :integer +# merge_error :string(255) +# merge_params :text +# merge_when_build_succeeds :boolean default(FALSE), not null +# merge_user_id :integer # FactoryGirl.define do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 112213377ff..c14b99606ba 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -29,6 +29,13 @@ # import_source :string(255) # commit_count :integer default(0) # import_error :text +# ci_id :integer +# builds_enabled :boolean default(TRUE), not null +# shared_runners_enabled :boolean default(TRUE), not null +# runners_token :string +# build_coverage_regex :string +# build_allow_git_fetch :boolean default(TRUE), not null +# build_timeout :integer default(3600), not null # FactoryGirl.define do 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/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb index e6e73e5e67c..30e29d9d552 100644 --- a/spec/features/ci_lint_spec.rb +++ b/spec/features/ci_lint_spec.rb @@ -35,5 +35,13 @@ describe 'CI Lint' do expect(page).to have_content('Error: Please provide content of .gitlab-ci.yml') end end + + describe 'YAML revalidate' do + let(:yaml_content) { 'my yaml content' } + + it 'loads previous YAML content after validation' do + expect(page).to have_field('content', with: 'my yaml content', type: 'textarea') + end + end end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index a2fb3e4c75d..e844e681ebf 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -127,15 +127,15 @@ describe 'Issues', feature: true do it 'sorts by newest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created) - expect(first_issue).to include('foo') - expect(last_issue).to include('baz') + expect(first_issue).to include('baz') + expect(last_issue).to include('foo') end it 'sorts by oldest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created) - expect(first_issue).to include('baz') - expect(last_issue).to include('foo') + expect(first_issue).to include('foo') + expect(last_issue).to include('baz') end it 'sorts by most recently updated' do @@ -190,8 +190,8 @@ describe 'Issues', feature: true do sort: sort_value_oldest_created, assignee_id: user2.id) - expect(first_issue).to include('bar') - expect(last_issue).to include('foo') + expect(first_issue).to include('foo') + expect(last_issue).to include('bar') expect(page).not_to have_content 'baz' end end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index fdd8cf07b12..e836d81c40b 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -212,6 +212,7 @@ describe 'GitLab Markdown', feature: true do expect(doc).to reference_commit_ranges expect(doc).to reference_commits expect(doc).to reference_labels + expect(doc).to reference_milestones end end 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/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index e8dfc5c0eb1..0620096d689 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -214,6 +214,13 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) - Link to label by reference: [Label](<%= label.to_reference %>) +#### MilestoneReferenceFilter + +- Milestone: <%= milestone.to_reference %> +- Milestone in another project: <%= xmilestone.to_reference(project) %> +- Ignored in code: `<%= milestone.to_reference %>` +- Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>) + ### Task Lists - [ ] Incomplete task 1 diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 68527c3a4f8..efc850eb705 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -240,7 +240,7 @@ describe ApplicationHelper do describe 'time_ago_with_tooltip' do def element(*arguments) Time.zone = 'UTC' - time = Time.zone.parse('2015-07-02 08:00') + time = Time.zone.parse('2015-07-02 08:23') element = helper.time_ago_with_tooltip(time, *arguments) Nokogiri::HTML::DocumentFragment.parse(element).first_element_child @@ -251,15 +251,15 @@ describe ApplicationHelper do end it 'includes the date string' do - expect(element.text).to eq '2015-07-02 08:00:00 UTC' + expect(element.text).to eq '2015-07-02 08:23:00 UTC' end it 'has a datetime attribute' do - expect(element.attr('datetime')).to eq '2015-07-02T08:00:00Z' + expect(element.attr('datetime')).to eq '2015-07-02T08:23:00Z' end it 'has a formatted title attribute' do - expect(element.attr('title')).to eq 'Jul 02, 2015 8:00am' + expect(element.attr('title')).to eq 'Jul 2, 2015 8:23am' end it 'includes a default js-timeago class' do diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index fd7107779f6..cf632f594c7 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -2,10 +2,8 @@ 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' + it 'defaults to nil' do + expect(helper.page_description).to eq nil end it 'returns the last-pushed description' do @@ -42,58 +40,32 @@ describe PageLayoutHelper do 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) + %w(project user group).each do |type| + context "with @#{type} assigned" do + it "uses #{type.titlecase} avatar if available" do + object = double(avatar_url: 'http://example.com/uploads/avatar.png') + assign(type, object) - expect(helper.page_image).to eq project.avatar_url - end + expect(helper.page_image).to eq object.avatar_url + end - it 'falls back to the default' do - project = double(avatar_url: nil) - helper.instance_variable_set(:@project, project) + it 'falls back to the default when avatar_url is nil' do + object = double(avatar_url: nil) + assign(type, object) - expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + end 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 + context "with no assignments" do + it 'falls back to the default' do + expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + 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/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml index e867e4de2b9..1701652c61e 100644 --- a/spec/javascripts/fixtures/zen_mode.html.haml +++ b/spec/javascripts/fixtures/zen_mode.html.haml @@ -1,9 +1,8 @@ .zennable - %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } .zen-backdrop - %textarea#note_note.js-gfm-input.markdown-area{placeholder: 'Leave a comment'} - %a.zen-enter-link{tabindex: '-1'} + %textarea#note_note.js-gfm-input.markdown-area + %a.js-zen-enter(tabindex="-1" href="#") %i.fa.fa-expand - Edit in fullscreen - %a.zen-leave-link + Edit in fullscreen + %a.js-zen-leave(tabindex="-1" href="#") %i.fa.fa-compress diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index e1c22860da7..86ba9dd8e96 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -26,10 +26,10 @@ describe 'reopen/close issue', -> 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 + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://gitlab.com/issues/6/close') + req.success saved: true $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') @@ -46,10 +46,10 @@ describe 'reopen/close issue', -> it 'fails to close an issue with success:false', -> - $.ajax = (obj) -> - expect(obj.type).toBe('PUT') - expect(obj.url).toBe('http://goesnowhere.nothing/whereami') - obj.success saved: false + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://goesnowhere.nothing/whereami') + req.success saved: false $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') @@ -69,10 +69,10 @@ describe 'reopen/close issue', -> 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() + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://goesnowhere.nothing/whereami') + req.error() $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') @@ -91,10 +91,10 @@ describe 'reopen/close issue', -> 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 + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://gitlab.com/issues/6/reopen') + req.success saved: true $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee index 4cb3836755f..b790fce01ed 100644 --- a/spec/javascripts/zen_mode_spec.js.coffee +++ b/spec/javascripts/zen_mode_spec.js.coffee @@ -15,14 +15,6 @@ describe 'ZenMode', -> # Set this manually because we can't actually scroll the window @zen.scroll_position = 456 - # Ohmmmmmmm - enterZen = -> - $('.zen-toggle-comment').prop('checked', true).trigger('change') - - # Wh- what was that?! - exitZen = -> - $('.zen-toggle-comment').prop('checked', false).trigger('change') - describe 'on enter', -> it 'pauses Mousetrap', -> spyOn(Mousetrap, 'pause') @@ -35,16 +27,14 @@ describe 'ZenMode', -> expect('textarea').not.toHaveAttr('style') describe 'in use', -> - beforeEach -> - enterZen() + beforeEach -> enterZen() it 'exits on Escape', -> - $(document).trigger(jQuery.Event('keydown', {keyCode: 27})) - expect($('.zen-toggle-comment').prop('checked')).toBe(false) + escapeKeydown() + expect($('.zen-backdrop')).not.toHaveClass('fullscreen') describe 'on exit', -> - beforeEach -> - enterZen() + beforeEach -> enterZen() it 'unpauses Mousetrap', -> spyOn(Mousetrap, 'unpause') @@ -52,6 +42,10 @@ describe 'ZenMode', -> expect(Mousetrap.unpause).toHaveBeenCalled() it 'restores the scroll position', -> - spyOn(@zen, 'restoreScroll') + spyOn(@zen, 'scrollTo') exitZen() - expect(@zen.restoreScroll).toHaveBeenCalledWith(456) + expect(@zen.scrollTo).toHaveBeenCalled() + +enterZen = -> $('a.js-zen-enter').click() # Ohmmmmmmm +exitZen = -> $('a.js-zen-leave').click() +escapeKeydown = -> $('textarea').trigger($.Event('keydown', {keyCode: 27})) diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb new file mode 100644 index 00000000000..ebf3d7489b5 --- /dev/null +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Banzai::Filter::MilestoneReferenceFilter, lib: true do + include FilterSpecHelper + + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>milestone #{milestone.to_reference}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + # Convert the Markdown link to only the URL, since these tests aren't run through the regular Markdown pipeline. + # Milestone reference behavior in the full Markdown pipeline is tested elsewhere. + let(:reference) { milestone.to_reference.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '\2') } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + end + + it 'links with adjacent text' do + doc = reference_filter("milestone (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(milestone.title)}<\/a>\.\)/) + end + + it 'includes a title attribute' do + doc = reference_filter("milestone #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Milestone: #{milestone.title}" + end + + it 'escapes the title attribute' do + milestone.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = reference_filter("milestone #{reference}") + expect(doc.text).to eq "milestone #{milestone.title}" + end + + it 'includes default classes' do + doc = reference_filter("milestone #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + end + + it 'includes a data-project attribute' do + doc = reference_filter("milestone #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-milestone attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-milestone') + expect(link.attr('data-milestone')).to eq milestone.id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("milestone #{reference}") + expect(result[:references][:milestone]).to eq [milestone] + end + end +end 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/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb index f2e3a44478d..569cbc885c7 100644 --- a/spec/lib/banzai/filter/task_list_filter_spec.rb +++ b/spec/lib/banzai/filter/task_list_filter_spec.rb @@ -7,4 +7,10 @@ describe Banzai::Filter::TaskListFilter, lib: true do exp = act = %(<ul><li>Item</li></ul>) expect(filter(act).to_html).to eq exp end + + it 'applies `task-list` to single-item task lists' do + act = filter('<ul><li>[ ] Task 1</li></ul>') + + expect(act.to_html).to start_with '<ul class="task-list">' + 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/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb index 839b30f1ff4..38be9448794 100644 --- a/spec/lib/gitlab/build_data_builder_spec.rb +++ b/spec/lib/gitlab/build_data_builder_spec.rb @@ -14,6 +14,7 @@ describe 'Gitlab::BuildDataBuilder' do it { expect(data[:tag]).to eq(build.tag) } it { expect(data[:build_id]).to eq(build.id) } it { expect(data[:build_status]).to eq(build.status) } + it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } end diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index b535413bbd4..abe179cd4af 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -42,7 +42,7 @@ describe Gitlab::Email::Receiver, lib: true do context "when the email was auto generated" do let!(:reply_key) { '636ca428858779856c226bb145ef4fad' } let!(:email_raw) { fixture_file("emails/auto_reply.eml") } - + it "raises an AutoGeneratedEmailError" do expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError) end @@ -90,7 +90,7 @@ describe Gitlab::Email::Receiver, lib: true do context "when the reply is blank" do let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } - + it "raises an EmptyEmailError" do expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError) end @@ -107,13 +107,16 @@ describe Gitlab::Email::Receiver, lib: true do end context "when everything is fine" do + let(:markdown) { "![image](uploads/image.png)" } + before do allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( [ { url: "uploads/image.png", is_image: true, - alt: "image" + alt: "image", + markdown: markdown } ] ) @@ -132,7 +135,7 @@ describe Gitlab::Email::Receiver, lib: true do note = noteable.notes.last - expect(note.note).to include("![image](uploads/image.png)") + expect(note.note).to include(markdown) end end end diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb new file mode 100644 index 00000000000..a324a82e69f --- /dev/null +++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::CommentFormatter, lib: true do + let(:project) { create(:project) } + let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') } + let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') } + let(:base_data) do + { + body: "I'm having a problem with this.", + user: octocat, + created_at: created_at, + updated_at: updated_at + } + end + + subject(:comment) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when do not reference a portion of the diff' do + let(:raw_data) { OpenStruct.new(base_data) } + + it 'returns formatted attributes' do + expected = { + project: project, + note: "*Created by: octocat*\n\nI'm having a problem with this.", + commit_id: nil, + line_code: nil, + author_id: project.creator_id, + created_at: created_at, + updated_at: updated_at + } + + expect(comment.attributes).to eq(expected) + end + end + + context 'when on a portion of the diff' do + let(:diff_data) do + { + body: 'Great stuff', + commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', + diff_hunk: '@@ -16,33 +16,40 @@ public class Connection : IConnection...', + path: 'file1.txt', + position: 1 + } + end + + let(:raw_data) { OpenStruct.new(base_data.merge(diff_data)) } + + it 'returns formatted attributes' do + expected = { + project: project, + note: "*Created by: octocat*\n\nGreat stuff", + commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', + line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_0_1', + author_id: project.creator_id, + created_at: created_at, + updated_at: updated_at + } + + expect(comment.attributes).to eq(expected) + end + end + + context 'when author is a GitLab user' do + let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + + it 'returns project#creator_id as author_id when is not a GitLab user' do + expect(comment.attributes.fetch(:author_id)).to eq project.creator_id + end + + it 'returns GitLab user id as author_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(comment.attributes.fetch(:author_id)).to eq gl_user.id + end + end + end +end diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb new file mode 100644 index 00000000000..fd05428b322 --- /dev/null +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -0,0 +1,139 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::IssueFormatter, lib: true do + let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) } + let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + + let(:base_data) do + { + number: 1347, + state: 'open', + title: 'Found a bug', + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil + } + end + + subject(:issue) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when issue is open' do + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) } + + it 'returns formatted attributes' do + expected = { + project: project, + title: 'Found a bug', + description: "*Created by: octocat*\n\nI'm having a problem with this.", + state: 'opened', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: updated_at + } + + expect(issue.attributes).to eq(expected) + end + end + + context 'when issue is closed' do + let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) } + + it 'returns formatted attributes' do + expected = { + project: project, + title: 'Found a bug', + description: "*Created by: octocat*\n\nI'm having a problem with this.", + state: 'closed', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: closed_at + } + + expect(issue.attributes).to eq(expected) + end + end + + context 'when it is assigned to someone' do + let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) } + + it 'returns nil as assignee_id when is not a GitLab user' do + expect(issue.attributes.fetch(:assignee_id)).to be_nil + end + + it 'returns GitLab user id as assignee_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id + end + end + + context 'when author is a GitLab user' do + let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + + it 'returns project#creator_id as author_id when is not a GitLab user' do + expect(issue.attributes.fetch(:author_id)).to eq project.creator_id + end + + it 'returns GitLab user id as author_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(issue.attributes.fetch(:author_id)).to eq gl_user.id + end + end + end + + describe '#has_comments?' do + context 'when number of comments is greater than zero' do + let(:raw_data) { OpenStruct.new(base_data.merge(comments: 1)) } + + it 'returns true' do + expect(issue.has_comments?).to eq true + end + end + + context 'when number of comments is equal to zero' do + let(:raw_data) { OpenStruct.new(base_data.merge(comments: 0)) } + + it 'returns false' do + expect(issue.has_comments?).to eq false + end + end + end + + describe '#number' do + let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } + + it 'returns pull request number' do + expect(issue.number).to eq 1347 + end + end + + describe '#valid?' do + context 'when mention a pull request' do + let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: OpenStruct.new)) } + + it 'returns false' do + expect(issue.valid?).to eq false + end + end + + context 'when does not mention a pull request' do + let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: nil)) } + + it 'returns true' do + expect(issue.valid?).to eq true + end + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb new file mode 100644 index 00000000000..9aefec77f6d --- /dev/null +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -0,0 +1,184 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::PullRequestFormatter, lib: true do + let(:project) { create(:project) } + let(:source_branch) { OpenStruct.new(ref: 'feature') } + let(:target_branch) { OpenStruct.new(ref: 'master') } + let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:base_data) do + { + number: 1347, + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil + } + end + + subject(:pull_request) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when pull request is open' do + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) } + + it 'returns formatted attributes' do + expected = { + title: 'New feature', + description: "*Created by: octocat*\n\nPlease pull these awesome changes", + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + state: 'opened', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: updated_at + } + + expect(pull_request.attributes).to eq(expected) + end + end + + context 'when pull request is closed' do + let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) } + + it 'returns formatted attributes' do + expected = { + title: 'New feature', + description: "*Created by: octocat*\n\nPlease pull these awesome changes", + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + state: 'closed', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: closed_at + } + + expect(pull_request.attributes).to eq(expected) + end + end + + context 'when pull request is merged' do + let(:merged_at) { DateTime.strptime('2011-01-28T13:01:12Z') } + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', merged_at: merged_at)) } + + it 'returns formatted attributes' do + expected = { + title: 'New feature', + description: "*Created by: octocat*\n\nPlease pull these awesome changes", + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + state: 'merged', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: merged_at + } + + expect(pull_request.attributes).to eq(expected) + end + end + + context 'when it is assigned to someone' do + let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) } + + it 'returns nil as assignee_id when is not a GitLab user' do + expect(pull_request.attributes.fetch(:assignee_id)).to be_nil + end + + it 'returns GitLab user id as assignee_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id + end + end + + context 'when author is a GitLab user' do + let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + + it 'returns project#creator_id as author_id when is not a GitLab user' do + expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id + end + + it 'returns GitLab user id as author_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id + end + end + end + + describe '#cross_project?' do + context 'when source repo is not a fork' do + let(:local_repo) { OpenStruct.new(fork: false) } + let(:source_branch) { OpenStruct.new(ref: 'feature', repo: local_repo) } + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch)) } + + it 'returns false' do + expect(pull_request.cross_project?).to eq false + end + end + + context 'when source repo is a fork' do + let(:forked_repo) { OpenStruct.new(fork: true) } + let(:source_branch) { OpenStruct.new(ref: 'feature', repo: forked_repo) } + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch)) } + + it 'returns true' do + expect(pull_request.cross_project?).to eq true + end + end + end + + describe '#number' do + let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } + + it 'returns pull request number' do + expect(pull_request.number).to eq 1347 + end + end + + describe '#valid?' do + let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') } + + context 'when source and target branches exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) } + + it 'returns true' do + expect(pull_request.valid?).to eq true + end + end + + context 'when source branch doesn not exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end + end + + context 'when target branch doesn not exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end + end + end +end diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index a7eab9d11cc..2a37cd40dde 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -48,6 +48,9 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) + expect(transaction).to receive(:increment). + with(:method_duration, a_kind_of(Numeric)) + expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy.foo') @@ -102,6 +105,9 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) + expect(transaction).to receive(:increment). + with(:method_duration, a_kind_of(Numeric)) + expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy#bar') diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb index ec39bc9cce8..f718d536130 100644 --- a/spec/lib/gitlab/metrics/metric_spec.rb +++ b/spec/lib/gitlab/metrics/metric_spec.rb @@ -37,12 +37,6 @@ describe Gitlab::Metrics::Metric do it 'includes the tags' do expect(hash[:tags]).to be_an_instance_of(Hash) - - expect(hash[:tags][:hostname]).to be_an_instance_of(String) - expect(hash[:tags][:ruby_engine]).to be_an_instance_of(String) - expect(hash[:tags][:ruby_version]).to be_an_instance_of(String) - expect(hash[:tags][:gitlab_version]).to be_an_instance_of(String) - expect(hash[:tags][:process_type]).to be_an_instance_of(String) end it 'includes the values' do diff --git a/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb b/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb deleted file mode 100644 index 0f01ee588c9..00000000000 --- a/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Metrics::ObfuscatedSQL do - describe '#to_s' do - describe 'using single values' do - it 'replaces a single integer' do - sql = described_class.new('SELECT x FROM y WHERE a = 10') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - - it 'replaces a single float' do - sql = described_class.new('SELECT x FROM y WHERE a = 10.5') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - - it 'replaces a single quoted string' do - sql = described_class.new("SELECT x FROM y WHERE a = 'foo'") - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - - if Gitlab::Database.mysql? - it 'replaces a double quoted string' do - sql = described_class.new('SELECT x FROM y WHERE a = "foo"') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - end - - it 'replaces a single regular expression' do - sql = described_class.new('SELECT x FROM y WHERE a = /foo/') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - - it 'replaces regular expressions using escaped slashes' do - sql = described_class.new('SELECT x FROM y WHERE a = /foo\/bar/') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - end - - describe 'using consecutive values' do - it 'replaces multiple integers' do - sql = described_class.new('SELECT x FROM y WHERE z IN (10, 20, 30)') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)') - end - - it 'replaces multiple floats' do - sql = described_class.new('SELECT x FROM y WHERE z IN (1.5, 2.5, 3.5)') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)') - end - - it 'replaces multiple single quoted strings' do - sql = described_class.new("SELECT x FROM y WHERE z IN ('foo', 'bar')") - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)') - end - - if Gitlab::Database.mysql? - it 'replaces multiple double quoted strings' do - sql = described_class.new('SELECT x FROM y WHERE z IN ("foo", "bar")') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)') - end - end - - it 'replaces multiple regular expressions' do - sql = described_class.new('SELECT x FROM y WHERE z IN (/foo/, /bar/)') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)') - end - end - - if Gitlab::Database.postgresql? - it 'replaces double quotes' do - sql = described_class.new('SELECT "x" FROM "y"') - - expect(sql.to_s).to eq('SELECT x FROM y') - end - end - end -end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index a143fe4cfcd..b99be4e1060 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -40,9 +40,9 @@ describe Gitlab::Metrics::RackMiddleware 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') + it 'stores the request method and URI in the transaction as values' do + expect(transaction.values[:request_method]).to eq('GET') + expect(transaction.values[:request_uri]).to eq('/foo') end end @@ -57,7 +57,7 @@ describe Gitlab::Metrics::RackMiddleware do middleware.tag_controller(transaction, env) - expect(transaction.tags[:action]).to eq('TestController#show') + expect(transaction.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 index 69376c0b79b..27211350fbe 100644 --- a/spec/lib/gitlab/metrics/sampler_spec.rb +++ b/spec/lib/gitlab/metrics/sampler_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Metrics::Sampler do describe '#flush' do it 'schedules the metrics using Sidekiq' do - expect(MetricsWorker).to receive(:perform_async). + expect(Gitlab::Metrics).to receive(:submit_metrics). with([an_instance_of(Hash)]) sampler.sample_memory_usage @@ -51,8 +51,8 @@ describe Gitlab::Metrics::Sampler do expect(Gitlab::Metrics::System).to receive(:memory_usage). and_return(9000) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('memory_usage', value: 9000). + expect(sampler).to receive(:add_metric). + with(/memory_usage/, value: 9000). and_call_original sampler.sample_memory_usage @@ -64,8 +64,8 @@ describe Gitlab::Metrics::Sampler do expect(Gitlab::Metrics::System).to receive(:file_descriptor_count). and_return(4) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('file_descriptors', value: 4). + expect(sampler).to receive(:add_metric). + with(/file_descriptors/, value: 4). and_call_original sampler.sample_file_descriptors @@ -74,8 +74,8 @@ describe Gitlab::Metrics::Sampler do describe '#sample_objects' do it 'adds a metric containing the amount of allocated objects' do - expect(Gitlab::Metrics::Metric).to receive(:new). - with('object_counts', an_instance_of(Hash), an_instance_of(Hash)). + expect(sampler).to receive(:add_metric). + with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)). at_least(:once). and_call_original @@ -87,11 +87,33 @@ describe Gitlab::Metrics::Sampler do it 'adds a metric containing garbage collection statistics' do expect(GC::Profiler).to receive(:total_time).and_return(0.24) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('gc_statistics', an_instance_of(Hash)). + 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 index 05214efc565..e520a968999 100644 --- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb @@ -5,30 +5,15 @@ describe Gitlab::Metrics::SidekiqMiddleware do 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 + worker = double(:worker, class: double(:class, name: 'TestWorker')) - it 'does not track jobs of the MetricsWorker' do - worker = MetricsWorker.new + expect(Gitlab::Metrics::Transaction).to receive(:new). + with('TestWorker#perform'). + and_call_original - expect(Gitlab::Metrics::Transaction).to_not receive(: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 index c6cd584663f..05e4fbbeb51 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -28,6 +28,9 @@ describe Gitlab::Metrics::Subscribers::ActionView do line: 4 } + expect(transaction).to receive(:increment). + with(:view_duration, 2.1) + expect(transaction).to receive(:add_metric). with(described_class::SERIES, values, tags) diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 05b6cc14716..7bc070a4d09 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -2,31 +2,34 @@ require 'spec_helper' describe Gitlab::Metrics::Subscribers::ActiveRecord do let(:transaction) { Gitlab::Metrics::Transaction.new } - - let(:subscriber) { described_class.new } + let(:subscriber) { described_class.new } let(:event) do double(:event, duration: 0.2, payload: { sql: 'SELECT * FROM users WHERE id = 10' }) end - before do - allow(subscriber).to receive(:current_transaction).and_return(transaction) + describe '#sql' do + describe 'without a current transaction' do + it 'simply returns' do + expect_any_instance_of(Gitlab::Metrics::Transaction). + to_not receive(:increment) - allow(Gitlab::Metrics).to receive(:last_relative_application_frame). - and_return(['app/models/foo.rb', 4]) - end + subscriber.sql(event) + end + end - describe '#sql' do - it 'tracks the execution of a SQL query' do - sql = 'SELECT * FROM users WHERE id = ?' - values = { duration: 0.2 } - tags = { sql: sql, file: 'app/models/foo.rb', line: 4 } + 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(:add_metric). - with(described_class::SERIES, values, tags) + expect(transaction).to receive(:increment). + with(:sql_duration, 0.2) - subscriber.sql(event) + subscriber.sql(event) + end end end end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 5f17ff8ee75..6bdeb719491 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -30,14 +30,37 @@ describe Gitlab::Metrics::Transaction do end describe '#add_metric' do - it 'adds a metric tagged with the transaction UUID' do + it 'adds a metric to the transaction' do expect(Gitlab::Metrics::Metric).to receive(:new). - with('foo', { number: 10 }, { transaction_id: transaction.uuid }) + with('rails_foo', { number: 10 }, {}) 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 '#set' do + it 'sets a value' do + transaction.set(:number, 10) + + expect(transaction).to receive(:add_metric). + with('transactions', { duration: 0.0, number: 10 }, {}) + + transaction.track_self + end + end + describe '#add_tag' do it 'adds a tag' do transaction.add_tag(:foo, 'bar') @@ -58,7 +81,7 @@ describe Gitlab::Metrics::Transaction do describe '#track_self' do it 'adds a metric for the transaction itself' do expect(transaction).to receive(:add_metric). - with(described_class::SERIES, { duration: transaction.duration }, {}) + with('transactions', { duration: transaction.duration }, {}) transaction.track_self end @@ -68,10 +91,27 @@ describe Gitlab::Metrics::Transaction do it 'submits the metrics to Sidekiq' do transaction.track_self - expect(MetricsWorker).to receive(:perform_async). + expect(Gitlab::Metrics).to receive(:submit_metrics). with([an_instance_of(Hash)]) transaction.submit end + + it 'adds the action as a tag for every metric' do + transaction.action = 'Foo#bar' + transaction.track_self + + hash = { + series: 'rails_transactions', + tags: { action: 'Foo#bar' }, + values: { duration: 0.0 }, + timestamp: an_instance_of(Fixnum) + } + + expect(Gitlab::Metrics).to receive(:submit_metrics). + with([hash]) + + transaction.submit + end end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 944642909aa..c2782f95c8e 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -1,15 +1,9 @@ require 'spec_helper' describe Gitlab::Metrics do - describe '.pool_size' do - it 'returns a Fixnum' do - expect(described_class.pool_size).to be_an_instance_of(Fixnum) - end - end - - describe '.timeout' do - it 'returns a Fixnum' do - expect(described_class.timeout).to be_an_instance_of(Fixnum) + describe '.settings' do + it 'returns a Hash' do + expect(described_class.settings).to be_an_instance_of(Hash) end end @@ -19,12 +13,6 @@ describe Gitlab::Metrics do end end - describe '.hostname' do - it 'returns a String containing the hostname' do - expect(described_class.hostname).to eq(Socket.gethostname) - 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 @@ -33,4 +21,52 @@ describe Gitlab::Metrics do 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/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 35d8220ae54..91b250265e6 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -2,32 +2,45 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string(255) -# session_expire_delay :integer default(10080), not null -# import_sources :text -# help_page_text :text -# admin_notification_email :string(255) -# shared_runners_enabled :boolean default(TRUE), not null -# max_artifacts_size :integer default(100), not null -# runners_registration_token :string(255) +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) +# restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) +# max_attachment_size :integer default(10), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null +# import_sources :text +# help_page_text :text +# admin_notification_email :string(255) +# shared_runners_enabled :boolean default(TRUE), not null +# max_artifacts_size :integer default(100), not null +# runners_registration_token :string +# require_two_factor_authentication :boolean default(FALSE) +# two_factor_grace_period :integer default(48) +# metrics_enabled :boolean default(FALSE) +# metrics_host :string default("localhost") +# metrics_username :string +# metrics_password :string +# metrics_pool_size :integer default(16) +# metrics_timeout :integer default(10) +# metrics_method_call_threshold :integer default(10) +# recaptcha_enabled :boolean default(FALSE) +# recaptcha_site_key :string +# recaptcha_private_key :string +# metrics_port :integer default(8089) # require 'spec_helper' diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb new file mode 100644 index 00000000000..36d10636ae9 --- /dev/null +++ b/spec/models/ci/build_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Ci::Build, models: true do + let(:build) { create(:ci_build) } + let(:test_trace) { 'This is a test' } + + describe '#trace' do + it 'obfuscates project runners token' do + allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}") + + expect(build.trace).to eq("Test: xxxxxx") + end + + it 'empty project runners token' do + allow(build).to receive(:raw_trace).and_return(test_trace) + # runners_token can't normally be set to nil + allow(build.project).to receive(:runners_token).and_return(nil) + + expect(build.trace).to eq(test_trace) + end + end +end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index b193e16e7f8..dfc0cc3be1c 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -13,7 +13,7 @@ # tag :boolean default(FALSE) # yaml_errors :text # committed_at :datetime -# project_id :integer +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb index da8491357a5..000a732db77 100644 --- a/spec/models/ci/runner_project_spec.rb +++ b/spec/models/ci/runner_project_spec.rb @@ -2,11 +2,12 @@ # # Table name: ci_runner_projects # -# id :integer not null, primary key -# runner_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# runner_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index cb2f51e2011..159be939300 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -2,12 +2,13 @@ # # Table name: ci_triggers # -# id :integer not null, primary key -# token :string(255) -# project_id :integer not null -# deleted_at :datetime -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# token :string(255) +# project_id :integer +# deleted_at :datetime +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 31b56953a13..71e84091cb7 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -3,12 +3,13 @@ # Table name: ci_variables # # id :integer not null, primary key -# project_id :integer not null +# project_id :integer # key :string(255) # value :text # encrypted_value :text # encrypted_value_salt :string(255) # encrypted_value_iv :string(255) +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index b8f901b3433..82c68ff6cb1 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index f9d3c56750f..021d62cdf0c 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -99,4 +99,18 @@ describe Issue, "Issuable" do to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' }) end end + + describe "votes" do + before do + author = create :user + project = create :empty_project + issue.notes.awards.create!(note: "thumbsup", author: author, project: project) + issue.notes.awards.create!(note: "thumbsdown", author: author, project: project) + end + + it "returns correct values" do + expect(issue.upvotes).to eq(1) + expect(issue.downvotes).to eq(1) + end + end end diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb index b198aa77526..d37978720bf 100644 --- a/spec/models/external_wiki_service_spec.rb +++ b/spec/models/external_wiki_service_spec.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require 'spec_helper' diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index d61c1c96bde..5b0883d8702 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 646f767e6fe..3c995053eec 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # require 'spec_helper' @@ -38,14 +37,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 +103,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/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 2d90b0793cc..7070aa4ac62 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -77,5 +77,17 @@ describe ProjectHook, models: true do expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error']) end + + it "handles 200 status code" do + WebMock.stub_request(:post, @project_hook.url).to_return(status: 200, body: "Success") + + expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + end + + it "handles 2xx status codes" do + WebMock.stub_request(:post, @project_hook.url).to_return(status: 201, body: "Success") + + expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index e0653a8327d..291e6200a5b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2,25 +2,28 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime +# updated_at :datetime +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer +# description :text +# position :integer default(0) +# locked_at :datetime +# updated_by_id :integer +# merge_error :string(255) +# merge_params :text +# merge_when_build_succeeds :boolean default(FALSE), not null +# merge_user_id :integer # require 'spec_helper' diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 4fa2d2bc4d2..e0b3290e416 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # require 'spec_helper' diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 593d8f76215..151a29e974b 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -125,6 +125,19 @@ describe Note, models: true do let(:set_mentionable_text) { ->(txt) { subject.note = txt } } end + describe "#all_references" do + let!(:note1) { create(:note) } + let!(:note2) { create(:note) } + + it "reads the rendered note body from the cache" do + expect(Banzai::Renderer).to receive(:render).with(note1.note, pipeline: :note, cache_key: [note1, "note"], project: note1.project) + expect(Banzai::Renderer).to receive(:render).with(note2.note, pipeline: :note, cache_key: [note2, "note"], project: note2.project) + + note1.all_references + note2.all_references + end + end + describe :search do let!(:note) { create(:note, note: "WoW") } @@ -164,7 +177,7 @@ describe Note, models: true do expect(note.editable?).to be_falsy end end - + describe "set_award!" do let(:issue) { create :issue } diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index 64bb92fba95..f3d15f3c1ea 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb @@ -40,6 +40,20 @@ describe AsanaService, models: true do let(:user) { create(:user) } let(:project) { create(:project) } + def create_data_for_commits(*messages) + { + object_kind: 'push', + ref: 'master', + user_name: user.name, + commits: messages.map do |m| + { + message: m, + url: 'https://gitlab.com/', + } + end + } + end + before do @asana = AsanaService.new allow(@asana).to receive_messages( @@ -51,16 +65,67 @@ describe AsanaService, models: true do ) end - it 'should call Asana service to created a story' do - expect(Asana::Task).to receive(:find).with('123456').once + it 'should call Asana service to create a story' do + data = create_data_for_commits('Message from commit. related to #123456') + expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}" - @asana.check_commit('related to #123456', 'pushed') + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment).with(text: expected_message) + expect(Asana::Task).to receive(:find_by_id).with(anything, '123456').once.and_return(d1) + + @asana.execute(data) end - it 'should call Asana service to created a story and close a task' do - expect(Asana::Task).to receive(:find).with('456789').twice + it 'should call Asana service to create a story and close a task' do + data = create_data_for_commits('fix #456789') + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1) + + @asana.execute(data) + end + + it 'should be able to close via url' do + data = create_data_for_commits('closes https://app.asana.com/19292/956299/42') + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1) + + @asana.execute(data) + end + + it 'should allow multiple matches per line' do + message = <<-EOF + minor bigfix, refactoring, fixed #123 and Closes #456 work on #789 + ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12 + EOF + data = create_data_for_commits(message) + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1) + + d2 = double('Asana::Task') + expect(d2).to receive(:add_comment) + expect(d2).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2) + + d3 = double('Asana::Task') + expect(d3).to receive(:add_comment) + expect(Asana::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3) + + d4 = double('Asana::Task') + expect(d4).to receive(:add_comment) + expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4) + + d5 = double('Asana::Task') + expect(d5).to receive(:add_comment) + expect(d5).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5) - @asana.check_commit('fix #456789', 'pushed') + @asana.execute(data) end end end diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb new file mode 100644 index 00000000000..905379a64e3 --- /dev/null +++ b/spec/models/project_services/builds_email_service_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe BuildsEmailService do + let(:build) { create(:ci_build) } + let(:data) { Gitlab::BuildDataBuilder.build(build) } + let(:service) { BuildsEmailService.new } + + describe :execute do + it "sends email" do + service.recipients = 'test@gitlab.com' + data[:build_status] = 'failed' + expect(BuildEmailWorker).to receive(:perform_async) + service.execute(data) + end + + it "does not sends email with failed build and allowed_failure on" do + data[:build_status] = 'failed' + data[:build_allow_failure] = true + expect(BuildEmailWorker).not_to receive(:perform_async) + service.execute(data) + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 400bdf2d962..a3de23369e1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -29,6 +29,13 @@ # import_source :string(255) # commit_count :integer default(0) # import_error :text +# ci_id :integer +# builds_enabled :boolean default(TRUE), not null +# shared_runners_enabled :boolean default(TRUE), not null +# runners_token :string +# build_coverage_regex :string +# build_allow_git_fetch :boolean default(TRUE), not null +# build_timeout :integer default(3600), not null # require 'spec_helper' diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 0ca82365b98..173628c08d0 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require 'spec_helper' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2f184bbaf92..3cd63b2b0e8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,62 +2,63 @@ # # Table name: users # -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# bio :string(255) -# failed_attempts :integer default(0) -# locked_at :datetime -# unlock_token :string(255) -# username :string(255) -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# last_credential_check_at :datetime -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string(255) default(""), not null -# notification_email :string(255) -# hide_no_password :boolean default(FALSE) -# password_automatically_set :boolean default(FALSE) -# location :string(255) -# encrypted_otp_secret :string(255) -# encrypted_otp_secret_iv :string(255) -# encrypted_otp_secret_salt :string(255) -# otp_required_for_login :boolean default(FALSE), not null -# otp_backup_codes :text -# public_email :string(255) default(""), not null -# dashboard :integer default(0) -# project_view :integer default(0) -# consumed_timestep :integer -# layout :integer default(0) -# hide_project_limit :boolean default(FALSE) +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# name :string(255) +# admin :boolean default(FALSE), not null +# projects_limit :integer default(10) +# skype :string(255) default(""), not null +# linkedin :string(255) default(""), not null +# twitter :string(255) default(""), not null +# authentication_token :string(255) +# theme_id :integer default(1), not null +# bio :string(255) +# failed_attempts :integer default(0) +# locked_at :datetime +# username :string(255) +# can_create_group :boolean default(TRUE), not null +# can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer +# last_credential_check_at :datetime +# avatar :string(255) +# confirmation_token :string(255) +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string(255) +# hide_no_ssh_key :boolean default(FALSE) +# website_url :string(255) default(""), not null +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# location :string(255) +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean default(FALSE), not null +# otp_backup_codes :text +# public_email :string(255) default(""), not null +# dashboard :integer default(0) +# project_view :integer default(0) +# consumed_timestep :integer +# layout :integer default(0) +# hide_project_limit :boolean default(FALSE) +# unlock_token :string +# otp_grace_period_started_at :datetime # require 'spec_helper' @@ -106,7 +107,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..6f4c336b66c 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -353,6 +353,20 @@ describe API::API, api: true do end end + describe "POST /projects/:id/uploads" do + before { project } + + it "uploads the file and returns its info" do + post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") + + expect(response.status).to be(201) + expect(json_response['alt']).to eq("dk") + expect(json_response['url']).to start_with("/uploads/") + expect(json_response['url']).to end_with("/dk.png") + expect(json_response['is_image']).to eq(true) + end + end + describe 'GET /projects/:id' do before { project } before { project_member } @@ -382,6 +396,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/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 17f2643fd45..f966e38cd3e 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -65,6 +65,27 @@ describe API::API, api: true do end end + describe 'DELETE /projects/:id/repository/tags/:tag_name' do + let(:tag_name) { project.repository.tag_names.sort.reverse.first } + + before do + allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true) + end + + context 'delete tag' do + it 'should delete an existing tag' do + delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user) + expect(response.status).to eq(200) + expect(json_response['tag_name']).to eq(tag_name) + end + + it 'should raise 404 if the tag does not exist' do + delete api("/projects/#{project.id}/repository/tags/foobar", user) + expect(response.status).to eq(404) + end + end + end + context 'annotated tag' do it 'should create a new annotated tag' do # Identity must be set in .gitconfig to create annotated tag. diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 82f62a8709c..22ba25217f0 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -80,6 +80,7 @@ describe ProjectsController, 'routing' do it 'to #show' do expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') + expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') end it 'to #update' do @@ -434,6 +435,18 @@ describe Projects::TreeController, 'routing' do end end +# project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/} +# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/} +describe Projects::FindFileController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + end + + it 'to #list' do + expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + end +end + describe Projects::BlobController, 'routing' do it 'to #edit' do expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to( diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index c103752198d..6d219f35895 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(7).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) @@ -245,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) @@ -260,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 @@ -282,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) @@ -296,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) @@ -310,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) @@ -324,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) @@ -338,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) @@ -387,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/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index 5ceed5af9a5..f252e2c5902 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -33,12 +33,12 @@ describe Projects::DownloadService, services: true do @link_to_file = download_file(@project, url) end - it { expect(@link_to_file).to have_key('alt') } - it { expect(@link_to_file).to have_key('url') } - it { expect(@link_to_file).to have_key('is_image') } - it { expect(@link_to_file['is_image']).to be true } - it { expect(@link_to_file['url']).to match('rails_sample.jpg') } - it { expect(@link_to_file['alt']).to eq('rails_sample') } + it { expect(@link_to_file).to have_key(:alt) } + it { expect(@link_to_file).to have_key(:url) } + it { expect(@link_to_file).to have_key(:is_image) } + it { expect(@link_to_file[:is_image]).to be true } + it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } + it { expect(@link_to_file[:alt]).to eq('rails_sample') } end context 'a txt file' do @@ -47,12 +47,12 @@ describe Projects::DownloadService, services: true do @link_to_file = download_file(@project, url) end - it { expect(@link_to_file).to have_key('alt') } - it { expect(@link_to_file).to have_key('url') } - it { expect(@link_to_file).to have_key('is_image') } - it { expect(@link_to_file['is_image']).to be false } - it { expect(@link_to_file['url']).to match('doc_sample.txt') } - it { expect(@link_to_file['alt']).to eq('doc_sample.txt') } + it { expect(@link_to_file).to have_key(:alt) } + it { expect(@link_to_file).to have_key(:url) } + it { expect(@link_to_file).to have_key(:is_image) } + it { expect(@link_to_file[:is_image]).to be false } + it { expect(@link_to_file[:url]).to match('doc_sample.txt') } + it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') } end end end 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/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index c9f828ae2f7..d3364a71022 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -171,7 +171,7 @@ describe SystemNoteService, services: true do context 'when milestone added' do it 'sets the note text' do - expect(subject.note).to eq "Milestone changed to #{milestone.title}" + expect(subject.note).to eq "Milestone changed to #{milestone.to_reference}" end end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index d6d3062a197..5d97fdd4882 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -59,6 +59,10 @@ class MarkdownFeature @label ||= create(:label, name: 'awaiting feedback', project: project) end + def milestone + @milestone ||= create(:milestone, project: project) + end + # Cross-references ----------------------------------------------------------- def xproject @@ -93,6 +97,10 @@ class MarkdownFeature end end + def xmilestone + @xmilestone ||= create(:milestone, project: xproject) + end + def urls Gitlab::Application.routes.url_helpers end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 7eadcd58c1f..b251e7f8f23 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -130,6 +130,15 @@ module MarkdownMatchers end end + # MilestoneReferenceFilter + matcher :reference_milestones do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-milestone', count: 3) + end + end + # TaskListFilter matcher :parse_task_lists do set_default_markdown_messages diff --git a/spec/workers/metrics_worker_spec.rb b/spec/workers/metrics_worker_spec.rb deleted file mode 100644 index 18260ea0c24..00000000000 --- a/spec/workers/metrics_worker_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe MetricsWorker do - let(:worker) { described_class.new } - - describe '#perform' 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) - - worker.perform([{ 'series' => 'kittens', 'tags' => {} }]) - end - end - - describe '#prepare_metrics' do - it 'returns a Hash with the keys as Symbols' do - metrics = worker.prepare_metrics([{ 'values' => {}, 'tags' => {} }]) - - expect(metrics).to eq([{ values: {}, tags: {} }]) - end - - it 'escapes tag values' do - metrics = worker.prepare_metrics([ - { 'values' => {}, 'tags' => { 'foo' => 'bar=' } } - ]) - - expect(metrics).to eq([{ values: {}, tags: { 'foo' => 'bar\\=' } }]) - end - - it 'drops empty tags' do - metrics = worker.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(worker.escape_value('foo=')).to eq('foo\\=') - end - - it 'casts values to Strings' do - expect(worker.escape_value(10)).to eq('10') - end - end -end diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.min.js b/vendor/assets/javascripts/fuzzaldrin-plus.min.js new file mode 100644 index 00000000000..3f25c2d8373 --- /dev/null +++ b/vendor/assets/javascripts/fuzzaldrin-plus.min.js @@ -0,0 +1 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){(function(){var PathSeparator,legacy_scorer,pluckCandidates,scorer,sortCandidates;scorer=require('./scorer');legacy_scorer=require('./legacy');pluckCandidates=function(a){return a.candidate};sortCandidates=function(a,b){return b.score-a.score};PathSeparator=require('path').sep;module.exports=function(candidates,query,arg){var allowErrors,bAllowErrors,bKey,candidate,coreQuery,i,j,key,legacy,len,len1,maxInners,maxResults,prepQuery,queryHasSlashes,ref,score,scoredCandidates,spotLeft,string;ref=arg!=null?arg:{},key=ref.key,maxResults=ref.maxResults,maxInners=ref.maxInners,allowErrors=ref.allowErrors,legacy=ref.legacy;scoredCandidates=[];spotLeft=(maxInners!=null)&&maxInners>0?maxInners:candidates.length;bAllowErrors=!!allowErrors;bKey=key!=null;prepQuery=scorer.prepQuery(query);if(!legacy){for(i=0,len=candidates.length;i<len;i++){candidate=candidates[i];string=bKey?candidate[key]:candidate;if(!string){continue}score=scorer.score(string,query,prepQuery,bAllowErrors);if(score>0){scoredCandidates.push({candidate:candidate,score:score});if(!--spotLeft){break}}}}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;for(j=0,len1=candidates.length;j<len1;j++){candidate=candidates[j];string=key!=null?candidate[key]:candidate;if(!string){continue}score=legacy_scorer.score(string,coreQuery,queryHasSlashes);if(!queryHasSlashes){score=legacy_scorer.basenameScore(string,coreQuery,score)}if(score>0){scoredCandidates.push({candidate:candidate,score:score})}}}scoredCandidates.sort(sortCandidates);candidates=scoredCandidates.map(pluckCandidates);if(maxResults!=null){candidates=candidates.slice(0,maxResults)}return candidates}}).call(this)},{"./legacy":4,"./scorer":6,"path":7}],2:[function(require,module,exports){(function(){var PathSeparator,filter,legacy_scorer,matcher,prepQueryCache,scorer;scorer=require('./scorer');legacy_scorer=require('./legacy');filter=require('./filter');matcher=require('./matcher');PathSeparator=require('path').sep;prepQueryCache=null;module.exports={filter:function(candidates,query,options){if(!((query!=null?query.length:void 0)&&(candidates!=null?candidates.length:void 0))){return[]}return filter(candidates,query,options)},prepQuery:function(query){return scorer.prepQuery(query)},score:function(string,query,prepQuery,arg){var allowErrors,coreQuery,legacy,queryHasSlashes,ref,score;ref=arg!=null?arg:{},allowErrors=ref.allowErrors,legacy=ref.legacy;if(!((string!=null?string.length:void 0)&&(query!=null?query.length:void 0))){return 0}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!legacy){score=scorer.score(string,query,prepQuery,!!allowErrors)}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;score=legacy_scorer.score(string,coreQuery,queryHasSlashes);if(!queryHasSlashes){score=legacy_scorer.basenameScore(string,coreQuery,score)}}return score},match:function(string,query,prepQuery,arg){var allowErrors,baseMatches,i,matches,query_lw,ref,results,string_lw;allowErrors=(arg!=null?arg:{}).allowErrors;if(!string){return[]}if(!query){return[]}if(string===query){return(function(){results=[];for(var i=0,ref=string.length;0<=ref?i<ref:i>ref;0<=ref?i++:i--){results.push(i)}return results}).apply(this)}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!(allowErrors||scorer.isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return[]}string_lw=string.toLowerCase();query_lw=prepQuery.query_lw;matches=matcher.match(string,string_lw,prepQuery);if(matches.length===0){return matches}if(string.indexOf(PathSeparator)>-1){baseMatches=matcher.basenameMatch(string,string_lw,prepQuery);matches=matcher.mergeMatches(matches,baseMatches)}return matches}}}).call(this)},{"./filter":1,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],3:[function(require,module,exports){fuzzaldrinPlus=require('./fuzzaldrin')},{"./fuzzaldrin":2}],4:[function(require,module,exports){(function(){var PathSeparator,queryIsLastPathSegment;PathSeparator=require('path').sep;exports.basenameScore=function(string,query,score){var base,depth,index,lastCharacter,segmentCount,slashCount;index=string.length-1;while(string[index]===PathSeparator){index--}slashCount=0;lastCharacter=index;base=null;while(index>=0){if(string[index]===PathSeparator){slashCount++;if(base==null){base=string.substring(index+1,lastCharacter+1)}}else if(index===0){if(lastCharacter<string.length-1){if(base==null){base=string.substring(0,lastCharacter+1)}}else{if(base==null){base=string}}}index--}if(base===string){score*=2}else if(base){score+=exports.score(base,query)}segmentCount=slashCount+1;depth=Math.max(1,10-segmentCount);score*=depth*0.01;return score};exports.score=function(string,query){var character,characterScore,indexInQuery,indexInString,lowerCaseIndex,minIndex,queryLength,queryScore,ref,stringLength,totalCharacterScore,upperCaseIndex;if(string===query){return 1}if(queryIsLastPathSegment(string,query)){return 1}totalCharacterScore=0;queryLength=query.length;stringLength=string.length;indexInQuery=0;indexInString=0;while(indexInQuery<queryLength){character=query[indexInQuery++];lowerCaseIndex=string.indexOf(character.toLowerCase());upperCaseIndex=string.indexOf(character.toUpperCase());minIndex=Math.min(lowerCaseIndex,upperCaseIndex);if(minIndex===-1){minIndex=Math.max(lowerCaseIndex,upperCaseIndex)}indexInString=minIndex;if(indexInString===-1){return 0}characterScore=0.1;if(string[indexInString]===character){characterScore+=0.1}if(indexInString===0||string[indexInString-1]===PathSeparator){characterScore+=0.8}else if((ref=string[indexInString-1])==='-'||ref==='_'||ref===' '){characterScore+=0.7}string=string.substring(indexInString+1,stringLength);totalCharacterScore+=characterScore}queryScore=totalCharacterScore/queryLength;return((queryScore*(queryLength/stringLength))+queryScore)/2};queryIsLastPathSegment=function(string,query){if(string[string.length-query.length-1]===PathSeparator){return string.lastIndexOf(query)===string.length-query.length}};exports.match=function(string,query,stringOffset){var character,i,indexInQuery,indexInString,lowerCaseIndex,matches,minIndex,queryLength,ref,results,stringLength,upperCaseIndex;if(stringOffset==null){stringOffset=0}if(string===query){return(function(){results=[];for(var i=stringOffset,ref=stringOffset+string.length;stringOffset<=ref?i<ref:i>ref;stringOffset<=ref?i++:i--){results.push(i)}return results}).apply(this)}queryLength=query.length;stringLength=string.length;indexInQuery=0;indexInString=0;matches=[];while(indexInQuery<queryLength){character=query[indexInQuery++];lowerCaseIndex=string.indexOf(character.toLowerCase());upperCaseIndex=string.indexOf(character.toUpperCase());minIndex=Math.min(lowerCaseIndex,upperCaseIndex);if(minIndex===-1){minIndex=Math.max(lowerCaseIndex,upperCaseIndex)}indexInString=minIndex;if(indexInString===-1){return[]}matches.push(stringOffset+indexInString);stringOffset+=indexInString+1;string=string.substring(indexInString+1,stringLength)}return matches}}).call(this)},{"path":7}],5:[function(require,module,exports){(function(){var PathSeparator,scorer;PathSeparator=require('path').sep;scorer=require('./scorer');exports.basenameMatch=function(subject,subject_lw,prepQuery){var basePos,depth,end;end=subject.length-1;while(subject[end]===PathSeparator){end--}basePos=subject.lastIndexOf(PathSeparator,end);if(basePos===-1){return[]}depth=prepQuery.depth;while(depth-->0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return[]}}basePos++;end++;return exports.match(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery,basePos)};exports.mergeMatches=function(a,b){var ai,bj,i,j,m,n,out;m=a.length;n=b.length;if(n===0){return a.slice()}if(m===0){return b.slice()}i=-1;j=0;bj=b[j];out=[];while(++i<m){ai=a[i];while(bj<=ai&&++j<n){if(bj<ai){out.push(bj)}bj=b[j]}out.push(ai)}while(j<n){out.push(b[j++])}return out};exports.match=function(subject,subject_lw,prepQuery,offset){var DIAGONAL,LEFT,STOP,UP,acro_score,align,backtrack,csc_diag,csc_row,csc_score,i,j,m,matches,move,n,pos,query,query_lw,score,score_diag,score_row,score_up,si_lw,start,trace;if(offset==null){offset=0}query=prepQuery.query;query_lw=prepQuery.query_lw;m=subject.length;n=query.length;acro_score=scorer.scoreAcronyms(subject,subject_lw,query,query_lw).score;score_row=new Array(n);csc_row=new Array(n);STOP=0;UP=1;LEFT=2;DIAGONAL=3;trace=new Array(m*n);pos=-1;j=-1;while(++j<n){score_row[j]=0;csc_row[j]=0}i=-1;while(++i<m){score=0;score_up=0;csc_diag=0;si_lw=subject_lw[i];j=-1;while(++j<n){csc_score=0;align=0;score_diag=score_up;if(query_lw[j]===si_lw){start=scorer.isWordStart(i,subject,subject_lw);csc_score=csc_diag>0?csc_diag:scorer.scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scorer.scoreCharacter(i,j,start,acro_score,csc_score)}score_up=score_row[j];csc_diag=csc_row[j];if(score>score_up){move=LEFT}else{score=score_up;move=UP}if(align>score){score=align;move=DIAGONAL}else{csc_score=0}score_row[j]=score;csc_row[j]=csc_score;trace[++pos]=score>0?move:STOP}}i=m-1;j=n-1;pos=i*n+j;backtrack=true;matches=[];while(backtrack&&i>=0&&j>=0){switch(trace[pos]){case UP:i--;pos-=n;break;case LEFT:j--;pos--;break;case DIAGONAL:matches.push(i+offset);j--;i--;pos-=n+1;break;default:backtrack=false}}matches.reverse();return matches}}).call(this)},{"./scorer":6,"path":7}],6:[function(require,module,exports){(function(){var AcronymResult,PathSeparator,Query,basenameScore,coreChars,countDir,doScore,emptyAcronymResult,file_coeff,isMatch,isSeparator,isWordEnd,isWordStart,miss_coeff,opt_char_re,pos_bonus,scoreAcronyms,scoreCharacter,scoreConsecutives,scoreExact,scoreExactMatch,scorePattern,scorePosition,scoreSize,tau_depth,tau_size,truncatedUpperCase,wm;PathSeparator=require('path').sep;wm=150;pos_bonus=20;tau_depth=13;tau_size=85;file_coeff=1.2;miss_coeff=0.75;opt_char_re=/[ _\-:\/\\]/g;exports.coreChars=coreChars=function(query){return query.replace(opt_char_re,'')};exports.score=function(string,query,prepQuery,allowErrors){var score,string_lw;if(prepQuery==null){prepQuery=new Query(query)}if(allowErrors==null){allowErrors=false}if(!(allowErrors||isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return 0}string_lw=string.toLowerCase();score=doScore(string,string_lw,prepQuery);return Math.ceil(basenameScore(string,string_lw,prepQuery,score))};Query=(function(){function Query(query){if(!(query!=null?query.length:void 0)){return null}this.query=query;this.query_lw=query.toLowerCase();this.core=coreChars(query);this.core_lw=this.core.toLowerCase();this.core_up=truncatedUpperCase(this.core);this.depth=countDir(query,query.length)}return Query})();exports.prepQuery=function(query){return new Query(query)};exports.isMatch=isMatch=function(subject,query_lw,query_up){var i,j,m,n,qj_lw,qj_up,si;m=subject.length;n=query_lw.length;if(!m||!n||n>m){return false}i=-1;j=-1;while(++j<n){qj_lw=query_lw[j];qj_up=query_up[j];while(++i<m){si=subject[i];if(si===qj_lw||si===qj_up){break}}if(i===m){return false}}return true};doScore=function(subject,subject_lw,prepQuery){var acro,acro_score,align,csc_diag,csc_row,csc_score,i,j,m,miss_budget,miss_left,mm,n,pos,query,query_lw,record_miss,score,score_diag,score_row,score_up,si_lw,start,sz;query=prepQuery.query;query_lw=prepQuery.query_lw;m=subject.length;n=query.length;acro=scoreAcronyms(subject,subject_lw,query,query_lw);acro_score=acro.score;if(acro.count===n){return scoreExact(n,m,acro_score,acro.pos)}pos=subject_lw.indexOf(query_lw);if(pos>-1){return scoreExactMatch(subject,subject_lw,query,query_lw,pos,n,m)}score_row=new Array(n);csc_row=new Array(n);sz=scoreSize(n,m);miss_budget=Math.ceil(miss_coeff*n)+5;miss_left=miss_budget;j=-1;while(++j<n){score_row[j]=0;csc_row[j]=0}i=subject_lw.indexOf(query_lw[0]);if(i>-1){i--}mm=subject_lw.lastIndexOf(query_lw[n-1],m);if(mm>i){m=mm+1}while(++i<m){score=0;score_diag=0;csc_diag=0;si_lw=subject_lw[i];record_miss=true;j=-1;while(++j<n){score_up=score_row[j];if(score_up>score){score=score_up}csc_score=0;if(query_lw[j]===si_lw){start=isWordStart(i,subject,subject_lw);csc_score=csc_diag>0?csc_diag:scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scoreCharacter(i,j,start,acro_score,csc_score);if(align>score){score=align;miss_left=miss_budget}else{if(record_miss&&--miss_left<=0){return score_row[n-1]*sz}record_miss=false}}score_diag=score_up;csc_diag=csc_row[j];csc_row[j]=csc_score;score_row[j]=score}}return score*sz};exports.isWordStart=isWordStart=function(pos,subject,subject_lw){var curr_s,prev_s;if(pos===0){return true}curr_s=subject[pos];prev_s=subject[pos-1];return isSeparator(curr_s)||isSeparator(prev_s)||(curr_s!==subject_lw[pos]&&prev_s===subject_lw[pos-1])};exports.isWordEnd=isWordEnd=function(pos,subject,subject_lw,len){var curr_s,next_s;if(pos===len-1){return true}curr_s=subject[pos];next_s=subject[pos+1];return isSeparator(curr_s)||isSeparator(next_s)||(curr_s===subject_lw[pos]&&next_s!==subject_lw[pos+1])};isSeparator=function(c){return c===' '||c==='.'||c==='-'||c==='_'||c==='/'||c==='\\'};scorePosition=function(pos){var sc;if(pos<pos_bonus){sc=pos_bonus-pos;return 100+sc*sc}else{return Math.max(100+pos_bonus-pos,0)}};scoreSize=function(n,m){return tau_size/(tau_size+Math.abs(m-n))};scoreExact=function(n,m,quality,pos){return 2*n*(wm*quality+scorePosition(pos))*scoreSize(n,m)};exports.scorePattern=scorePattern=function(count,len,sameCase,start,end){var bonus,sz;sz=count;bonus=6;if(sameCase===count){bonus+=2}if(start){bonus+=3}if(end){bonus+=1}if(count===len){if(start){if(sameCase===len){sz+=2}else{sz+=1}}if(end){bonus+=1}}return sameCase+sz*(sz+bonus)};exports.scoreCharacter=scoreCharacter=function(i,j,start,acro_score,csc_score){var posBonus;posBonus=scorePosition(i);if(start){return posBonus+wm*((acro_score>csc_score?acro_score:csc_score)+10)}return posBonus+wm*csc_score};exports.scoreConsecutives=scoreConsecutives=function(subject,subject_lw,query,query_lw,i,j,start){var k,m,mi,n,nj,sameCase,startPos,sz;m=subject.length;n=query.length;mi=m-i;nj=n-j;k=mi<nj?mi:nj;startPos=i;sameCase=0;sz=0;if(query[j]===subject[i]){sameCase++}while(++sz<k&&query_lw[++j]===subject_lw[++i]){if(query[j]===subject[i]){sameCase++}}if(sz===1){return 1+2*sameCase}return scorePattern(sz,n,sameCase,start,isWordEnd(i,subject,subject_lw,m))};exports.scoreExactMatch=scoreExactMatch=function(subject,subject_lw,query,query_lw,pos,n,m){var end,i,pos2,sameCase,start;start=isWordStart(pos,subject,subject_lw);if(!start){pos2=subject_lw.indexOf(query_lw,pos+1);if(pos2>-1){start=isWordStart(pos2,subject,subject_lw);if(start){pos=pos2}}}i=-1;sameCase=0;while(++i<n){if(query[pos+i]===subject[i]){sameCase++}}end=isWordEnd(pos+n-1,subject,subject_lw,m);return scoreExact(n,m,scorePattern(n,n,sameCase,start,end),pos)};AcronymResult=(function(){function AcronymResult(score1,pos1,count1){this.score=score1;this.pos=pos1;this.count=count1}return AcronymResult})();emptyAcronymResult=new AcronymResult(0,0.1,0);exports.scoreAcronyms=scoreAcronyms=function(subject,subject_lw,query,query_lw){var count,i,j,m,n,pos,qj_lw,sameCase,score;m=subject.length;n=query.length;if(!(m>1&&n>1)){return emptyAcronymResult}count=0;pos=0;sameCase=0;i=-1;j=-1;while(++j<n){qj_lw=query_lw[j];while(++i<m){if(qj_lw===subject_lw[i]&&isWordStart(i,subject,subject_lw)){if(query[j]===subject[i]){sameCase++}pos+=i;count++;break}}if(i===m){break}}if(count<2){return emptyAcronymResult}score=scorePattern(count,n,sameCase,true,false);return new AcronymResult(score,pos/count,count)};basenameScore=function(subject,subject_lw,prepQuery,fullPathScore){var alpha,basePathScore,basePos,depth,end;if(fullPathScore===0){return 0}end=subject.length-1;while(subject[end]===PathSeparator){end--}basePos=subject.lastIndexOf(PathSeparator,end);if(basePos===-1){return fullPathScore}depth=prepQuery.depth;while(depth-->0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return fullPathScore}}basePos++;end++;basePathScore=doScore(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery);alpha=0.5*tau_depth/(tau_depth+countDir(subject,end+1));return alpha*basePathScore+(1-alpha)*fullPathScore*scoreSize(0,file_coeff*(end-basePos))};exports.countDir=countDir=function(path,end){var count,i;if(end<1){return 0}count=0;i=-1;while(++i<end&&path[i]===PathSeparator){continue}while(++i<end){if(path[i]===PathSeparator){count++;while(++i<end&&path[i]===PathSeparator){continue}}}return count};truncatedUpperCase=function(str){var char,l,len1,upper;upper="";for(l=0,len1=str.length;l<len1;l++){char=str[l];upper+=char.toUpperCase()[0]}return upper}}).call(this)},{"path":7}],7:[function(require,module,exports){(function(process){function normalizeArray(parts,allowAboveRoot){var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==='.'){parts.splice(i,1)}else if(last==='..'){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up--;up){parts.unshift('..')}}return parts}var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;var splitPath=function(filename){return splitPathRe.exec(filename).slice(1)};exports.resolve=function(){var resolvedPath='',resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=(i>=0)?arguments[i]:process.cwd();if(typeof path!=='string'){throw new TypeError('Arguments to path.resolve must be strings');}else if(!path){continue}resolvedPath=path+'/'+resolvedPath;resolvedAbsolute=path.charAt(0)==='/'}resolvedPath=normalizeArray(filter(resolvedPath.split('/'),function(p){return!!p}),!resolvedAbsolute).join('/');return((resolvedAbsolute?'/':'')+resolvedPath)||'.'};exports.normalize=function(path){var isAbsolute=exports.isAbsolute(path),trailingSlash=substr(path,-1)==='/';path=normalizeArray(filter(path.split('/'),function(p){return!!p}),!isAbsolute).join('/');if(!path&&!isAbsolute){path='.'}if(path&&trailingSlash){path+='/'}return(isAbsolute?'/':'')+path};exports.isAbsolute=function(path){return path.charAt(0)==='/'};exports.join=function(){var paths=Array.prototype.slice.call(arguments,0);return exports.normalize(filter(paths,function(p,index){if(typeof p!=='string'){throw new TypeError('Arguments to path.join must be strings');}return p}).join('/'))};exports.relative=function(from,to){from=exports.resolve(from).substr(1);to=exports.resolve(to).substr(1);function trim(arr){var start=0;for(;start<arr.length;start++){if(arr[start]!=='')break}var end=arr.length-1;for(;end>=0;end--){if(arr[end]!=='')break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split('/'));var toParts=trim(to.split('/'));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i<length;i++){if(fromParts[i]!==toParts[i]){samePartsLength=i;break}}var outputParts=[];for(var i=samePartsLength;i<fromParts.length;i++){outputParts.push('..')}outputParts=outputParts.concat(toParts.slice(samePartsLength));return outputParts.join('/')};exports.sep='/';exports.delimiter=':';exports.dirname=function(path){var result=splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return'.'}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir};exports.basename=function(path,ext){var f=splitPath(path)[2];if(ext&&f.substr(-1*ext.length)===ext){f=f.substr(0,f.length-ext.length)}return f};exports.extname=function(path){return splitPath(path)[3]};function filter(xs,f){if(xs.filter)return xs.filter(f);var res=[];for(var i=0;i<xs.length;i++){if(f(xs[i],i,xs))res.push(xs[i])}return res}var substr='ab'.substr(-1)==='b'?function(str,start,len){return str.substr(start,len)}:function(str,start,len){if(start<0)start=str.length+start;return str.substr(start,len)}}).call(this,require('_process'))},{"_process":8}],8:[function(require,module,exports){var process=module.exports={};var queue=[];var draining=false;var currentQueue;var queueIndex=-1;function cleanUpNextTick(){draining=false;if(currentQueue.length){queue=currentQueue.concat(queue)}else{queueIndex=-1}if(queue.length){drainQueue()}}function drainQueue(){if(draining){return}var timeout=setTimeout(cleanUpNextTick);draining=true;var len=queue.length;while(len){currentQueue=queue;queue=[];while(++queueIndex<len){if(currentQueue){currentQueue[queueIndex].run()}}queueIndex=-1;len=queue.length}currentQueue=null;draining=false;clearTimeout(timeout)}process.nextTick=function(fun){var args=new Array(arguments.length-1);if(arguments.length>1){for(var i=1;i<arguments.length;i++){args[i-1]=arguments[i]}}queue.push(new Item(fun,args));if(queue.length===1&&!draining){setTimeout(drainQueue,0)}};function Item(fun,array){this.fun=fun;this.array=array}Item.prototype.run=function(){this.fun.apply(null,this.array)};process.title='browser';process.browser=true;process.env={};process.argv=[];process.version='';process.versions={};function noop(){}process.on=noop;process.addListener=noop;process.once=noop;process.off=noop;process.removeListener=noop;process.removeAllListeners=noop;process.emit=noop;process.binding=function(name){throw new Error('process.binding is not supported');};process.cwd=function(){return'/'};process.chdir=function(dir){throw new Error('process.chdir is not supported');};process.umask=function(){return 0}},{}]},{},[3]); 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 || ' ')+'</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 || ' ')+'</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("<","<").replace(">",">").replace(" & "," & ")}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 |