diff --git a/.gitignore b/.gitignore index 8a68bb3e4f0..2a97eacad48 100644 --- a/.gitignore +++ b/.gitignore @@ -20,12 +20,13 @@ backups/* config/aws.yml config/database.yml config/gitlab.yml -config/initializers/omniauth.rb +config/gitlab_ci.yml config/initializers/rack_attack.rb config/initializers/smtp_settings.rb config/resque.yml config/unicorn.rb config/mail_room.yml +config/secrets.yml coverage/* db/*.sqlite3 db/*.sqlite3-journal @@ -41,3 +42,4 @@ rails_best_practices_output.html /tags tmp/ vendor/bundle/* +builds/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ddf4e31204a..cf6d28b01af 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,6 +24,14 @@ spec:api: - ruby - mysql +spec:benchmark: + script: + - RAILS_ENV=test bundle exec rake spec:benchmark + tags: + - ruby + - mysql + allow_failure: true + spec:other: script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other diff --git a/.rubocop.yml b/.rubocop.yml index ea4d365761e..11e4502849a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -932,7 +932,7 @@ Lint/UselessAccessModifier: Lint/UselessAssignment: Description: 'Checks for useless assignment to a local variable.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' - Enabled: false + Enabled: true Lint/UselessComparison: Description: 'Checks for comparison of something with itself.' @@ -998,7 +998,9 @@ AllCops: - 'tmp/**/*' - 'bin/**/*' - 'lib/backup/**/*' + - 'lib/ci/backup/**/*' - 'lib/tasks/**/*' + - 'lib/ci/migrate/**/*' - 'lib/email_validator.rb' - 'lib/gitlab/upgrader.rb' - 'lib/gitlab/seeder.rb' diff --git a/CHANGELOG b/CHANGELOG index 8c390c16205..388fa2f8966 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,14 +1,91 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.0.0 (unreleased) +v 8.1.0 (unreleased) + - Fix bug where transferring a project would result in stale commit links (Stan Hu) + - Include full path of source and target branch names in New Merge Request page (Stan Hu) + - Add user preference to view activities as default dashboard (Stan Hu) + - Add option to admin area to sign in as a specific user (Pavel Forkert) + - Show CI status on all pages where commits list is rendered + - Automatically enable CI when push .gitlab-ci.yml file to repository + - Move CI charts to project graphs area + - Fix cases where Markdown did not render links in activity feed (Stan Hu) + - Add first and last to pagination (Zeger-Jan van de Weg) + - Show CI status on commit page + - Show CI status on Your projects page and Starred projects page + - Remove "Continuous Integration" page from dashboard + - Add notes and SSL verification entries to hook APIs (Ben Boeckel) + - Fix grammar in admin area "labels" .nothing-here-block when no labels exist. + - Move CI runners page to project settings area + - Move CI variables page to project settings area + - Move CI triggers page to project settings area + - Move CI project settings page to CE project settings area + - Fix bug when removed file was not appearing in merge request diff + - Note the original location of a moved project when notifying users of the move + - Improve error message when merging fails + - Add support of multibyte characters in LDAP UID (Roman Petrov) + - Show additions/deletions stats on merge request diff + - Remove footer text in emails (Zeger-Jan van de Weg) + - Ensure code blocks are properly highlighted after a note is updated + - Fix wrong access level badge on MR comments + - Hide password in the service settings form + - Move CI web hooks page to project settings area + - Fix User Identities API. It now allows you to properly create or update user's identities. + - Add user preference to change layout width (Peter Göbel) + - Use commit status in merge request widget as preffered source of CI status + - Integrate CI commit and build pages into project pages + +v 8.0.4 + - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) + - Fix referrals for :back and relative URL installs + - Fix anchors to comments in diffs + - Remove CI token from build traces + - Fix "Assign All" button on Runner admin page + - Fix search in Files + +v 8.0.3 + - Fix URL shown in Slack notifications + - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu) + - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu) + +v 8.0.2 + - Fix default avatar not rendering in network graph (Stan Hu) + - Skip check_initd_configured_correctly on omnibus installs + - Prevent double-prefixing of help page paths + - Clarify confirmation text on user deletion + - Make commit graphs responsive to window width changes (Stan Hu) + - Fix top margin for sign-in button on public pages + - Fix LDAP attribute mapping + - Remove git refs used internally by GitLab from network graph (Stan Hu) + - Use standard Markdown font in Markdown preview instead of fixed-width font (Stan Hu) + - Fix Reply by email for non-UTF-8 messages. + - Add option to use StartTLS with Reply by email IMAP server. + - Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie) + +v 8.0.1 + - Remove git refs used internally by GitLab from network graph (Stan Hu) + - Improve CI migration procedure and documentation + +v 8.0.0 + - Fix Markdown links not showing up in dashboard activity feed (Stan Hu) + - Remove milestones from merge requests when milestones are deleted (Stan Hu) + - Fix HTML link that was improperly escaped in new user e-mail (Stan Hu) + - Fix broken sort in merge request API (Stan Hu) + - Bump rouge to 1.10.1 to remove warning noise and fix other syntax highlighting bugs (Stan Hu) + - Gracefully handle errors in syntax highlighting by leaving the block unformatted (Stan Hu) + - Add "replace" and "upload" functionalities to allow user replace existing file and upload new file into current repository + - Fix URL construction for merge requests, issues, notes, and commits for relative URL config (Stan Hu) + - Fix emoji URLs in Markdown when relative_url_root is used (Stan Hu) + - Omit filename in Content-Disposition header in raw file download to avoid RFC 6266 encoding issues (Stan HU) + - Fix broken Wiki Page History (Stan Hu) + - Import forked repositories asynchronously to prevent large repositories from timing out (Stan Hu) - Prevent anchors from being hidden by header (Stan Hu) - Fix bug where only the first 15 Bitbucket issues would be imported (Stan Hu) - Sort issues by creation date in Bitbucket importer (Stan Hu) - - Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu) - Prevent too many redirects upon login when home page URL is set to external_url (Stan Hu) - Improve dropdown positioning on the project home page (Hannes Rosenögger) - Upgrade browser gem to 1.0.0 to avoid warning in IE11 compatibilty mode (Stan Hu) - Remove user OAuth tokens from the database and request new tokens each session (Stan Hu) + - Restrict users API endpoints to use integer IDs (Stan Hu) - Only show recent push event if the branch still exists or a recent merge request has not been created (Stan Hu) - Remove satellites - Better performance for web editor (switched from satellites to rugged) @@ -20,20 +97,50 @@ v 8.0.0 (unreleased) - Create cross-reference for closing references on commits pushed to non-default branches (Maël Valais) - Ability to search milestones - Gracefully handle SMTP user input errors (e.g. incorrect email addresses) to prevent Sidekiq retries (Stan Hu) - - Move dashboard activity to separate page + - Move dashboard activity to separate page (for your projects and starred projects) - Improve performance of git blame - Limit content width to 1200px for most of pages to improve readability on big screens - Fix 500 error when submit project snippet without body - Improve search page usability - Bring more UI consistency in way how projects, snippets and groups lists are rendered - - Make all profiles public + - Make all profiles and group public - Fixed login failure when extern_uid changes (Joel Koglin) - Don't notify users without access to the project when they are (accidentally) mentioned in a note. - Retrieving oauth token with LDAP credentials + - Load Application settings from running database unless env var USE_DB=false + - Added Drone CI integration (Kirill Zaitsev) + - Allow developers to retry builds + - Hide advanced project options for non-admin users + - Fail builds if no .gitlab-ci.yml is found + - Refactored service API and added automatically service docs generator (Kirill Zaitsev) + - Added web_url key project hook_attrs (Kirill Zaitsev) + - Add ability to get user information by ID of an SSH key via the API + - Fix bug which IE cannot show image at markdown when the image is raw file of gitlab + - Add support for Crowd + - Global Labels that are available to all projects + - Fix highlighting of deleted lines in diffs. + - Project notification level can be set on the project page itself + - Added service API endpoint to retrieve service parameters (Petheő Bence) + - Add FogBugz project import (Jared Szechy) + - Sort users autocomplete lists by user (Allister Antosik) + - Webhook for issue now contains repository field (Jungkook Park) + - Add ability to add custom text to the help page (Jeroen van Baarsen) + - Add pg_schema to backup config + - Fix references to target project issues in Merge Requests markdown preview and textareas (Francesco Levorato) + - Redirect from incorrectly cased group or project path to correct one (Francesco Levorato) + - Removed API calls from CE to CI + +v 7.14.3 + - No changes + +v 7.14.2 + - Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu) + - Allow configuration of LDAP attributes GitLab will use for the new user account. v 7.14.1 - Improve abuse reports management from admin area - Fix "Reload with full diff" URL button in compare branch view (Stan Hu) + - Disabled DNS lookups for SSH in docker image (Rowan Wookey) - Only include base URL in OmniAuth full_host parameter (Stan Hu) - Fix Error 500 in API when accessing a group that has an avatar (Stan Hu) - Ability to enable SSL verification for Webhooks @@ -114,7 +221,7 @@ v 7.13.4 v 7.13.3 - Fix bug causing Bitbucket importer to crash when OAuth application had been removed. - Allow users to send abuse reports - - Remove satellites + - Remove satellites - Link username to profile on Group Members page (Tom Webster) v 7.13.2 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 2714f5313ae..57cf282ebbc 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.4 +2.6.5 diff --git a/Gemfile b/Gemfile index 2a55bb31cb9..4938cbf8b80 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,14 @@ source "https://rubygems.org" -gem 'rails', '4.1.11' +def darwin_only(require_as) + RUBY_PLATFORM.include?('darwin') && require_as +end + +def linux_only(require_as) + RUBY_PLATFORM.include?('linux') && require_as +end + +gem 'rails', '4.1.12' # Specify a sprockets version due to security issue # See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY @@ -10,40 +18,41 @@ gem 'sprockets', '~> 2.12.3' gem "default_value_for", "~> 3.0.0" # Supported DBs -gem "mysql2", group: :mysql -gem "pg", group: :postgres +gem "mysql2", '~> 0.3.16', group: :mysql +gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries -gem "devise", '3.2.4' -gem "devise-async", '0.9.0' +gem "devise", '~> 3.5.2' +gem "devise-async", '~> 0.9.0' gem 'omniauth', "~> 1.2.2" -gem 'omniauth-google-oauth2' -gem 'omniauth-twitter' -gem 'omniauth-github' -gem 'omniauth-shibboleth' -gem 'omniauth-kerberos', group: :kerberos -gem 'omniauth-gitlab' -gem 'omniauth-bitbucket' +gem 'omniauth-google-oauth2', '~> 0.2.5' +gem 'omniauth-twitter', '~> 1.0.1' +gem 'omniauth-github', '~> 1.1.1' +gem 'omniauth-shibboleth', '~> 1.1.1' +gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos +gem 'omniauth-gitlab', '~> 1.0.0' +gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-saml', '~> 1.4.0' -gem 'doorkeeper', '2.1.3' +gem 'doorkeeper', '~> 2.1.3' +gem 'omniauth_crowd' gem "rack-oauth2", "~> 1.0.5" # Two-factor authentication -gem 'devise-two-factor' -gem 'rqrcode-rails3' -gem 'attr_encrypted', '1.3.4' +gem 'devise-two-factor', '~> 2.0.0' +gem 'rqrcode-rails3', '~> 0.1.7' +gem 'attr_encrypted', '~> 1.3.4' # Browser detection gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.15' +gem "gitlab_git", '~> 7.2.17' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master -gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap" +gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap" # Git Wiki gem 'gollum-lib', '~> 4.0.2' @@ -58,47 +67,47 @@ gem "gitlab-linguist", "~> 3.0.1", require: "linguist" # API gem "grape", "~> 0.6.1" gem "grape-entity", "~> 0.4.2" -gem 'rack-cors', require: 'rack/cors' +gem 'rack-cors', '~> 0.2.9', require: 'rack/cors' # Format dates and times # based on human-friendly examples -gem "stamp" +gem "stamp", '~> 0.5.0' # Enumeration fields -gem 'enumerize' +gem 'enumerize', '~> 0.7.0' # Pagination -gem "kaminari", "~> 0.15.1" +gem "kaminari", "~> 0.16.3" # HAML -gem "haml-rails" +gem "haml-rails", '~> 0.5.3' # Files attachments -gem "carrierwave" +gem "carrierwave", '~> 0.9.0' # Drag and Drop UI -gem 'dropzonejs-rails' +gem 'dropzonejs-rails', '~> 0.7.1' # for aws storage gem "fog", "~> 1.25.0" -gem "unf" +gem "unf", '~> 0.1.4' # Authorization -gem "six" +gem "six", '~> 0.2.0' # Seed data -gem "seed-fu" +gem "seed-fu", '~> 2.3.5' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' -gem 'task_list', '1.0.2', require: 'task_list/railtie' -gem 'github-markup' +gem 'task_list', '~> 1.0.2', require: 'task_list/railtie' +gem 'github-markup', '~> 1.3.1' gem 'redcarpet', '~> 3.3.2' -gem 'RedCloth' +gem 'RedCloth', '~> 4.2.9' gem 'rdoc', '~>3.6' -gem 'org-ruby', '= 0.9.12' +gem 'org-ruby', '~> 0.9.12' gem 'creole', '~>0.3.6' -gem 'wikicloth', '=0.8.1' +gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' # Diffs @@ -106,37 +115,40 @@ gem 'diffy', '~> 3.0.3' # Application server group :unicorn do - gem "unicorn", '~> 4.6.3' - gem 'unicorn-worker-killer' + gem "unicorn", '~> 4.8.2' + gem 'unicorn-worker-killer', '~> 0.4.2' end # State machine -gem "state_machine" +gem "state_machine", '~> 1.2.0' +# Run events after state machine commits +gem 'after_commit_queue' # Issue tags gem 'acts-as-taggable-on', '~> 3.4' # Background jobs -gem 'slim' -gem 'sinatra', require: nil -gem 'sidekiq', '~> 3.3' -gem 'sidetiq', '0.6.3' +gem 'slim', '~> 2.0.2' +gem 'sinatra', '~> 1.4.4', require: nil +gem 'sidekiq', '3.3.0' +gem 'sidetiq', '~> 0.6.3' # HTTP requests -gem "httparty" +gem "httparty", '~> 0.13.3' # Colored output to console -gem "colored" +gem "colored", '~> 1.2' +gem "colorize", '~> 0.5.8' # GitLab settings -gem 'settingslogic' +gem 'settingslogic', '~> 2.0.9' # Misc -gem "foreman" -gem 'version_sorter' + +gem 'version_sorter', '~> 2.0.0' # Cache -gem "redis-rails" +gem "redis-rails", '~> 4.0.0' # Campfire integration gem 'tinder', '~> 1.9.2' @@ -156,6 +168,9 @@ gem "slack-notifier", "~> 1.0.0" # Asana integration gem 'asana', '~> 0.0.6' +# FogBugz integration +gem 'ruby-fogbugz', '~> 0.2.1' + # d3 gem 'd3_rails', '~> 3.5.5' @@ -172,69 +187,70 @@ gem "sanitize", '~> 2.0' gem "rack-attack", '~> 4.3.0' # Ace editor -gem 'ace-rails-ap' +gem 'ace-rails-ap', '~> 2.0.1' # Keyboard shortcuts -gem 'mousetrap-rails' +gem 'mousetrap-rails', '~> 1.4.6' # Detect and convert string character encoding -gem 'charlock_holmes' +gem 'charlock_holmes', '~> 0.6.9.4' gem "sass-rails", '~> 4.0.5' -gem "coffee-rails" -gem "uglifier" +gem "coffee-rails", '~> 4.1.0' +gem "uglifier", '~> 2.3.2' gem 'turbolinks', '~> 2.5.0' -gem 'jquery-turbolinks' +gem 'jquery-turbolinks', '~> 2.0.1' -gem 'addressable' +gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.0' gem 'font-awesome-rails', '~> 4.2' gem 'gitlab_emoji', '~> 0.1' gem 'gon', '~> 5.0.0' gem 'jquery-atwho-rails', '~> 1.0.0' -gem 'jquery-rails', '3.1.3' -gem 'jquery-scrollto-rails' -gem 'jquery-ui-rails' -gem 'nprogress-rails' +gem 'jquery-rails', '~> 3.1.3' +gem 'jquery-scrollto-rails', '~> 1.4.3' +gem 'jquery-ui-rails', '~> 4.2.1' +gem 'nprogress-rails', '~> 0.1.2.3' gem 'raphael-rails', '~> 2.1.2' -gem 'request_store' +gem 'request_store', '~> 1.2.0' gem 'select2-rails', '~> 3.5.9' -gem 'virtus' +gem 'virtus', '~> 1.0.1' group :development do - gem 'brakeman', require: false - gem "annotate", "~> 2.6.0.beta2" - gem "letter_opener" - gem 'quiet_assets', '~> 1.0.1' - gem 'rack-mini-profiler', require: false + gem "foreman" + gem 'brakeman', '3.0.1', require: false + + gem "annotate", "~> 2.6.0" + gem "letter_opener", '~> 1.1.2' + gem 'quiet_assets', '~> 1.0.2' + gem 'rack-mini-profiler', '~> 0.9.0', require: false gem 'rerun', '~> 0.10.0' # Better errors handler - gem 'better_errors' - gem 'binding_of_caller' + gem 'better_errors', '~> 1.0.1' + gem 'binding_of_caller', '~> 0.7.2' # Docs generator - gem "sdoc" + gem "sdoc", '~> 0.3.20' # thin instead webrick - gem 'thin' + gem 'thin', '~> 1.6.1' end group :development, :test do - gem 'awesome_print' gem 'byebug', platform: :mri - gem 'fuubar', '~> 2.0.0' gem 'pry-rails' - gem 'coveralls', '~> 0.8.2', require: false + gem 'awesome_print', '~> 1.2.0' + gem 'fuubar', '~> 2.0.0' + gem 'database_cleaner', '~> 1.4.0' - gem 'factory_girl_rails' + gem 'factory_girl_rails', '~> 4.3.0' gem 'rspec-rails', '~> 3.3.0' - gem 'rubocop', '0.28.0', require: false - gem 'spinach-rails' + gem 'spinach-rails', '~> 0.2.1' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) - gem 'minitest', '~> 5.3.0' + gem 'minitest', '~> 5.7.0' # Generate Fake data gem 'ffaker', '~> 2.0.0' @@ -244,30 +260,59 @@ group :development, :test do gem 'poltergeist', '~> 1.6.0' gem 'teaspoon', '~> 1.0.0' - gem 'teaspoon-jasmine' + gem 'teaspoon-jasmine', '~> 2.2.0' - gem 'spring', '~> 1.3.1' - gem 'spring-commands-rspec', '~> 1.0.0' + gem 'spring', '~> 1.3.6' + gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-spinach', '~> 1.0.0' gem 'spring-commands-teaspoon', '~> 0.0.2' + + gem 'rubocop', '~> 0.28.0', require: false + gem 'coveralls', '~> 0.8.2', require: false + gem 'simplecov', '~> 0.10.0', require: false + + gem 'benchmark-ips', require: false end group :test do - gem 'simplecov', require: false gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'email_spec', '~> 1.6.0' gem 'webmock', '~> 1.21.0' - gem 'test_after_commit' + gem 'test_after_commit', '~> 0.2.2' + gem 'sham_rack' end group :production do gem "gitlab_meta", '7.0' end -gem "newrelic_rpm" +gem "newrelic_rpm", '~> 3.9.4.245' +gem 'newrelic-grape' -gem 'octokit', '3.7.0' +gem 'octokit', '~> 3.7.0' -gem "mail_room", "~> 0.4.1" +gem "mail_room", "~> 0.5.2" -gem 'email_reply_parser' +gem 'email_reply_parser', '~> 0.5.8' + +## CI +gem 'activerecord-deprecated_finders', '~> 1.0.3' +gem 'activerecord-session_store', '~> 0.1.0' +gem "nested_form", '~> 0.3.2' + +# Scheduled +gem 'whenever', '~> 0.8.4', require: false + +# OAuth +gem 'oauth2', '~> 1.0.0' + +# Soft deletion +gem "paranoia", "~> 2.0" + +group :development, :test do + gem 'guard-rspec', '~> 4.2.0' + + gem 'rb-fsevent', require: darwin_only('rb-fsevent') + gem 'growl', require: darwin_only('growl') + gem 'rb-inotify', require: linux_only('rb-inotify') +end diff --git a/Gemfile.lock b/Gemfile.lock index 2450b95d973..1dd56cd9c8c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,31 +4,36 @@ GEM CFPropertyList (2.3.1) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.1.11) - actionpack (= 4.1.11) - actionview (= 4.1.11) + actionmailer (4.1.12) + actionpack (= 4.1.12) + actionview (= 4.1.12) mail (~> 2.5, >= 2.5.4) - actionpack (4.1.11) - actionview (= 4.1.11) - activesupport (= 4.1.11) + actionpack (4.1.12) + actionview (= 4.1.12) + activesupport (= 4.1.12) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.11) - activesupport (= 4.1.11) + actionview (4.1.12) + activesupport (= 4.1.12) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.11) - activesupport (= 4.1.11) + activemodel (4.1.12) + activesupport (= 4.1.12) builder (~> 3.1) - activerecord (4.1.11) - activemodel (= 4.1.11) - activesupport (= 4.1.11) + activerecord (4.1.12) + activemodel (= 4.1.12) + activesupport (= 4.1.12) arel (~> 5.0.0) + activerecord-deprecated_finders (1.0.4) + activerecord-session_store (0.1.1) + actionpack (>= 4.0.0, < 5) + activerecord (>= 4.0.0, < 5) + railties (>= 4.0.0, < 5) activeresource (4.0.0) activemodel (~> 4.0) activesupport (~> 4.0) rails-observers (~> 0.1.1) - activesupport (4.1.11) + activesupport (4.1.12) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -37,33 +42,37 @@ GEM acts-as-taggable-on (3.5.0) activerecord (>= 3.2, < 5) addressable (2.3.8) - annotate (2.6.0) - activerecord (>= 2.3.0) - rake (>= 0.8.7) + after_commit_queue (1.1.0) + rails (>= 3.0) + annotate (2.6.10) + activerecord (>= 3.2, <= 4.3) + rake (~> 10.4) arel (5.0.1.20140414130214) asana (0.0.6) activeresource (>= 3.2.3) asciidoctor (1.5.2) - ast (2.0.0) - astrolabe (1.3.0) - parser (>= 2.2.0.pre.3, < 3.0) + ast (2.1.0) + astrolabe (1.3.1) + parser (~> 2.2) attr_encrypted (1.3.4) encryptor (>= 1.3.0) attr_required (1.0.0) - autoprefixer-rails (5.1.11) + autoprefixer-rails (5.2.1.2) execjs json awesome_print (1.2.0) - axiom-types (0.0.5) - descendants_tracker (~> 0.0.1) - ice_nine (~> 0.9) - bcrypt (3.1.7) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + bcrypt (3.1.10) + benchmark-ips (2.3.0) better_errors (1.0.1) coderay (>= 1.0.0) erubis (>= 2.6.6) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootstrap-sass (3.3.4.1) + bootstrap-sass (3.3.5) autoprefixer-rails (>= 5.0.0.1) sass (>= 3.2.19) brakeman (3.0.1) @@ -78,9 +87,7 @@ GEM terminal-table (~> 1.4) browser (1.0.0) builder (3.2.2) - byebug (3.2.0) - columnize (~> 0.8) - debugger-linecache (~> 1.2) + byebug (6.0.2) cal-heatmap-rails (0.0.1) capybara (2.4.4) mime-types (>= 1.16) @@ -88,7 +95,7 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - capybara-screenshot (1.0.9) + capybara-screenshot (1.0.11) capybara (>= 1.0, < 3) launchy carrierwave (0.9.0) @@ -98,6 +105,8 @@ GEM celluloid (0.16.0) timers (~> 4.0.0) charlock_holmes (0.6.9.4) + chronic (0.10.2) + chunky_png (1.3.4) cliver (0.3.2) coderay (1.1.0) coercible (1.0.0) @@ -111,8 +120,7 @@ GEM coffee-script-source (1.9.1.1) colored (1.2) colorize (0.5.8) - columnize (0.9.0) - connection_pool (2.1.0) + connection_pool (2.2.0) coveralls (0.8.2) json (~> 1.8) rest-client (>= 1.6.8, < 2) @@ -122,38 +130,37 @@ GEM crack (0.4.2) safe_yaml (~> 1.0.0) creole (0.3.8) - d3_rails (3.5.5) + d3_rails (3.5.6) railties (>= 3.1.0) - daemons (1.1.9) + daemons (1.2.3) database_cleaner (1.4.1) debug_inspector (0.0.2) - debugger-linecache (1.2.0) - default_value_for (3.0.0) + default_value_for (3.0.1) activerecord (>= 3.2.0, < 5.0) - descendants_tracker (0.0.3) - devise (3.2.4) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + devise (3.5.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) + responders thread_safe (~> 0.1) warden (~> 1.2.3) devise-async (0.9.0) devise (~> 3.2) - devise-two-factor (1.0.1) - activemodel + devise-two-factor (2.0.0) activesupport attr_encrypted (~> 1.3.2) - devise (~> 3.2.4) - rails - rotp (~> 1.6.1) + devise (~> 3.5.0) + railties + rotp (~> 2) diff-lcs (1.2.5) - diffy (3.0.3) + diffy (3.0.7) docile (1.1.5) domain_name (0.5.24) unf (>= 0.0.5, < 1.0.0) - doorkeeper (2.1.3) + doorkeeper (2.1.4) railties (>= 3.2) - dotenv (0.9.0) dropzonejs-rails (0.7.1) rails (> 3.1) email_reply_parser (0.5.8) @@ -163,25 +170,25 @@ GEM encryptor (1.3.0) enumerize (0.7.0) activesupport (>= 3.2) - equalizer (0.0.8) + equalizer (0.0.11) erubis (2.7.0) escape_utils (0.2.4) - eventmachine (1.0.4) - excon (0.45.3) - execjs (2.5.2) + eventmachine (1.0.8) + excon (0.45.4) + execjs (2.6.0) expression_parser (0.9.0) factory_girl (4.3.0) activesupport (>= 3.0.0) factory_girl_rails (4.3.0) factory_girl (~> 4.3.0) railties (>= 3.0.0) - faraday (0.8.9) + faraday (0.8.10) multipart-post (~> 1.2.0) - faraday_middleware (0.9.0) - faraday (>= 0.7.4, < 0.9) + faraday_middleware (0.10.0) + faraday (>= 0.7.4, < 0.10) fastercsv (1.5.5) ffaker (2.0.0) - ffi (1.9.8) + ffi (1.9.10) fission (0.5.0) CFPropertyList (~> 2.2) flowdock (0.7.0) @@ -202,11 +209,11 @@ GEM ipaddress (~> 0.5) nokogiri (~> 1.5, >= 1.5.11) opennebula - fog-brightbox (0.7.1) + fog-brightbox (0.9.0) fog-core (~> 1.22) fog-json inflecto (~> 0.0.2) - fog-core (1.30.0) + fog-core (1.32.1) builder excon (~> 0.45) formatador (~> 0.2) @@ -216,7 +223,7 @@ GEM fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) - fog-profitbricks (0.0.3) + fog-profitbricks (0.0.5) fog-core fog-xml nokogiri @@ -227,7 +234,7 @@ GEM fog-sakuracloud (1.0.1) fog-core fog-json - fog-softlayer (0.4.6) + fog-softlayer (0.4.7) fog-core fog-json fog-terremark (0.1.0) @@ -242,28 +249,26 @@ GEM fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) - font-awesome-rails (4.2.0.0) + font-awesome-rails (4.4.0.0) railties (>= 3.2, < 5.0) - foreman (0.63.0) - dotenv (>= 0.7) - thor (>= 0.13.6) + foreman (0.78.0) + thor (~> 0.19.1) formatador (0.2.5) fuubar (2.0.0) rspec (~> 3.0) ruby-progressbar (~> 1.4) gemnasium-gitlab-service (0.2.6) rugged (~> 0.21) - gemojione (2.0.0) + gemojione (2.0.1) json - gherkin-ruby (0.3.1) - racc - github-markup (1.3.1) - posix-spawn (~> 0.3.8) + get_process_mem (0.2.0) + gherkin-ruby (0.3.2) + github-markup (1.3.3) gitlab-flowdock-git-hook (1.0.1) flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json - gitlab-grit (2.7.2) + gitlab-grit (2.7.3) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) mime-types (~> 1.15) @@ -272,9 +277,9 @@ GEM charlock_holmes (~> 0.6.6) escape_utils (~> 0.2.4) mime-types (~> 1.19) - gitlab_emoji (0.1.0) + gitlab_emoji (0.1.1) gemojione (~> 2.0) - gitlab_git (7.2.15) + gitlab_git (7.2.17) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -285,16 +290,16 @@ GEM omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.1) rubyntlm (~> 0.3) - gollum-grit_adapter (0.1.3) + gollum-grit_adapter (1.0.0) gitlab-grit (~> 2.7, >= 2.7.1) - gollum-lib (4.0.2) - github-markup (~> 1.3.1) - gollum-grit_adapter (~> 0.1, >= 0.1.1) + gollum-lib (4.0.3) + github-markup (~> 1.3.3) + gollum-grit_adapter (~> 1.0) nokogiri (~> 1.6.4) - rouge (~> 1.9) + rouge (~> 1.10.1) sanitize (~> 2.1.0) stringex (~> 2.5.1) - gon (5.0.1) + gon (5.0.4) actionpack (>= 2.3.0) json grape (0.6.1) @@ -307,9 +312,22 @@ GEM rack-accept rack-mount virtus (>= 1.0.0) - grape-entity (0.4.2) + grape-entity (0.4.8) activesupport multi_json (>= 1.3.2) + growl (1.0.3) + guard (2.13.0) + formatador (>= 0.2.4) + listen (>= 2.7, <= 4.0) + lumberjack (~> 1.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-rspec (4.2.10) + guard (~> 2.1) + rspec (>= 2.14, < 4.0) haml (4.0.7) tilt haml-rails (0.5.3) @@ -320,24 +338,23 @@ GEM hashie (2.1.2) highline (1.6.21) hike (1.2.3) - hipchat (1.5.0) + hipchat (1.5.2) httparty mimemagic - hitimes (1.2.2) + hitimes (1.2.3) html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) http-cookie (1.0.2) domain_name (~> 0.5) http_parser.rb (0.5.3) - httparty (0.13.3) + httparty (0.13.5) json (~> 1.8) multi_xml (>= 0.5.2) - httpauth (0.2.1) - httpclient (2.5.3.3) + httpclient (2.6.0.1) i18n (0.7.0) ice_cube (0.11.1) - ice_nine (0.10.0) + ice_nine (0.11.1) inflecto (0.0.2) ipaddress (0.8.0) jquery-atwho-rails (1.0.1) @@ -346,58 +363,66 @@ GEM thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) railties (> 3.1, < 5.0) - jquery-turbolinks (2.0.1) + jquery-turbolinks (2.0.2) railties (>= 3.1.0) turbolinks jquery-ui-rails (4.2.1) railties (>= 3.2.16) json (1.8.3) - jwt (0.1.13) - multi_json (>= 1.5) - kaminari (0.15.1) + jwt (1.5.1) + kaminari (0.16.3) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - kgio (2.9.2) + kgio (2.9.3) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - listen (2.10.0) + listen (2.10.1) celluloid (~> 0.16.0) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) + lumberjack (1.0.9) macaddr (1.7.1) systemu (~> 2.6.2) mail (2.6.3) mime-types (>= 1.16, < 3) - mail_room (0.4.1) + mail_room (0.5.2) method_source (0.8.2) mime-types (1.25.1) mimemagic (0.3.0) mini_portile (0.6.2) - minitest (5.3.5) + minitest (5.7.0) mousetrap-rails (1.4.6) multi_json (1.11.2) multi_xml (0.5.5) multipart-post (1.2.0) - mysql2 (0.3.16) + mysql2 (0.3.20) + nenv (0.2.0) + nested_form (0.3.2) net-ldap (0.11) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (2.9.2) netrc (0.10.3) + newrelic-grape (2.0.0) + grape + newrelic_rpm newrelic_rpm (3.9.4.245) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) + notiffany (0.0.7) + nenv (~> 0.1) + shellany (~> 0.0) nprogress-rails (0.1.2.3) oauth (0.4.7) - oauth2 (0.8.1) - faraday (~> 0.8) - httpauth (~> 0.1) - jwt (~> 0.1.4) - multi_json (~> 1.0) + oauth2 (1.0.0) + faraday (>= 0.8, < 0.10) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) rack (~> 1.2) - octokit (3.7.0) + octokit (3.7.1) sawyer (~> 0.6.0, >= 0.5.3) omniauth (1.2.2) hashie (>= 1.2, < 4) @@ -406,34 +431,38 @@ GEM multi_json (~> 1.7) omniauth (~> 1.1) omniauth-oauth (~> 1.0) - omniauth-github (1.1.1) + omniauth-github (1.1.2) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) omniauth-gitlab (1.0.0) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) - omniauth-google-oauth2 (0.2.5) + omniauth-google-oauth2 (0.2.6) omniauth (> 1.0) omniauth-oauth2 (~> 1.1) omniauth-kerberos (0.2.0) omniauth-multipassword timfel-krb5-auth (~> 0.8) - omniauth-multipassword (0.4.1) + omniauth-multipassword (0.4.2) omniauth (~> 1.0) - omniauth-oauth (1.0.1) + omniauth-oauth (1.1.0) oauth omniauth (~> 1.0) - omniauth-oauth2 (1.1.1) - oauth2 (~> 0.8.0) - omniauth (~> 1.0) + omniauth-oauth2 (1.3.1) + oauth2 (~> 1.0) + omniauth (~> 1.2) omniauth-saml (1.4.1) omniauth (~> 1.1) ruby-saml (~> 1.0.0) - omniauth-shibboleth (1.1.1) + omniauth-shibboleth (1.1.2) omniauth (>= 1.0.0) omniauth-twitter (1.0.1) multi_json (~> 1.3) omniauth-oauth (~> 1.0) + omniauth_crowd (2.2.3) + activesupport + nokogiri (>= 1.4.4) + omniauth (~> 1.0) opennebula (4.12.1) json nokogiri @@ -441,7 +470,9 @@ GEM org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) - parser (2.2.0.2) + paranoia (2.1.3) + activerecord (~> 4.0) + parser (2.2.2.6) ast (>= 1.1, < 3.0) pg (0.18.2) poltergeist (1.6.0) @@ -449,60 +480,59 @@ GEM cliver (~> 0.3.1) multi_json (~> 1.0) websocket-driver (>= 0.2.0) - posix-spawn (0.3.9) + posix-spawn (0.3.11) powerpack (0.0.9) - pry (0.9.12.4) - coderay (~> 1.0) - method_source (~> 0.8) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) slop (~> 3.4) - pry-rails (0.3.2) + pry-rails (0.3.4) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) - quiet_assets (1.0.2) + quiet_assets (1.0.3) railties (>= 3.1, < 5.0) - racc (1.4.12) rack (1.5.5) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.3.0) rack rack-cors (0.2.9) - rack-mini-profiler (0.9.0) + rack-mini-profiler (0.9.7) rack (>= 1.1.3) rack-mount (0.8.3) rack (>= 1.0.0) - rack-oauth2 (1.0.8) + rack-oauth2 (1.0.10) activesupport (>= 2.3) attr_required (>= 0.0.5) - httpclient (>= 2.2.0.2) + httpclient (>= 2.4) multi_json (>= 1.3.6) rack (>= 1.1) - rack-protection (1.5.1) + rack-protection (1.5.3) rack rack-test (0.6.3) rack (>= 1.0) - rails (4.1.11) - actionmailer (= 4.1.11) - actionpack (= 4.1.11) - actionview (= 4.1.11) - activemodel (= 4.1.11) - activerecord (= 4.1.11) - activesupport (= 4.1.11) + rails (4.1.12) + actionmailer (= 4.1.12) + actionpack (= 4.1.12) + actionview (= 4.1.12) + activemodel (= 4.1.12) + activerecord (= 4.1.12) + activesupport (= 4.1.12) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.11) + railties (= 4.1.12) sprockets-rails (~> 2.0) rails-observers (0.1.2) activemodel (~> 4.0) - railties (4.1.11) - actionpack (= 4.1.11) - activesupport (= 4.1.11) + railties (4.1.12) + actionpack (= 4.1.12) + activesupport (= 4.1.12) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.0.0) - raindrops (0.13.0) + raindrops (0.15.0) rake (10.4.2) raphael-rails (2.1.2) - rb-fsevent (0.9.4) + rb-fsevent (0.9.5) rb-inotify (0.9.5) ffi (>= 0.5.0) rbvmomi (1.8.2) @@ -517,10 +547,10 @@ GEM actionpack (~> 4) redis-rack (~> 1.5.0) redis-store (~> 1.1.0) - redis-activesupport (4.0.0) + redis-activesupport (4.1.1) activesupport (~> 4) redis-store (~> 1.1.0) - redis-namespace (1.5.1) + redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) redis-rack (1.5.0) rack (~> 1.5) @@ -531,32 +561,35 @@ GEM redis-store (~> 1.1.0) redis-store (1.1.6) redis (>= 2.2) - request_store (1.0.5) + request_store (1.2.0) rerun (0.10.0) listen (~> 2.7, >= 2.7.3) + responders (1.1.2) + railties (>= 3.2, < 4.2) rest-client (1.8.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) rinku (1.7.3) - rotp (1.6.1) - rouge (1.9.1) - rqrcode (0.4.2) + rotp (2.1.1) + rouge (1.10.1) + rqrcode (0.7.0) + chunky_png rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) rspec (3.3.0) rspec-core (~> 3.3.0) rspec-expectations (~> 3.3.0) rspec-mocks (~> 3.3.0) - rspec-core (3.3.1) + rspec-core (3.3.2) rspec-support (~> 3.3.0) - rspec-expectations (3.3.0) + rspec-expectations (3.3.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.3.0) - rspec-mocks (3.3.0) + rspec-mocks (3.3.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.3.0) - rspec-rails (3.3.2) + rspec-rails (3.3.3) actionpack (>= 3.0, < 4.3) activesupport (>= 3.0, < 4.3) railties (>= 3.0, < 4.3) @@ -571,16 +604,18 @@ GEM powerpack (~> 0.0.6) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) - ruby-progressbar (1.7.1) + ruby-fogbugz (0.2.1) + crack (~> 0.4) + ruby-progressbar (1.7.5) ruby-saml (1.0.0) nokogiri (>= 1.5.10) uuid (~> 2.3) - ruby2ruby (2.1.3) + ruby2ruby (2.1.4) ruby_parser (~> 3.1) sexp_processor (~> 4.0) ruby_parser (3.5.0) sexp_processor (~> 4.1) - rubyntlm (0.5.0) + rubyntlm (0.5.2) rubypants (0.2.0) rugged (0.22.2) safe_yaml (1.0.4) @@ -604,7 +639,10 @@ GEM select2-rails (3.5.9.3) thor (~> 0.14) settingslogic (2.0.9) - sexp_processor (4.4.5) + sexp_processor (4.6.0) + sham_rack (1.3.6) + rack + shellany (0.0.1) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) sidekiq (3.3.0) @@ -623,19 +661,20 @@ GEM json (~> 1.8) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) - sinatra (1.4.4) + sinatra (1.4.6) rack (~> 1.4) rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) + tilt (>= 1.3, < 3) six (0.2.0) slack-notifier (1.0.0) - slim (2.0.2) + slim (2.0.3) temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) slop (3.6.0) - spinach (0.8.7) - colorize (= 0.5.8) - gherkin-ruby (>= 0.3.1) + spinach (0.8.10) + colorize + gherkin-ruby (>= 0.3.2) + json spinach-rails (0.2.1) capybara (>= 2.0.0) railties (>= 3) @@ -666,31 +705,32 @@ GEM railties (>= 3.2.5, < 5) teaspoon-jasmine (2.2.0) teaspoon (>= 1.0.0) - temple (0.6.7) + temple (0.6.10) term-ansicolor (1.3.2) tins (~> 1.0) - terminal-table (1.4.5) - test_after_commit (0.2.2) - thin (1.6.1) - daemons (>= 1.0.9) - eventmachine (>= 1.0.0) - rack (>= 1.0.0) + terminal-table (1.5.2) + test_after_commit (0.2.7) + activerecord (>= 3.2) + thin (1.6.3) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0) + rack (~> 1.0) thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - timers (4.0.1) + timers (4.0.4) hitimes timfel-krb5-auth (0.8.3) - tinder (1.9.3) + tinder (1.9.4) eventmachine (~> 1.0) - faraday (~> 0.8) + faraday (~> 0.8.9) faraday_middleware (~> 0.9) hashie (>= 1.0, < 3) json (~> 1.8.0) mime-types (~> 1.19) multi_json (~> 1.7) twitter-stream (~> 0.1) - tins (1.5.4) + tins (1.6.0) trollop (2.1.2) turbolinks (2.5.3) coffee-rails @@ -700,35 +740,39 @@ GEM simple_oauth (~> 0.1.4) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (2.3.2) + uglifier (2.3.3) execjs (>= 0.3.0) json (>= 1.8.0) underscore-rails (1.4.4) unf (0.1.4) unf_ext unf_ext (0.0.7.1) - unicorn (4.6.3) + unicorn (4.8.3) kgio (~> 2.6) rack raindrops (~> 0.7) - unicorn-worker-killer (0.4.2) + unicorn-worker-killer (0.4.3) + get_process_mem (~> 0) unicorn (~> 4) uuid (2.3.8) macaddr (~> 1.0) version_sorter (2.0.0) - virtus (1.0.1) - axiom-types (~> 0.0.5) + virtus (1.0.5) + axiom-types (~> 0.1) coercible (~> 1.0) - descendants_tracker (~> 0.0.1) - equalizer (~> 0.0.7) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) warden (1.2.3) rack (>= 1.0) webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) - websocket-driver (0.5.4) + websocket-driver (0.6.2) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) + whenever (0.8.4) + activesupport (>= 2.3.4) + chronic (>= 0.6.3) wikicloth (0.8.1) builder expression_parser @@ -740,141 +784,161 @@ PLATFORMS ruby DEPENDENCIES - RedCloth - ace-rails-ap + RedCloth (~> 4.2.9) + ace-rails-ap (~> 2.0.1) + activerecord-deprecated_finders (~> 1.0.3) + activerecord-session_store (~> 0.1.0) acts-as-taggable-on (~> 3.4) - addressable - annotate (~> 2.6.0.beta2) + addressable (~> 2.3.8) + after_commit_queue + annotate (~> 2.6.0) asana (~> 0.0.6) asciidoctor (~> 1.5.2) - attr_encrypted (= 1.3.4) - awesome_print - better_errors - binding_of_caller + attr_encrypted (~> 1.3.4) + awesome_print (~> 1.2.0) + benchmark-ips + better_errors (~> 1.0.1) + binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.0) - brakeman + brakeman (= 3.0.1) browser (~> 1.0.0) byebug cal-heatmap-rails (~> 0.0.1) capybara (~> 2.4.0) capybara-screenshot (~> 1.0.0) - carrierwave - charlock_holmes - coffee-rails - colored + carrierwave (~> 0.9.0) + charlock_holmes (~> 0.6.9.4) + coffee-rails (~> 4.1.0) + colored (~> 1.2) + colorize (~> 0.5.8) coveralls (~> 0.8.2) creole (~> 0.3.6) d3_rails (~> 3.5.5) database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) - devise (= 3.2.4) - devise-async (= 0.9.0) - devise-two-factor + devise (~> 3.5.2) + devise-async (~> 0.9.0) + devise-two-factor (~> 2.0.0) diffy (~> 3.0.3) - doorkeeper (= 2.1.3) - dropzonejs-rails - email_reply_parser + doorkeeper (~> 2.1.3) + dropzonejs-rails (~> 0.7.1) + email_reply_parser (~> 0.5.8) email_spec (~> 1.6.0) - enumerize - factory_girl_rails + enumerize (~> 0.7.0) + factory_girl_rails (~> 4.3.0) ffaker (~> 2.0.0) fog (~> 1.25.0) font-awesome-rails (~> 4.2) foreman fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) - github-markup + github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.2.15) + gitlab_git (~> 7.2.17) gitlab_meta (= 7.0) - gitlab_omniauth-ldap (= 1.2.1) + gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.0.2) gon (~> 5.0.0) grape (~> 0.6.1) grape-entity (~> 0.4.2) - haml-rails + growl + guard-rspec (~> 4.2.0) + haml-rails (~> 0.5.3) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) - httparty + httparty (~> 0.13.3) jquery-atwho-rails (~> 1.0.0) - jquery-rails (= 3.1.3) - jquery-scrollto-rails - jquery-turbolinks - jquery-ui-rails - kaminari (~> 0.15.1) - letter_opener - mail_room (~> 0.4.1) - minitest (~> 5.3.0) - mousetrap-rails - mysql2 - newrelic_rpm - nprogress-rails - octokit (= 3.7.0) + jquery-rails (~> 3.1.3) + jquery-scrollto-rails (~> 1.4.3) + jquery-turbolinks (~> 2.0.1) + jquery-ui-rails (~> 4.2.1) + kaminari (~> 0.16.3) + letter_opener (~> 1.1.2) + mail_room (~> 0.5.2) + minitest (~> 5.7.0) + mousetrap-rails (~> 1.4.6) + mysql2 (~> 0.3.16) + nested_form (~> 0.3.2) + newrelic-grape + newrelic_rpm (~> 3.9.4.245) + nprogress-rails (~> 0.1.2.3) + oauth2 (~> 1.0.0) + octokit (~> 3.7.0) omniauth (~> 1.2.2) - omniauth-bitbucket - omniauth-github - omniauth-gitlab - omniauth-google-oauth2 - omniauth-kerberos + omniauth-bitbucket (~> 0.0.2) + omniauth-github (~> 1.1.1) + omniauth-gitlab (~> 1.0.0) + omniauth-google-oauth2 (~> 0.2.5) + omniauth-kerberos (~> 0.2.0) omniauth-saml (~> 1.4.0) - omniauth-shibboleth - omniauth-twitter - org-ruby (= 0.9.12) - pg + omniauth-shibboleth (~> 1.1.1) + omniauth-twitter (~> 1.0.1) + omniauth_crowd + org-ruby (~> 0.9.12) + paranoia (~> 2.0) + pg (~> 0.18.2) poltergeist (~> 1.6.0) pry-rails - quiet_assets (~> 1.0.1) + quiet_assets (~> 1.0.2) rack-attack (~> 4.3.0) - rack-cors - rack-mini-profiler + rack-cors (~> 0.2.9) + rack-mini-profiler (~> 0.9.0) rack-oauth2 (~> 1.0.5) - rails (= 4.1.11) + rails (= 4.1.12) raphael-rails (~> 2.1.2) + rb-fsevent + rb-inotify rdoc (~> 3.6) redcarpet (~> 3.3.2) - redis-rails - request_store + redis-rails (~> 4.0.0) + request_store (~> 1.2.0) rerun (~> 0.10.0) - rqrcode-rails3 + rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.3.0) - rubocop (= 0.28.0) + rubocop (~> 0.28.0) + ruby-fogbugz (~> 0.2.1) sanitize (~> 2.0) sass-rails (~> 4.0.5) - sdoc - seed-fu + sdoc (~> 0.3.20) + seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) - settingslogic + settingslogic (~> 2.0.9) + sham_rack shoulda-matchers (~> 2.8.0) - sidekiq (~> 3.3) - sidetiq (= 0.6.3) - simplecov - sinatra - six + sidekiq (= 3.3.0) + sidetiq (~> 0.6.3) + simplecov (~> 0.10.0) + sinatra (~> 1.4.4) + six (~> 0.2.0) slack-notifier (~> 1.0.0) - slim - spinach-rails - spring (~> 1.3.1) - spring-commands-rspec (~> 1.0.0) + slim (~> 2.0.2) + spinach-rails (~> 0.2.1) + spring (~> 1.3.6) + spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.0.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 2.12.3) - stamp - state_machine - task_list (= 1.0.2) + stamp (~> 0.5.0) + state_machine (~> 1.2.0) + task_list (~> 1.0.2) teaspoon (~> 1.0.0) - teaspoon-jasmine - test_after_commit - thin + teaspoon-jasmine (~> 2.2.0) + test_after_commit (~> 0.2.2) + thin (~> 1.6.1) tinder (~> 1.9.2) turbolinks (~> 2.5.0) - uglifier + uglifier (~> 2.3.2) underscore-rails (~> 1.4.4) - unf - unicorn (~> 4.6.3) - unicorn-worker-killer - version_sorter - virtus + unf (~> 0.1.4) + unicorn (~> 4.8.2) + unicorn-worker-killer (~> 0.4.2) + version_sorter (~> 2.0.0) + virtus (~> 1.0.1) webmock (~> 1.21.0) + whenever (~> 0.8.4) wikicloth (= 0.8.1) + +BUNDLED WITH + 1.10.6 diff --git a/Procfile b/Procfile index 18fd9eb3d92..08880b9c425 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ 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 mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q common -q default +worker: bundle exec sidekiq -q post_receive -q mailer -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/README.md b/README.md index 52e12bb66ad..91855b42d29 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # GitLab -[![build status](https://ci.gitlab.com/projects/1/status.png?ref=master)](https://ci.gitlab.com/projects/1?ref=master) +[![build status](https://ci.gitlab.com/projects/1/status.svg?ref=master)](https://ci.gitlab.com/projects/1?ref=master) [![Build Status](https://semaphoreci.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/400484/shields_badge.svg)](https://semaphoreci.com/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) -[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) +[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.svg?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) ## Canonical source diff --git a/VERSION b/VERSION index 939cbc3ea74..a2264f05f50 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.0.pre +8.1.0.pre diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt new file mode 100755 index 00000000000..a9b845ed1d4 --- /dev/null +++ b/app/assets/fonts/OFL.txt @@ -0,0 +1,92 @@ +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. +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 + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf new file mode 100755 index 00000000000..cb89a2d171e Binary files /dev/null and b/app/assets/fonts/SourceSansPro-Black.ttf differ diff --git a/app/assets/fonts/SourceSansPro-BlackItalic.ttf b/app/assets/fonts/SourceSansPro-BlackItalic.ttf new file mode 100755 index 00000000000..c719243c0d6 Binary files /dev/null and b/app/assets/fonts/SourceSansPro-BlackItalic.ttf differ diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf new file mode 100755 index 00000000000..5d65c93242f Binary files /dev/null and b/app/assets/fonts/SourceSansPro-Bold.ttf differ diff --git a/app/assets/fonts/SourceSansPro-BoldItalic.ttf b/app/assets/fonts/SourceSansPro-BoldItalic.ttf new file mode 100755 index 00000000000..d20dd0c5eca Binary files /dev/null and b/app/assets/fonts/SourceSansPro-BoldItalic.ttf differ diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf new file mode 100755 index 00000000000..bb4176c6fff Binary files /dev/null and b/app/assets/fonts/SourceSansPro-ExtraLight.ttf differ diff --git a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf b/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf new file mode 100755 index 00000000000..2c34f3b8dc4 Binary files /dev/null and b/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf differ diff --git a/app/assets/fonts/SourceSansPro-Italic.ttf b/app/assets/fonts/SourceSansPro-Italic.ttf new file mode 100755 index 00000000000..e5a1a86e631 Binary files /dev/null and b/app/assets/fonts/SourceSansPro-Italic.ttf differ diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf new file mode 100755 index 00000000000..83a0a336661 Binary files /dev/null and b/app/assets/fonts/SourceSansPro-Light.ttf differ diff --git a/app/assets/fonts/SourceSansPro-LightItalic.ttf b/app/assets/fonts/SourceSansPro-LightItalic.ttf new file mode 100755 index 00000000000..88a6778d24f Binary files /dev/null and b/app/assets/fonts/SourceSansPro-LightItalic.ttf differ diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf new file mode 100755 index 00000000000..44486cdc670 Binary files /dev/null and b/app/assets/fonts/SourceSansPro-Regular.ttf differ diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf new file mode 100755 index 00000000000..86b00c067e0 Binary files /dev/null and b/app/assets/fonts/SourceSansPro-Semibold.ttf differ diff --git a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf b/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf new file mode 100755 index 00000000000..2c5ad3008c3 Binary files /dev/null and b/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf differ diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg new file mode 100644 index 00000000000..0e05674e840 Binary files /dev/null and b/app/assets/images/ci/arch.jpg differ diff --git a/app/assets/images/ci/favicon.ico b/app/assets/images/ci/favicon.ico new file mode 100644 index 00000000000..9663d4d00b9 Binary files /dev/null and b/app/assets/images/ci/favicon.ico differ diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif new file mode 100644 index 00000000000..2fcb8f2da0d Binary files /dev/null and b/app/assets/images/ci/loader.gif differ diff --git a/app/assets/images/ci/no_avatar.png b/app/assets/images/ci/no_avatar.png new file mode 100644 index 00000000000..752d26adba7 Binary files /dev/null and b/app/assets/images/ci/no_avatar.png differ diff --git a/app/assets/images/ci/rails.png b/app/assets/images/ci/rails.png new file mode 100644 index 00000000000..d5edc04e65f Binary files /dev/null and b/app/assets/images/ci/rails.png differ diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png new file mode 100644 index 00000000000..65d29e3fd89 Binary files /dev/null and b/app/assets/images/ci/service_sample.png differ diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index 777c62dc1b7..63803747413 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -1,7 +1,7 @@ class @Activities constructor: -> Pager.init 20, true - $(".event_filter_link").bind "click", (event) => + $(".event-filter .btn").bind "click", (event) => event.preventDefault() @toggleFilter($(event.currentTarget)) @reloadActivities() @@ -12,7 +12,7 @@ class @Activities toggleFilter: (sender) -> - sender.parent().toggleClass "active" + sender.toggleClass "active" event_filters = $.cookie("event_filter") filter = sender.attr("id").split("_")[0] if event_filters diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee new file mode 100644 index 00000000000..3ab3ba66754 --- /dev/null +++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee @@ -0,0 +1,67 @@ +class @BlobFileDropzone + constructor: (form, method) -> + form_dropzone = form.find('.dropzone') + Dropzone.autoDiscover = false + dropzone = form_dropzone.dropzone( + autoDiscover: false + autoProcessQueue: false + url: form.attr('action') + # Rails uses a hidden input field for PUT + # http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails + method: method + clickable: true + uploadMultiple: false + paramName: "file" + maxFilesize: gon.max_file_size or 10 + parallelUploads: 1 + maxFiles: 1 + addRemoveLinks: true + previewsContainer: '.dropzone-previews' + headers: + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + + init: -> + this.on 'addedfile', (file) -> + $('.dropzone-alerts').html('').hide() + commit_message = form.find('#commit_message')[0] + + if /^Upload/.test(commit_message.placeholder) + commit_message.placeholder = 'Upload ' + file.name + + return + + this.on 'removedfile', (file) -> + commit_message = form.find('#commit_message')[0] + + if /^Upload/.test(commit_message.placeholder) + commit_message.placeholder = 'Upload new file' + + return + + this.on 'success', (header, response) -> + window.location.href = response.filePath + return + + this.on 'maxfilesexceeded', (file) -> + @removeFile file + return + + this.on 'sending', (file, xhr, formData) -> + formData.append('commit_message', form.find('#commit_message').val()) + return + + # Override behavior of adding error underneath preview + error: (file, errorMessage) -> + stripped = $("
").html(errorMessage).text(); + $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show() + @removeFile file + return + ) + + submitButton = form.find('#submit-all')[0] + submitButton.addEventListener 'click', (e) -> + e.preventDefault() + e.stopPropagation() + alert "Please select a file" if dropzone[0].dropzone.getQueuedFiles().length == 0 + dropzone[0].dropzone.processQueue() + return false diff --git a/app/assets/javascripts/ci/Chart.min.js b/app/assets/javascripts/ci/Chart.min.js new file mode 100644 index 00000000000..ab635881087 --- /dev/null +++ b/app/assets/javascripts/ci/Chart.min.js @@ -0,0 +1,39 @@ +var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= +Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& +isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? +b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? +0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== +a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e
+
+ {
+ "build_id": 2,
+ "build_name":"rspec_linux"
+ "build_status": "failed",
+ "build_started_at": "2014-05-05T18:01:02.563Z",
+ "build_finished_at": "2014-05-05T18:01:07.611Z",
+ "project_id": 1,
+ "project_name": "Brightbox \/ Brightbox Cli",
+ "gitlab_url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli",
+ "ref": "master",
+ "sha": "a26cf5de9ed9827746d4970872376b10d9325f40",
+ "before_sha": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "push_data": {
+ "before": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "after": "a26cf5de9ed9827746d4970872376b10d9325f40",
+ "ref": "refs\/heads\/master",
+ "user_id": 1,
+ "user_name": "Administrator",
+ "project_id": 5,
+ "repository": {
+ "name": "Brightbox Cli",
+ "url": "dzaporozhets@localhost:brightbox\/brightbox-cli.git",
+ "description": "Voluptatibus quae error consectetur voluptas dolores vel excepturi possimus.",
+ "homepage": "http:\/\/localhost:3000\/brightbox\/brightbox-cli"
+ },
+ "commits": [
+ {
+ "id": "a26cf5de9ed9827746d4970872376b10d9325f40",
+ "message": "Release v1.2.2",
+ "timestamp": "2014-04-22T16:46:42+03:00",
+ "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/a26cf5de9ed9827746d4970872376b10d9325f40",
+ "author": {
+ "name": "Paul Thornthwaite",
+ "email": "tokengeek@gmail.com"
+ }
+ },
+ {
+ "id": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "message": "Fix server user data update\n\nIncorrect condition was being used so Base64 encoding option was having\nopposite effect from desired.",
+ "timestamp": "2014-04-11T18:17:26+03:00",
+ "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "author": {
+ "name": "Paul Thornthwaite",
+ "email": "tokengeek@gmail.com"
+ }
+ }
+ ],
+ "total_commits_count": 2,
+ "ci_yaml_file":"rspec_linux:\r\n script: ls\r\n"
+ }
+ }
+
+
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
new file mode 100644
index 00000000000..a634ae5dfda
--- /dev/null
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -0,0 +1,7 @@
+%ul.center-top-menu.commit-ci-menu
+ = nav_link(path: 'commit#show') do
+ = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+ Changes
+ = nav_link(path: 'commit#ci') do
+ = link_to ci_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+ Builds
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 3f645b81397..fbf0a9ec0c3 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -38,10 +38,17 @@
- @commit.parents.each do |parent|
= link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent)
+- if @ci_commit
+ .pull-right
+ = link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do
+ = ci_status_icon(@ci_commit)
+ build:
+ = @ci_commit.status
+
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
-.commit-box
+.commit-box.gray-content-block.middle-block
%h3.commit-title
= gfm escape_once(@commit.title)
- if @commit.description.present?
diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml
new file mode 100644
index 00000000000..f4382e88046
--- /dev/null
+++ b/app/views/projects/commit/ci.html.haml
@@ -0,0 +1,62 @@
+- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+= render "projects/commits/header_title"
+= render "commit_box"
+= render "ci_menu"
+
+- if @ci_project && current_user && can?(current_user, :manage_builds, @project)
+ .pull-right
+ - if @ci_commit.builds.running_or_pending.any?
+ = link_to "Cancel", cancel_ci_project_commits_path(@ci_project, @ci_commit), class: 'btn btn-sm btn-danger'
+
+
+- if @ci_commit.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - @ci_commit.yaml_errors.split(",").each do |error|
+ %li= error
+
+- unless @ci_commit.ci_yaml_file
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+- @ci_commit.refs.each do |ref|
+ .gray-content-block.second-block
+ Builds for #{ref}
+ - if @ci_commit.duration_for_ref(ref) > 0
+ %small.pull-right
+ %i.fa.fa-time
+ #{time_interval_in_words @ci_commit.duration_for_ref(ref)}
+
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @ci_project && @ci_project.coverage_enabled?
+ %th Coverage
+ %th
+ = render partial: "projects/builds/build", collection: @ci_commit.builds_without_retry.for_ref(ref), controls: true
+
+- if @ci_commit.retried_builds.any?
+ %h3
+ Retried builds
+
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Ref
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @ci_project && @ci_project.coverage_enabled?
+ %th Coverage
+ %th
+ = render partial: "projects/builds/build", collection: @ci_commit.retried_builds, ref: true
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 60b112e67d4..30a3973828f 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"
+= render "projects/commits/header_title"
= render "commit_box"
+= render "ci_menu" if @ci_commit
= render "projects/diffs/diffs", diffs: @diffs, project: @project
= render "projects/notes/notes_with_form", view: params[:view]
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 74f8d8b15cf..cddd5aa3a83 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -4,7 +4,11 @@
- notes = commit.notes
- note_count = notes.user.count
-= cache [project.id, commit.id, note_count] do
+- ci_commit = project.ci_commit(commit.sha)
+- cache_key = [project.path_with_namespace, commit.id, note_count]
+- cache_key.push(ci_commit.status) if ci_commit
+
+= cache(cache_key) do
%li.commit.js-toggle-container
.commit-row-title
%strong.str-truncated
@@ -13,6 +17,10 @@
%a.text-expander.js-toggle-button ...
.pull-right
+ - if ci_commit
+ = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do
+ = ci_status_icon(ci_commit)
+
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
.notes_count
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index e3d8cd0fdd5..a849bf84698 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,22 +1,18 @@
-%ul.nav.nav-tabs
+%ul.center-top-menu
= nav_link(controller: [:commit, :commits]) do
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do
- = icon("history")
+ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
%span.badge= number_with_delimiter(@repository.commit_count)
= nav_link(controller: :compare) do
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) do
- = icon("exchange")
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
Compare
= nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do
- = icon("code-fork")
Branches
%span.badge.js-totalbranch-count= @repository.branches.size
= nav_link(controller: :tags) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
- = icon("tags")
Tags
%span.badge.js-totaltags-count= @repository.tags.length
diff --git a/app/views/projects/commits/_header_title.html.haml b/app/views/projects/commits/_header_title.html.haml
new file mode 100644
index 00000000000..e4385893dd9
--- /dev/null
+++ b/app/views/projects/commits/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Commits", project_commits_path(@project))
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 55054a31977..2dd99cc8215 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -1,26 +1,28 @@
- page_title "Commits", @ref
+= render "header_title"
= 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 "head"
-.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'commits'
+.gray-content-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'commits'
-.commits-feed-holder.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
+ .commits-feed-holder.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
- - 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")
+ - 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")
-%ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
%div{id: dom_id(@project)}
#commits-list= render "commits", project: @project
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 3019893d12c..efc25eda26b 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,5 +1,5 @@
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
- .clearfix.append-bottom-20
+ .clearfix
- if params[:to] && params[:from]
= link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has_tooltip', title: 'Switch base of comparison'}
.form-group
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index d1e579a2ede..02be5a2d07f 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -1,9 +1,8 @@
- page_title "Compare"
+= render "projects/commits/header_title"
= render "projects/commits/head"
-%h3.page-title
- Compare View
-%p.slead
+.gray-content-block
Compare branches, tags or commit ranges.
%br
Fill input field with commit id like
@@ -14,4 +13,5 @@
%br
Changes are shown from the version in the first field to the version in the second field.
-= render "form"
+.prepend-top-20
+ = render "form"
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 3670dd5c13b..39755efd2fd 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -1,16 +1,17 @@
- page_title "#{params[:from]}...#{params[:to]}"
+= render "projects/commits/header_title"
= render "projects/commits/head"
-%h3.page-title
- Compare View
-= render "form"
+.gray-content-block
+ = render "form"
- if @commits.present?
- = render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs, project: @project
+ .prepend-top-20
+ = render "projects/commits/commit_list"
+ = render "projects/diffs/diffs", diffs: @diffs, project: @project
- else
- .light-well
+ .light-well.prepend-top-20
.center
%h4
There isn't anything to compare.
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 30943f49bba..4f1965bfb39 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,21 +1,26 @@
- if params[:view] == 'parallel'
- fluid_layout true
-.prepend-top-20.append-bottom-20
- .pull-right
+- diff_files = safe_diff_files(diffs)
+
+.gray-content-block.second-block
+ .inline-parallel-buttons
.btn-group
= inline_diff_btn
= parallel_diff_btn
- = render 'projects/diffs/stats', diffs: diffs
-
-- diff_files = safe_diff_files(diffs)
+ = render 'projects/diffs/stats', diff_files: diff_files
- if diff_files.count < diffs.size
= render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count
.files
- diff_files.each_with_index do |diff_file, index|
- = render 'projects/diffs/file', diff_file: diff_file, i: index, project: project
+ - diff_commit = commit_for_diff(diff_file.diff)
+ - blob = project.repository.blob_for_diff(diff_commit, diff_file.diff)
+ - next unless blob
+
+ = render 'projects/diffs/file', i: index, project: project,
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob
- if @diff_timeout
.alert.alert-danger
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 99ee23a1ddc..4617b188150 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,24 +1,18 @@
-- blob = project.repository.blob_for_diff(@commit, diff_file.diff)
-- return unless blob
-- blob_diff_path = namespace_project_blob_diff_path(project.namespace, project, tree_join(@commit.id, diff_file.file_path))
-.diff-file{id: "diff-#{i}", data: {blob_diff_path: blob_diff_path }}
+.diff-file{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)}
.diff-header{id: "file-path-#{hexdigest(diff_file.new_path || diff_file.old_path)}"}
- - if diff_file.deleted_file
- %span="#{diff_file.old_path} deleted"
-
- .diff-btn-group
- - if @commit.parent_ids.present?
- = view_file_btn(@commit.parent_id, diff_file, project)
- - elsif diff_file.diff.submodule?
+ - if diff_file.diff.submodule?
%span
- submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path)
= submodule_link(submodule_item, @commit.id, project.repository)
- else
%span
- - if diff_file.renamed_file
+ - if diff_file.deleted_file
+ = "#{diff_file.old_path} deleted"
+ - elsif diff_file.renamed_file
= "#{diff_file.old_path} renamed to #{diff_file.new_path}"
- else
= diff_file.new_path
+
- if diff_file.mode_changed?
%span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
@@ -28,12 +22,12 @@
%i.fa.fa-comments
- - if @merge_request && @merge_request.source_project
+ - if editable_diff?(diff_file)
= edit_blob_link(@merge_request.source_project,
@merge_request.source_branch, diff_file.new_path,
after: ' ', from_merge_request_id: @merge_request.id)
- = view_file_btn(@commit.id, diff_file, project)
+ = view_file_btn(diff_commit.id, diff_file, project)
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 1625930615a..ea2a3e01277 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -2,37 +2,35 @@
.commit-stat-summary
Showing
= link_to '#', class: 'js-toggle-button' do
- %strong #{pluralize(diffs.count, "changed file")}
- - if current_controller?(:commit)
- - unless @commit.has_zero_stats?
- with
- %strong.cgreen #{@commit.stats.additions} additions
- and
- %strong.cred #{@commit.stats.deletions} deletions
+ %strong #{pluralize(diff_files.count, "changed file")}
+ with
+ %strong.cgreen #{diff_files.sum(&:added_lines)} additions
+ and
+ %strong.cred #{diff_files.sum(&:removed_lines)} deletions
.file-stats.js-toggle-content.hide
- %ul.bordered-list
- - diffs.each_with_index do |diff, i|
+ %ul
+ - diff_files.each_with_index do |diff_file, i|
%li
- - if diff.deleted_file
+ - if diff_file.deleted_file
%span.deleted-file
%a{href: "#diff-#{i}"}
%i.fa.fa-minus
- = diff.old_path
- - elsif diff.renamed_file
+ = diff_file.old_path
+ - elsif diff_file.renamed_file
%span.renamed-file
%a{href: "#diff-#{i}"}
%i.fa.fa-minus
- = diff.old_path
+ = diff_file.old_path
→
- = diff.new_path
- - elsif diff.new_file
+ = diff_file.new_path
+ - elsif diff_file.new_file
%span.new-file
%a{href: "#diff-#{i}"}
%i.fa.fa-plus
- = diff.new_path
+ = diff_file.new_path
- else
%span.edit-file
%a{href: "#diff-#{i}"}
%i.fa.fa-adjust
- = diff.new_path
+ = diff_file.new_path
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index e8e65d87f47..90dce739992 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,10 +1,11 @@
+- @blank_container = true
+
.project-edit-container
.project-edit-errors
.project-edit-content
- %div
- %h3.page-title
+ .panel.panel-default
+ .panel-heading
Project settings
- %hr
.panel-body
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal fieldset-form" }, authenticity_token: true do |f|
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index e577d35d560..e06454fd148 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,10 +1,11 @@
-- if current_user && can?(current_user, :download_code, @project)
- = render 'shared/no_ssh'
- = render 'shared/no_password'
-
+.alert_holder
+ - if current_user && can?(current_user, :download_code, @project)
+ = render 'shared/no_ssh'
+ = render 'shared/no_password'
+
= render "home_panel"
-.center.light-well
+.gray-content-block.center
%h3.page-title
The repository for this project is empty
%p
@@ -15,38 +16,39 @@
file to this project.
.prepend-top-20
-%h3.page-title
- Command line instructions
-%div.git-empty
- %fieldset
- %h5 Git global setup
- %pre.light-well
- :preserve
- git config --global user.name "#{git_user_name}"
- git config --global user.email "#{git_user_email}"
+.empty_wrapper
+ %h3.page-title-empty
+ Command line instructions
+ %div.git-empty
+ %fieldset
+ %h5 Git global setup
+ %pre.light-well
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.light-well
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- cd #{@project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- git push -u origin master
+ %fieldset
+ %h5 Create a new repository
+ %pre.light-well
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ git push -u origin master
- %fieldset
- %h5 Existing folder or Git repository
- %pre.light-well
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git add .
- git commit
- git push -u origin master
+ %fieldset
+ %h5 Existing folder or Git repository
+ %pre.light-well
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git add .
+ git commit
+ git push -u origin master
-- if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index b7a2ed68e25..cd5f3a5d39e 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -10,21 +10,22 @@
- group.each do |namespace|
.col-md-2.col-sm-3
- if fork = namespace.find_fork_of(@project)
- .thumbnail.fork-exists-thumbnail
+ .fork-thumbnail
= link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do
- = image_tag namespace_icon(namespace, 200)
+ = image_tag namespace_icon(namespace, 100)
.caption
- %h4=namespace.human_name
- %p
- = namespace.path
+ %strong
+ = namespace.human_name
+ %div.text-primary
+ Already forked
+
- else
- .thumbnail.fork-thumbnail
+ .fork-thumbnail
= link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do
- = image_tag namespace_icon(namespace, 200)
+ = image_tag namespace_icon(namespace, 100)
.caption
- %h4=namespace.human_name
- %p
- = namespace.path
+ %strong
+ = namespace.human_name
%p.light
Fork is a copy of a project repository.
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 9383df13305..bbfaf422a82 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -3,3 +3,7 @@
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
= link_to 'Commits', commits_namespace_project_graph_path
+ - if @project.gitlab_ci?
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/graphs/_header_title.html.haml b/app/views/projects/graphs/_header_title.html.haml
new file mode 100644
index 00000000000..1e2f61cd22b
--- /dev/null
+++ b/app/views/projects/graphs/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Graphs", namespace_project_graph_path(@project.namespace, @project, current_ref))
diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml
new file mode 100644
index 00000000000..4f69cc64f7c
--- /dev/null
+++ b/app/views/projects/graphs/ci.html.haml
@@ -0,0 +1,7 @@
+- page_title "Continuous Integration", "Graphs"
+= render "header_title"
+= render 'head'
+#charts.ci-charts
+ = render 'projects/graphs/ci/builds'
+ = render 'projects/graphs/ci/build_times'
+= render 'projects/graphs/ci/overall'
diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml
new file mode 100644
index 00000000000..c3c2f572414
--- /dev/null
+++ b/app/views/projects/graphs/ci/_build_times.haml
@@ -0,0 +1,21 @@
+%fieldset
+ %legend
+ Commit duration in minutes for last 30 commits
+
+ %canvas#build_timesChart.padded{width: 800, height: 300}
+
+:javascript
+ var data = {
+ labels : #{@charts[:build_times].labels.to_json},
+ datasets : [
+ {
+ fillColor : "#4A3",
+ strokeColor : "rgba(151,187,205,1)",
+ pointColor : "rgba(151,187,205,1)",
+ pointStrokeColor : "#fff",
+ data : #{@charts[:build_times].build_times.to_json}
+ }
+ ]
+ }
+ var ctx = $("#build_timesChart").get(0).getContext("2d");
+ new Chart(ctx).Line(data,{"scaleOverlay": true});
diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml
new file mode 100644
index 00000000000..1b0039fb834
--- /dev/null
+++ b/app/views/projects/graphs/ci/_builds.haml
@@ -0,0 +1,41 @@
+%fieldset
+ %legend
+ Builds chart for last week
+ (#{date_from_to(Date.today - 7.days, Date.today)})
+
+ %canvas#weekChart.padded{width: 800, height: 200}
+
+%fieldset
+ %legend
+ Builds chart for last month
+ (#{date_from_to(Date.today - 30.days, Date.today)})
+
+ %canvas#monthChart.padded{width: 800, height: 300}
+
+%fieldset
+ %legend Builds chart for last year
+ %canvas#yearChart.padded{width: 800, height: 400}
+
+- [:week, :month, :year].each do |scope|
+ :javascript
+ var data = {
+ labels : #{@charts[scope].labels.to_json},
+ datasets : [
+ {
+ fillColor : "rgba(220,220,220,0.5)",
+ strokeColor : "rgba(220,220,220,1)",
+ pointColor : "rgba(220,220,220,1)",
+ pointStrokeColor : "#EEE",
+ data : #{@charts[scope].total.to_json}
+ },
+ {
+ fillColor : "#4A3",
+ strokeColor : "rgba(151,187,205,1)",
+ pointColor : "rgba(151,187,205,1)",
+ pointStrokeColor : "#fff",
+ data : #{@charts[scope].success.to_json}
+ }
+ ]
+ }
+ var ctx = $("##{scope}Chart").get(0).getContext("2d");
+ new Chart(ctx).Line(data,{"scaleOverlay": true});
diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml
new file mode 100644
index 00000000000..9550d719471
--- /dev/null
+++ b/app/views/projects/graphs/ci/_overall.haml
@@ -0,0 +1,22 @@
+- ci_project = @project.gitlab_ci_project
+%fieldset
+ %legend Overall
+ %p
+ Total:
+ %strong= pluralize ci_project.builds.count(:all), 'build'
+ %p
+ Successful:
+ %strong= pluralize ci_project.builds.success.count(:all), 'build'
+ %p
+ Failed:
+ %strong= pluralize ci_project.builds.failed.count(:all), 'build'
+
+ %p
+ Success ratio:
+ %strong
+ #{success_ratio(ci_project.builds.success, ci_project.builds.failed)}%
+
+ %p
+ Commits covered:
+ %strong
+ = ci_project.commits.count(:all)
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index a357736bf52..112be875b6b 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -1,4 +1,5 @@
-- page_title "Commit statistics"
+- page_title "Commits", "Graphs"
+= render "header_title"
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'graphs_commits'
= render 'head'
@@ -31,61 +32,55 @@
%div
%p.slead
Commits per day of month
- %canvas#month-chart{width: 800, height: 400}
+ %canvas#month-chart
.row
.col-md-6
%div
%p.slead
Commits per day hour (UTC)
- %canvas#hour-chart{width: 800, height: 400}
+ %canvas#hour-chart
.col-md-6
%div
%p.slead
Commits per weekday
- %canvas#weekday-chart{width: 800, height: 400}
+ %canvas#weekday-chart
:coffeescript
- data = {
- labels : #{@commits_per_time.keys.to_json},
- datasets : [{
- fillColor : "rgba(220,220,220,0.5)",
- strokeColor : "rgba(220,220,220,1)",
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data : #{@commits_per_time.values.to_json}
- }]
+ responsiveChart = (selector, data) ->
+ options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false }
+
+ # get selector by context
+ ctx = selector.get(0).getContext("2d")
+ # pointing parent container to make chart.js inherit its width
+ container = $(selector).parent()
+
+ generateChart = ->
+ selector.attr('width', $(container).width())
+ new Chart(ctx).Bar(data, options)
+
+ # enabling auto-resizing
+ $(window).resize( generateChart )
+
+ generateChart()
+
+ chartData = (keys, values) ->
+ data = {
+ labels : keys,
+ datasets : [{
+ fillColor : "rgba(220,220,220,0.5)",
+ strokeColor : "rgba(220,220,220,1)",
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
+ data : values
+ }]
}
- ctx = $("#hour-chart").get(0).getContext("2d");
- new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2})
+ hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json})
+ responsiveChart($('#hour-chart'), hourData)
- data = {
- labels : #{@commits_per_week_days.keys.to_json},
- datasets : [{
- fillColor : "rgba(220,220,220,0.5)",
- strokeColor : "rgba(220,220,220,1)",
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data : #{@commits_per_week_days.values.to_json}
- }]
- }
+ dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json})
+ responsiveChart($('#weekday-chart'), dayData)
- ctx = $("#weekday-chart").get(0).getContext("2d");
- new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2})
-
- data = {
- labels : #{@commits_per_month.keys.to_json},
- datasets : [{
- fillColor : "rgba(220,220,220,0.5)",
- strokeColor : "rgba(220,220,220,1)",
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data : #{@commits_per_month.values.to_json}
- }]
- }
-
- ctx = $("#month-chart").get(0).getContext("2d");
- new Chart(ctx).Bar(data, {"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2})
+ monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json})
+ responsiveChart($('#month-chart'), monthData)
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index ecdd0eaf52f..bd342911e49 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,4 +1,5 @@
-- page_title "Contributor statistics"
+- page_title "Contributors", "Graphs"
+= render "header_title"
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'graphs'
= render 'head'
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 39fe0fc1c4f..06886d215a3 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -3,8 +3,12 @@
.center
%h2
%i.fa.fa-spinner.fa-spin
- Import in progress.
- %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)}
+ - if @project.forked?
+ Forking in progress.
+ - else
+ Import in progress.
+ - unless @project.forked?
+ %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)}
%p Please wait while we import the repository for you. Refresh at will.
:javascript
new ProjectImport();
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index f61ae957208..d4a98eca473 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -7,21 +7,24 @@
= render 'shared/show_aside'
+.gray-content-block.second-block
+ .row
+ .col-md-9
+ .votes-holder.pull-right
+ #votes= render 'votes/votes_block', votable: @issue
+ .participants
+ %span= pluralize(@participants.count, 'participant')
+ - @participants.each do |participant|
+ = link_to_member(@project, participant, name: false, size: 24)
+ .col-md-3
+ %span.slead.has_tooltip{title: 'Cross-project reference'}
+ = cross_project_reference(@project, @issue)
+
.row
%section.col-md-9
- .votes-holder.pull-right
- #votes= render 'votes/votes_block', votable: @issue
- .participants
- %span= pluralize(@participants.count, 'participant')
- - @participants.each do |participant|
- = link_to_member(@project, participant, name: false, size: 24)
.voting_notes#notes= render 'projects/notes/notes_with_form'
%aside.col-md-3
.issuable-affix
- .clearfix
- %span.slead.has_tooltip{title: 'Cross-project reference'}
- = cross_project_reference(@project, @issue)
- %hr
.context
= render 'shared/issuable/context', issuable: @issue
diff --git a/app/views/projects/issues/_header_title.html.haml b/app/views/projects/issues/_header_title.html.haml
new file mode 100644
index 00000000000..99f03549c44
--- /dev/null
+++ b/app/views/projects/issues/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project))
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index b6910c8f796..55ce912829d 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -41,4 +41,4 @@
= issue.task_status
.pull-right.issue-updated-at
- %small updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
+ %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index 5d243adb5fe..a3399c57aa2 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,9 +1,8 @@
-.panel.panel-default
- %ul.well-list.issues-list
- = render @issues
- - if @issues.blank?
- %li
- .nothing-here-block No issues to show
+%ul.content-list.issues-list
+ = render @issues
+ - if @issues.blank?
+ %li
+ .nothing-here-block No issues to show
- if @issues.present?
.pull-right
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index d06225f5488..d6260ab2900 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,10 +1,12 @@
- page_title "Issues"
+= render "header_title"
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues")
-.append-bottom-10
- .pull-right
+.project-issuable-filter
+ .controls
.pull-left
- if current_user
.hidden-xs.pull-left
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index da6edd5c2d2..153447baa1b 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1,2 +1,4 @@
- page_title "New Issue"
+= render "header_title"
+
= render "form"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index e7b14e7582c..5cb814c9ea8 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,15 +1,18 @@
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
+= render "header_title"
+
.issue
.issue-details.issuable-details
- %h4.page-title
+ .page-title
.issue-box{ class: issue_box_class(@issue) }
- if @issue.closed?
Closed
- else
Open
- Issue ##{@issue.iid}
- %small.creator
- · created by #{link_to_member(@project, @issue.author)}
+ %span.issue-id Issue ##{@issue.iid}
+ %span.creator
+ · created by #{link_to_member(@project, @issue.author, size: 24)}
+ ·
= time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago')
- if @issue.updated_at != @issue.created_at
%span
@@ -32,18 +35,17 @@
= icon('pencil-square-o')
Edit
- %hr
- %h2.issue-title
- = gfm escape_once(@issue.title)
- %div
- - if @issue.description.present?
- .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''}
- .wiki
- = preserve do
- = markdown(@issue.description)
- %textarea.hidden.js-task-list-field
- = @issue.description
+ .gray-content-block.middle-block
+ %h2.issue-title
+ = gfm escape_once(@issue.title)
+ %div
+ - if @issue.description.present?
+ .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''}
+ .wiki
+ = preserve do
+ = markdown(@issue.description)
+ %textarea.hidden.js-task-list-field
+ = @issue.description
- %hr
.issue-discussion
= render 'projects/issues/discussion'
diff --git a/app/views/projects/labels/_header_title.html.haml b/app/views/projects/labels/_header_title.html.haml
new file mode 100644
index 00000000000..abe28da483b
--- /dev/null
+++ b/app/views/projects/labels/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Labels", namespace_project_labels_path(@project.namespace, @project))
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 645402667fd..bc4ab0ca27c 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,4 +1,6 @@
- page_title "Edit", @label.name, "Labels"
+= render "header_title"
+
%h3
Edit label
%span.light #{@label.name}
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index d44fe486212..97175f8232b 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,14 +1,16 @@
- page_title "Labels"
-- if can? current_user, :admin_label, @project
- = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do
- New label
-%h3.page-title
- Labels
-%hr
+= render "header_title"
+
+.gray-content-block.top-block
+ - if can? current_user, :admin_label, @project
+ = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do
+ New label
+ .oneline
+ Labels can be applied to issues and merge requests.
.labels
- if @labels.present?
- %ul.bordered-list.manage-labels-list
+ %ul.content-list.manage-labels-list
= render @labels
= paginate @labels, theme: 'gitlab'
- else
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index b3ef17025c3..342ad4f3f95 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,4 +1,6 @@
- page_title "New Label"
+= render "header_title"
+
%h3 New label
.back-link
= link_to namespace_project_labels_path(@project.namespace, @project) do
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index f855dfec321..38e66c3828b 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -7,18 +7,21 @@
= render 'shared/show_aside'
+.gray-content-block.second-block
+ .row
+ .col-md-9
+ .votes-holder.pull-right
+ #votes= render 'votes/votes_block', votable: @merge_request
+ = render "projects/merge_requests/show/participants"
+ .col-md-3
+ %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
+ = cross_project_reference(@project, @merge_request)
+
.row
%section.col-md-9
- .votes-holder.pull-right
- #votes= render 'votes/votes_block', votable: @merge_request
- = render "projects/merge_requests/show/participants"
= render "projects/notes/notes_with_form"
%aside.col-md-3
.issuable-affix
- .clearfix
- %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
- = cross_project_reference(@project, @merge_request)
- %hr
.context
= render 'shared/issuable/context', issuable: @merge_request
diff --git a/app/views/projects/merge_requests/_header_title.html.haml b/app/views/projects/merge_requests/_header_title.html.haml
new file mode 100644
index 00000000000..669a9b06bdf
--- /dev/null
+++ b/app/views/projects/merge_requests/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project))
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 0bcd543fee7..25e4e8ba80d 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -14,11 +14,6 @@
%span
%i.fa.fa-ban
CLOSED
- - else
- %span.hidden-xs.hidden-sm
- %span.label-branch<
- %i.fa.fa-code-fork
- %span= merge_request.target_branch
- note_count = merge_request.mr_and_commit_notes.user.count
- if merge_request.assignee
@@ -48,4 +43,4 @@
= merge_request.task_status
.pull-right.hidden-xs
- %small updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
+ %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index b8a0ca9a42f..d86707b3d97 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -1,9 +1,8 @@
-.panel.panel-default
- %ul.well-list.mr-list
- = render @merge_requests
- - if @merge_requests.blank?
- %li
- .nothing-here-block No merge requests to show
+%ul.content-list.mr-list
+ = render @merge_requests
+ - if @merge_requests.blank?
+ %li
+ .nothing-here-block No merge requests to show
- if @merge_requests.present?
.pull-right
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 7709330611a..452006162db 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -37,7 +37,7 @@
%h4 Compare failed
%p We can't compare selected branches. It may be because of huge diff. Please try again or select different branches.
- else
- .light-well
+ .light-well.append-bottom-10
.center
%h4
There isn't anything to merge.
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 76f44211dac..6244d3ba0b4 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -1,10 +1,11 @@
%h3.page-title
New merge request
%p.slead
+ - source_title, target_title = format_mr_branch_names(@merge_request)
From
- %strong.label-branch #{@merge_request.source_project_namespace}:#{@merge_request.source_branch}
+ %strong.label-branch #{source_title}
%span into
- %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch}
+ %strong.label-branch #{target_title}
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
@@ -18,15 +19,13 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.nav.nav-tabs.merge-request-tabs
+ %ul.merge-request-tabs
%li.commits-tab
= link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do
- = icon('history')
Commits
%span.badge= @commits.size
%li.diffs-tab.active
= link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do
- = icon('list-alt')
Changes
%span.badge= @diffs.size
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index ec1838eb489..0b0f52c653c 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,14 +1,14 @@
- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+= render "header_title"
+
- if params[:view] == 'parallel'
- fluid_layout true
.merge-request{'data-url' => merge_request_path(@merge_request)}
.merge-request-details.issuable-details
= render "projects/merge_requests/show/mr_title"
- %hr
= render "projects/merge_requests/show/mr_box"
- %hr
- .append-bottom-20.mr-source-target
+ .append-bottom-20.mr-source-target.prepend-top-default
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
@@ -39,20 +39,17 @@
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits.present?
- %ul.nav.nav-tabs.merge-request-tabs
+ %ul.merge-request-tabs
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do
- = icon('comments')
Discussion
%span.badge= @merge_request.mr_and_commit_notes.user.count
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#commits', action: 'commits', toggle: 'tab'} do
- = icon('history')
Commits
%span.badge= @commits.size
%li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do
- = icon('list-alt')
Changes
%span.badge= @merge_request.diffs.size
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 7e5cb07f249..303ca0a880b 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -1,4 +1,6 @@
- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+= render "header_title"
+
%h3.page-title
= "Edit merge request ##{@merge_request.iid}"
%hr
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 72fbe2e27a7..086298e5af1 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,7 +1,9 @@
- page_title "Merge Requests"
+= render "header_title"
+
= render 'projects/last_push'
-.append-bottom-10
- .pull-right
+.project-issuable-filter
+ .controls
= render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
- if can? current_user, :create_merge_request, @project
diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml
index 15bd4e2fafd..fc03ee73a3d 100644
--- a/app/views/projects/merge_requests/invalid.html.haml
+++ b/app/views/projects/merge_requests/invalid.html.haml
@@ -1,4 +1,6 @@
- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+= render "header_title"
+
.merge-request
= render "projects/merge_requests/show/mr_title"
= render "projects/merge_requests/show/mr_box"
diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml
index b038a640f67..9fdde80c6d9 100644
--- a/app/views/projects/merge_requests/new.html.haml
+++ b/app/views/projects/merge_requests/new.html.haml
@@ -1,4 +1,6 @@
- page_title "New Merge Request"
+= render "header_title"
+
- if @merge_request.can_be_created
= render 'new_submit'
- else
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index db1575f899a..f18cf96c17d 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -11,12 +11,12 @@
%pre.dark
- if @merge_request.for_fork?
:preserve
- git fetch #{@merge_request.source_project.http_url_to_repo} #{@merge_request.source_branch}
- git checkout -b #{@merge_request.source_project_path}-#{@merge_request.source_branch} FETCH_HEAD
+ git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch}
+ git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD
- else
:preserve
git fetch origin
- git checkout -b #{@merge_request.source_branch} origin/#{@merge_request.source_branch}
+ git checkout -b #{h @merge_request.source_branch} origin/#{h @merge_request.source_branch}
%p
%strong Step 2.
Review the changes locally
@@ -27,18 +27,18 @@
%pre.dark
- if @merge_request.for_fork?
:preserve
- git checkout #{@merge_request.target_branch}
- git merge --no-ff #{@merge_request.source_project_path}-#{@merge_request.source_branch}
+ git checkout #{h @merge_request.target_branch}
+ git merge --no-ff #{h @merge_request.source_project_path}-#{h @merge_request.source_branch}
- else
:preserve
- git checkout #{@merge_request.target_branch}
- git merge --no-ff #{@merge_request.source_branch}
+ git checkout #{h @merge_request.target_branch}
+ git merge --no-ff #{h @merge_request.source_branch}
%p
%strong Step 4.
Push the result of the merge to GitLab
%pre.dark
:preserve
- git push origin #{@merge_request.target_branch}
+ git push origin #{h @merge_request.target_branch}
- unless @merge_request.can_be_merged_by?(current_user)
%p
Note that pushing to GitLab requires write access to this repository.
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index e3cd4346872..b4f62a75890 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,11 +1,12 @@
-%h2.issue-title
- = gfm escape_once(@merge_request.title)
+.gray-content-block.middle-block
+ %h2.issue-title
+ = gfm escape_once(@merge_request.title)
-%div
- - if @merge_request.description.present?
- .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
- .wiki
- = preserve do
- = markdown(@merge_request.description)
- %textarea.hidden.js-task-list-field
- = @merge_request.description
+ %div
+ - if @merge_request.description.present?
+ .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
+ .wiki
+ = preserve do
+ = markdown(@merge_request.description)
+ %textarea.hidden.js-task-list-field
+ = @merge_request.description
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index 9a1eb36fc88..2bf9cd597a4 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,10 +1,11 @@
-%h4.page-title
+.page-title
.issue-box{ class: issue_box_class(@merge_request) }
= @merge_request.state_human_name
- Merge Request ##{@merge_request.iid}
- %small.creator
+ %span.issue-id Merge Request ##{@merge_request.iid}
+ %span.creator
+ ·
+ created by #{link_to_member(@project, @merge_request.author, size: 24)}
·
- created by #{link_to_member(@project, @merge_request.author)}
= time_ago_with_tooltip(@merge_request.created_at)
- if @merge_request.updated_at != @merge_request.created_at
%span
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 4d4e2f68f61..68dda1424cf 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,29 +1,44 @@
- if @merge_request.has_ci?
- .mr-widget-heading
- - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status|
- .ci_widget{class: "ci-#{status}", style: "display:none"}
- - if status == :success
- - status = "passed"
- = icon("check-circle")
- - else
- = icon("circle")
+ - ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha)
+ - if ci_commit
+ - status = ci_commit.status
+ .mr-widget-heading
+ .ci_widget{class: "ci-#{status}"}
+ = ci_status_icon(ci_commit)
%span CI build #{status}
for #{@merge_request.last_commit_short_sha}.
%span.ci-coverage
- = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink"
+ = link_to "View build details", ci_status_path(ci_commit)
- .ci_widget
- = icon("spinner spin")
- Checking CI status for #{@merge_request.last_commit_short_sha}…
+ - else
+ - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
+ - # Remove in later versions when services like Jenkins will set CI status via Commit status API
+ .mr-widget-heading
+ - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status|
+ .ci_widget{class: "ci-#{status}", style: "display:none"}
+ - if status == :success
+ - status = "passed"
+ = icon("check-circle")
+ - else
+ = icon("circle")
+ %span CI build #{status}
+ for #{@merge_request.last_commit_short_sha}.
+ %span.ci-coverage
+ - if ci_build_details_path(@merge_request)
+ = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink"
- .ci_widget.ci-not_found{style: "display:none"}
- = icon("times-circle")
- Could not find CI status for #{@merge_request.last_commit_short_sha}.
+ .ci_widget
+ = icon("spinner spin")
+ Checking CI status for #{@merge_request.last_commit_short_sha}…
- .ci_widget.ci-error{style: "display:none"}
- = icon("times-circle")
- Could not connect to the CI server. Please check your settings and try again.
+ .ci_widget.ci-not_found{style: "display:none"}
+ = icon("times-circle")
+ Could not find CI status for #{@merge_request.last_commit_short_sha}.
- :coffeescript
- $ ->
- merge_request_widget.getCiStatus()
+ .ci_widget.ci-error{style: "display:none"}
+ = icon("times-circle")
+ Could not connect to the CI server. Please check your settings and try again.
+
+ :coffeescript
+ $ ->
+ merge_request_widget.getCiStatus()
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index d22dfa085b8..f223f687def 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -20,7 +20,7 @@
The changes were merged into
%span.label-branch= @merge_request.target_branch
You can remove the source branch now.
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do
%i.fa.fa-times
Remove Source Branch
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index b61e193fc42..613525437ab 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -9,7 +9,7 @@
= label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
= check_box_tag :should_remove_source_branch
Remove source branch
- .accept-control
+ .accept-control.right
= link_to "#", class: "modify-merge-commit-link js-toggle-button" do
= icon('edit')
Modify commit message
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index b93462e5bdf..74e9668052d 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -21,7 +21,7 @@
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
- = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
.hint
.pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}.
diff --git a/app/views/projects/milestones/_header_title.html.haml b/app/views/projects/milestones/_header_title.html.haml
new file mode 100644
index 00000000000..5f4b6982a6d
--- /dev/null
+++ b/app/views/projects/milestones/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Milestones", namespace_project_milestones_path(@project.namespace, @project))
diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index 2ce5358fa74..5e93d55b1fb 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -1,28 +1,34 @@
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) }
- .pull-right
- - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-sm edit-milestone-link btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close"
- = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-sm btn-remove" do
- %i.fa.fa-trash-o
- Remove
+ .row
+ .col-sm-6
+ %strong
+ = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- %h4
- = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- - if milestone.expired? and not milestone.closed?
- %span.cred (Expired)
- %small
- = milestone.expires_at
+ .col-sm-6
+ .pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
= pluralize milestone.issues.count, 'Issue'
-
+ ·
= link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
= pluralize milestone.merge_requests.count, 'Merge Request'
-
- %span.light #{milestone.percent_complete}% complete
.col-sm-6
= milestone_progress_bar(milestone)
+
+ .row
+ .col-sm-6
+ - if milestone.expired? and not milestone.closed?
+ %span.cred (Expired)
+ - if milestone.expires_at
+ %span
+ = milestone.expires_at
+ .col-sm-6
+ - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
+ = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do
+ %i.fa.fa-pencil-square-o
+ Edit
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
+ = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
+ %i.fa.fa-trash-o
+ Remove
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index c09815a212a..e9dc0b77462 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,2 +1,3 @@
- page_title "Edit", @milestone.title, "Milestones"
+= render "header_title"
= render "form"
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 995eecd7830..a207385bd43 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,18 +1,22 @@
- page_title "Milestones"
-.pull-right
- - if can? current_user, :admin_milestone, @project
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do
- %i.fa.fa-plus
- New Milestone
+= render "header_title"
= render 'shared/milestones_filter'
-.milestones
- .panel.panel-default
- %ul.well-list
- = render @milestones
+.gray-content-block
+ .pull-right
+ - if can? current_user, :admin_milestone, @project
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do
+ %i.fa.fa-plus
+ New Milestone
+ .oneline
+ Milestone allows you to group issues and set due date for it
- - if @milestones.blank?
- %li
- .nothing-here-block No milestones to show
+.milestones
+ %ul.content-list
+ = render @milestones
+
+ - if @milestones.blank?
+ %li
+ .nothing-here-block No milestones to show
= paginate @milestones, theme: "gitlab"
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 47149dfea41..9ba9acb6f77 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,2 +1,3 @@
- page_title "New Milestone"
+= render "header_title"
= render "form"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 7b1681df336..4eeb0621e52 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,4 +1,6 @@
- page_title @milestone.title, "Milestones"
+= render "header_title"
+
%h4.page-title
.issue-box{ class: issue_box_class(@milestone) }
- if @milestone.closed?
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 52b5b8b877e..16005161df6 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,4 +1,5 @@
- page_title "Network", @ref
+= header_title project_title(@project, "Network", namespace_project_network_path(@project.namespace, @project, current_ref))
= render "head"
.project-network
.controls
diff --git a/app/views/projects/network/show.json.erb b/app/views/projects/network/show.json.erb
index dc82adcb2c6..122e84b41b2 100644
--- a/app/views/projects/network/show.json.erb
+++ b/app/views/projects/network/show.json.erb
@@ -9,7 +9,7 @@
author: {
name: c.author_name,
email: c.author_email,
- icon: avatar_icon(c.author_email, 20)
+ icon: image_path(avatar_icon(c.author_email, 20))
},
time: c.time,
space: c.spaces.first,
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 636218368cc..bccea21e7a8 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -72,6 +72,11 @@
%i.fa.fa-google
Google Code
+ - if fogbugz_import_enabled?
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+ %i.fa.fa-bug
+ Fogbugz
+
- if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do
%i.fa.fa-git
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 8f7d2e84c70..a0e26f9827e 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,7 +1,7 @@
.note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f|
= note_target_fields(note)
- = render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do
+ = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
= render 'projects/notes/hints'
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 3be8f44b282..d99445da59a 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -7,7 +7,7 @@
= f.hidden_field :noteable_id
= f.hidden_field :noteable_type
- = render layout: 'projects/md_preview', locals: { preview_class: "note-text", referenced_users: true } do
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text'
= render 'projects/notes/hints'
.error-alert
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index de75d44fc41..1638ad6891a 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,11 +1,8 @@
%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: { discussion: note.discussion_id } }
.timeline-entry-inner
.timeline-icon
- - if note.system
- %span= icon('circle')
- - else
- = link_to user_path(note.author) do
- = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: ''
+ = link_to user_path(note.author) do
+ = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: ''
.timeline-content
.note-header
- if note_editable?(note)
@@ -17,14 +14,10 @@
= icon('trash-o')
- unless note.system
- - member = note.project.team.find_member(note.author.id)
- - if member
+ - access = note.project.team.human_max_access(note.author.id)
+ - if access
%span.note-role.label
- = member.human_access
-
- - if note.system
- = link_to user_path(note.author) do
- = image_tag avatar_icon(note.author_email), class: 'avatar s16', alt: ''
+ = access
= link_to_member(note.project, note.author, avatar: false)
@@ -66,7 +59,9 @@
.note-text
= preserve do
= markdown(note.note, {no_header_anchors: true})
- = render 'projects/notes/edit_form', note: note
+ - unless note.system?
+ -# System notes can't be edited
+ = render 'projects/notes/edit_form', note: note
- if note.attachment.url
.note-attachment
diff --git a/app/views/projects/project_members/_header_title.html.haml b/app/views/projects/project_members/_header_title.html.haml
new file mode 100644
index 00000000000..a31f0a37fa2
--- /dev/null
+++ b/app/views/projects/project_members/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Members", namespace_project_project_members_path(@project.namespace, @project))
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index 6914543f6da..189906498cb 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -1,4 +1,6 @@
- page_title "Import members"
+= render "header_title"
+
%h3.page-title
Import members from another project
%p.light
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 162583e4b1d..9a0a824b811 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,30 +1,29 @@
- page_title "Members"
-%h3.page-title
- Users with access to this project
+= render "header_title"
-%p.light
+.gray-content-block.top-block
+ .clearfix.js-toggle-container
+ = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' }
+ = button_tag 'Search', class: 'btn'
+
+ - if can?(current_user, :admin_project_member, @project)
+ %span.pull-right
+ = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do
+ Add members
+ %i.fa.fa-chevron-down
+ = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
+ Import members
+
+ .js-toggle-content.hide.new-group-member-holder
+ = render "new_project_member"
+
+%p.prepend-top-default.light
+ Users with access to this project are listed below.
Read more about project permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
-%hr
-
-.clearfix.js-toggle-container
- = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' }
- = button_tag 'Search', class: 'btn'
-
- - if can?(current_user, :admin_project_member, @project)
- %span.pull-right
- = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do
- Add members
- %i.fa.fa-chevron-down
- = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
- Import members
-
- .js-toggle-content.hide.new-group-member-holder
- = render "new_project_member"
-
= render "team", members: @project_members
- if @group
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
new file mode 100644
index 00000000000..e6b8a2e6fe7
--- /dev/null
+++ b/app/views/projects/runners/_runner.html.haml
@@ -0,0 +1,34 @@
+%li.runner{id: dom_id(runner)}
+ %h4
+ = runner_status_icon(runner)
+ %span.monospace
+ - if @runners.include?(runner)
+ = link_to runner.short_sha, runner_path(runner)
+ %small
+ =link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
+ %i.fa.fa-edit.btn
+ - else
+ = runner.short_sha
+
+ .pull-right
+ - if @runners.include?(runner)
+ - if runner.belongs_to_one_project?
+ = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - else
+ - runner_project = @ci_project.runner_projects.find_by(runner_id: runner)
+ = link_to 'Disable for this project', [:ci, @ci_project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - elsif runner.specific?
+ = form_for [:ci, @ci_project, @ci_project.runner_projects.new] do |f|
+ = f.hidden_field :runner_id, value: runner.id
+ = f.submit 'Enable for this project', class: 'btn btn-sm'
+ .pull-right
+ %small.light
+ \##{runner.id}
+ - if runner.description.present?
+ %p.runner-description
+ = runner.description
+ - if runner.tag_list.present?
+ %p
+ - runner.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
new file mode 100644
index 00000000000..316ea747b14
--- /dev/null
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -0,0 +1,23 @@
+%h3 Shared runners
+
+.bs-callout.bs-callout-warning
+ GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X.
+ %hr
+ - if @ci_project.shared_runners_enabled
+ = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-warning', method: :post do
+ Disable shared runners
+ - else
+ = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-success', method: :post do
+ Enable shared runners
+ for this project
+
+- if @shared_runners_count.zero?
+ This application has no shared runners yet.
+ Please use specific runners or ask administrator to create one
+- else
+ %h4.underlined-title Available shared runners - #{@shared_runners_count}
+ %ul.bordered-list.available-shared-runners
+ = render partial: 'runner', collection: @shared_runners, as: :runner
+ - if @shared_runners_count > 10
+ .light
+ and #{@shared_runners_count - 10} more...
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
new file mode 100644
index 00000000000..c13625c7e49
--- /dev/null
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -0,0 +1,29 @@
+%h3 Specific runners
+
+.bs-callout.help-callout
+ %h4 How to setup a new project specific runner
+
+ %ol
+ %li
+ Install GitLab Runner software.
+ Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
+ %li
+ Specify following URL during runner setup:
+ %code #{ci_root_url(only_path: false)}
+ %li
+ Use the following registration token during setup:
+ %code #{@ci_project.token}
+ %li
+ Start runner!
+
+
+- if @runners.any?
+ %h4.underlined-title Runners activated for this project
+ %ul.bordered-list.activated-specific-runners
+ = render partial: 'runner', collection: @runners, as: :runner
+
+- if @specific_runners.any?
+ %h4.underlined-title Available specific runners
+ %ul.bordered-list.available-specific-runners
+ = render partial: 'runner', collection: @specific_runners, as: :runner
+ = paginate @specific_runners
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
new file mode 100644
index 00000000000..66851d38316
--- /dev/null
+++ b/app/views/projects/runners/edit.html.haml
@@ -0,0 +1,27 @@
+%h4 Runner ##{@runner.id}
+%hr
+= form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label :active, "Active", class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.check_box :active
+ %span.light Paused runners don't accept new builds
+ .form-group
+ = label_tag :token, class: 'control-label' do
+ Token
+ .col-sm-10
+ = f.text_field :token, class: 'form-control', readonly: true
+ .form-group
+ = label_tag :description, class: 'control-label' do
+ Description
+ .col-sm-10
+ = f.text_field :description, class: 'form-control'
+ .form-group
+ = label_tag :tag_list, class: 'control-label' do
+ Tags
+ .col-sm-10
+ = f.text_field :tag_list, class: 'form-control'
+ .help-block You can setup jobs to only use runners with specific tags
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
new file mode 100644
index 00000000000..529fb9c296d
--- /dev/null
+++ b/app/views/projects/runners/index.html.haml
@@ -0,0 +1,25 @@
+.light
+ %p
+ A 'runner' is a process which runs a build.
+ You can setup as many runners as you need.
+ %br
+ Runners can be placed on separate users, servers, and even on your local machine.
+
+ %p Each runner can be in one of the following states:
+ %div
+ %ul
+ %li
+ %span.label.label-success active
+ \- runner is active and can process any new build
+ %li
+ %span.label.label-danger paused
+ \- runner is paused and will not receive any new build
+
+%hr
+
+%p.lead To start serving your builds you can either add specific runners to your project or use shared runners
+.row
+ .col-sm-6
+ = render 'specific_runners'
+ .col-sm-6
+ = render 'shared_runners'
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
new file mode 100644
index 00000000000..ffec495f85a
--- /dev/null
+++ b/app/views/projects/runners/show.html.haml
@@ -0,0 +1,64 @@
+= content_for :title do
+ %h3.project-title
+ Runner ##{@runner.id}
+ .pull-right
+ - if @runner.shared?
+ %span.runner-state.runner-state-shared
+ Shared
+ - else
+ %span.runner-state.runner-state-specific
+ Specific
+
+%table.table
+ %thead
+ %tr
+ %th Property Name
+ %th Value
+ %tr
+ %td
+ Tags
+ %td
+ - @runner.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+ %tr
+ %td
+ Name
+ %td
+ = @runner.name
+ %tr
+ %td
+ Version
+ %td
+ = @runner.version
+ %tr
+ %td
+ Revision
+ %td
+ = @runner.revision
+ %tr
+ %td
+ Platform
+ %td
+ = @runner.platform
+ %tr
+ %td
+ Architecture
+ %td
+ = @runner.architecture
+ %tr
+ %td
+ Description
+ %td
+ = @runner.description
+ %tr
+ %td
+ Last contact
+ %td
+ - if @runner.contacted_at
+ #{time_ago_in_words(@runner.contacted_at)} ago
+ - else
+ Never
+
+
+
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 507f2c7beb0..efa119edd5a 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -11,10 +11,10 @@
= render "home_panel"
-.project-stats
+.project-stats.gray-content-block
%ul.nav.nav-pills
%li
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do
+ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
= pluralize(number_with_delimiter(@project.commit_count), 'commit')
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
@@ -24,7 +24,7 @@
= pluralize(number_with_delimiter(@repository.tag_names.count), 'tag')
%li
- = link_to namespace_project_path(@project.namespace, @project) do
+ = link_to project_files_path(@project) do
= repository_size
- if !prefer_readme? && @repository.readme
@@ -63,21 +63,22 @@
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
-%hr
%section
- if prefer_readme?
- = render 'projects/readme'
+ .project-show-readme
+ = render 'projects/readme'
- else
- = render 'projects/activity'
+ .project-show-activity
+ = render 'projects/activity'
- if current_user
- access = user_max_access_in_project(current_user, @project)
- if access
- %hr
- %p.light
- 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
\ No newline at end of file
+ .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/snippets/_header_title.html.haml b/app/views/projects/snippets/_header_title.html.haml
new file mode 100644
index 00000000000..04f0bbe9853
--- /dev/null
+++ b/app/views/projects/snippets/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Snippets", namespace_project_snippets_path(@project.namespace, @project))
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 945f0084dff..e69f2d99709 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,4 +1,6 @@
- page_title "Edit", @snippet.title, "Snippets"
+= render "header_title"
+
%h3.page-title
Edit snippet
%hr
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 45d4de6a385..3fed2c9949d 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,4 +1,6 @@
- page_title "Snippets"
+= render "header_title"
+
%h3.page-title
Snippets
- if can? current_user, :create_project_snippet, @project
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index e38d95c45e7..67cd69fd215 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,4 +1,6 @@
- page_title "New Snippets"
+= render "header_title"
+
%h3.page-title
New snippet
%hr
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 8cbb813c758..be7d4d486fa 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,4 +1,6 @@
- page_title @snippet.title, "Snippets"
+= render "header_title"
+
%h3.page-title
= @snippet.title
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 28ad272322f..2ca295fc5f3 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -1,13 +1,14 @@
- commit = @repository.commit(tag.target)
%li
- %h4
+ %div
= link_to namespace_project_commits_path(@project.namespace, @project, tag.name), class: "" do
- %i.fa.fa-tag
- = tag.name
+ %strong
+ %i.fa.fa-tag
+ = tag.name
- if tag.message.present?
= strip_gpg_signature(tag.message)
- .pull-right
+ .controls
- if can? current_user, :download_code, @project
= render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs'
- if can?(current_user, :admin_project, @project)
@@ -15,8 +16,7 @@
%i.fa.fa-trash-o
- if commit
- %ul.list-unstyled
- = render 'projects/commits/inline_commit', commit: commit, project: @project
+ = render 'projects/branches/commit', commit: commit, project: @project
- else
%p
Cant find HEAD commit for this tag
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index d4652a47cba..85d76eae3b5 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,21 +1,19 @@
- page_title "Tags"
+= render "projects/commits/header_title"
= render "projects/commits/head"
-%h3.page-title
- Git Tags
+.gray-content-block
- if can? current_user, :push_code, @project
.pull-right
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
%i.fa.fa-add-sign
New tag
-
-%p.light
- Tags give the ability to mark specific points in history as being important
-%hr
+ .oneline
+ Tags give the ability to mark specific points in history as being important
.tags
- unless @tags.empty?
- %ul.bordered-list
+ %ul.content-list
- @tags.each do |tag|
= render 'tag', tag: @repository.find_tag(tag)
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 172fafdeeff..9f5c1be125c 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -1,4 +1,6 @@
- page_title "New Tag"
+= render "projects/commits/header_title"
+
- if @error
.alert.alert-danger
%button{ type: "button", class: "close", "data-dismiss" => "alert"} ×
diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml
index 5048154cb2f..367a87927d7 100644
--- a/app/views/projects/tree/_tree.html.haml
+++ b/app/views/projects/tree/_tree.html.haml
@@ -14,7 +14,7 @@
%small
%i.fa.fa-plus
-%div#tree-content-holder.tree-content-holder
+%div#tree-content-holder.tree-content-holder.prepend-top-20
%table#tree-slider{class: "table_#{@hex_path} tree-table" }
%thead
%tr
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index c9e59428e78..dec4677f830 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -1,4 +1,5 @@
- page_title @path.presence || "Files", @ref
+- header_title project_title(@project, "Files", project_files_path(@project))
= 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")
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
new file mode 100644
index 00000000000..48b3b5c9920
--- /dev/null
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -0,0 +1,14 @@
+%tr
+ %td
+ .clearfix
+ %span.monospace= trigger.token
+
+ %td
+ - if trigger.last_trigger_request
+ #{time_ago_in_words(trigger.last_trigger_request.created_at)} ago
+ - else
+ Never
+
+ %td
+ .pull-right
+ = link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped"
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
new file mode 100644
index 00000000000..17dcb78e256
--- /dev/null
+++ b/app/views/projects/triggers/index.html.haml
@@ -0,0 +1,67 @@
+%h3.page-title
+ Triggers
+
+%p.light
+ Triggers can be used to force a rebuild of a specific branch or tag with an API call.
+
+%hr.clearfix
+
+-if @triggers.any?
+ %table.table
+ %thead
+ %th Token
+ %th Last used
+ %th
+ = render partial: 'trigger', collection: @triggers, as: :trigger
+- else
+ %h4 No triggers
+
+= form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create'), html: { class: 'form-horizontal' } do |f|
+ .clearfix
+ = f.submit "Add Trigger", class: 'btn btn-success pull-right'
+
+%hr.clearfix
+
+-if @triggers.any?
+ %h3
+ Use CURL
+
+ %p.light
+ Copy the token above and set your branch or tag name. This is the reference that will be rebuild.
+
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}
+ %h3
+ Use .gitlab-ci.yml
+
+ %p.light
+ Copy the snippet to
+ %i .gitlab-ci.yml
+ of dependent project.
+ At the end of your build it will trigger this project to rebuilt.
+
+ %pre
+ :plain
+ trigger:
+ type: deploy
+ script:
+ - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}"
+ %h3
+ Pass build variables
+
+ %p.light
+ Add
+ %strong variables[VARIABLE]=VALUE
+ to API request.
+ The value of variable could then be used to distinguish triggered build from normal one.
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F "variables[RUN_NIGHTLY_BUILD]=true" \
+ #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}
diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml
new file mode 100644
index 00000000000..29416a94ff6
--- /dev/null
+++ b/app/views/projects/variables/show.html.haml
@@ -0,0 +1,39 @@
+%h3.page-title
+ Secret Variables
+
+%p.light
+ These variables will be set to environment by the runner and will be hidden in the build log.
+ %br
+ So you can use them for passwords, secret keys or whatever you want.
+
+%hr
+
+
+= nested_form_for @ci_project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
+ - if @project.errors.any?
+ #error_explanation
+ %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:"
+ .alert.alert-error
+ %ul
+ - @ci_project.errors.full_messages.each do |msg|
+ %li= msg
+
+ = f.fields_for :variables do |variable_form|
+ .form-group
+ = variable_form.label :key, 'Key', class: 'control-label'
+ .col-sm-10
+ = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE"
+
+ .form-group
+ = variable_form.label :value, 'Value', class: 'control-label'
+ .col-sm-10
+ = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: ""
+
+ = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right prepend-top-10'
+ %hr
+ %p
+ .clearfix
+ = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right'
+
+ .form-actions
+ = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 904600499ae..05d754adbe5 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -21,7 +21,7 @@
.form-group.wiki-content
= f.label :content, class: 'control-label'
.col-sm-10
- = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
.col-sm-12.hint
.pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}
diff --git a/app/views/projects/wikis/_header_title.html.haml b/app/views/projects/wikis/_header_title.html.haml
new file mode 100644
index 00000000000..408adc36ca6
--- /dev/null
+++ b/app/views/projects/wikis/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, 'Wiki', get_project_wiki_path(@project))
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 788bb8cf1e2..14f25822259 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,8 +1,15 @@
%span.pull-right
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new btn-grouped", "data-toggle" => "modal" do
+ %i.fa.fa-plus
+ New Page
+
- if (@page && @page.persisted?)
- = link_to history_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
Page History
- if can?(current_user, :create_wiki, @project)
- = link_to edit_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
+
+= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index 804a1b52dbe..fffb4eb31ab 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,19 +1,10 @@
-%ul.nav.nav-tabs
+%ul.center-top-menu
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
= nav_link(path: 'wikis#pages') do
- = link_to 'Pages', pages_namespace_project_wikis_path(@project.namespace, @project)
+ = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
= nav_link(path: 'wikis#git_access') do
- = link_to git_access_namespace_project_wikis_path(@project.namespace, @project) do
- %i.fa.fa-download
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
Git Access
-
- - if can?(current_user, :create_wiki, @project)
- .pull-right
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- %i.fa.fa-plus
- New Page
-
-= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 3f1dce1050c..0b709c3695b 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,4 +1,6 @@
- page_title "Edit", @page.title, "Wiki"
+= render "header_title"
+
= render 'nav'
.pull-right
= render 'main_links'
diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml
index ead99412406..c7e490c3cd1 100644
--- a/app/views/projects/wikis/empty.html.haml
+++ b/app/views/projects/wikis/empty.html.haml
@@ -1,4 +1,6 @@
- page_title "Wiki"
+= render "header_title"
+
%h3.page-title Empty page
%hr
.error_message
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 825f2a161c4..6417ef4a38b 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,15 +1,18 @@
- page_title "Git Access", "Wiki"
+= render "header_title"
+
= render 'nav'
-.row
- .col-sm-6
- %h3.page-title
- Git access for
- %strong= @project_wiki.path_with_namespace
+.gray-content-block
+ .row
+ .col-sm-6
+ %h3.page-title
+ Git access for
+ %strong= @project_wiki.path_with_namespace
- .col-sm-6
- = render "shared/clone_panel", project: @project_wiki
+ .col-sm-6
+ = render "shared/clone_panel", project: @project_wiki
-.git-empty
+.git-empty.prepend-top-default
%fieldset
%legend Install Gollum:
%pre.dark
@@ -20,7 +23,7 @@
%pre.dark
:preserve
git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
- cd #{@project_wiki.path}
+ cd #{h @project_wiki.path}
%legend Start Gollum And Edit Locally:
%pre.dark
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 673ec2d20e5..bfbef823b35 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,8 +1,11 @@
-- page_title "History", @page.title, "Wiki"
+- page_title "History", @page.title.capitalize, "Wiki"
+= render "header_title"
+
= render 'nav'
-%h3.page-title
- %span.light History for
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
+.gray-content-block
+ %h3.page-title
+ %span.light History for
+ = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
%table.table
%thead
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 890ff1aed73..03e6a522b25 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,8 +1,11 @@
- page_title "All Pages", "Wiki"
+= render "header_title"
+
= render 'nav'
-%h3.page-title
- All Pages
-%ul.bordered-list
+.gray-content-block
+ %h3.page-title
+ All Pages
+%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
%h4
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 5c4dd7f91ae..55fbf5a8b6e 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,25 +1,28 @@
-- page_title @page.title, "Wiki"
-= render 'nav'
-%h3.page-title
- = @page.title
- = render 'main_links'
+- page_title @page.title.capitalize, "Wiki"
+= render "header_title"
-.wiki-last-edit-by
- Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+= render 'nav'
+
+.gray-content-block
+ = render 'main_links'
+ %h3.page-title
+ = @page.title.capitalize
+
+ .wiki-last-edit-by
+ Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
- if @page.historical?
.warning_message
This is an old version of this page.
- You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", history_namespace_project_wiki_path(@project.namespace, @project, @page)}.
+ You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
-%hr
-.wiki-holder
+.wiki-holder.prepend-top-default
.wiki
= preserve do
= render_wiki_content(@page)
-%hr
-.wiki-last-edit-by
- Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+.gray-content-block.footer-block
+ .wiki-last-edit-by
+ Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 07672359dba..b23b2f0d5eb 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -4,7 +4,7 @@
.input-group-btn
%button{ |
type: 'button', |
- class: "btn btn-sm #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
+ class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
:"data-clone" => project.ssh_url_to_repo, |
:"data-title" => "Add an SSH key to your profile#{code}" + end # Replace the parent `pre` element with the entire highlighted block node.parent.replace(highlighted) diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index 0446cd49f8e..c08811effb2 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -51,7 +51,7 @@ module Gitlab private def urls - Rails.application.routes.url_helpers + Gitlab::Application.routes.url_helpers end def link_class diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb index 9b8e783d16c..d94b104bbf8 100644 --- a/lib/gitlab/o_auth/auth_hash.rb +++ b/lib/gitlab/o_auth/auth_hash.rb @@ -16,16 +16,6 @@ module Gitlab @provider ||= Gitlab::Utils.force_utf8(auth_hash.provider.to_s) end - def info - auth_hash.info - end - - def get_info(key) - value = info.try(key) - Gitlab::Utils.force_utf8(value) if value - value - end - def name @name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}" end @@ -44,9 +34,19 @@ module Gitlab private + def info + auth_hash.info + end + + def get_info(key) + value = info[key] + Gitlab::Utils.force_utf8(value) if value + value + end + def username_and_email @username_and_email ||= begin - username = get_info(:nickname) || get_info(:username) + username = get_info(:username) || get_info(:nickname) email = get_info(:email) username ||= generate_username(email) if email diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 11b0d44f340..6f0d02cafd1 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -1,6 +1,6 @@ module Gitlab class UrlBuilder - include Rails.application.routes.url_helpers + include Gitlab::Application.routes.url_helpers include GitlabRoutingHelper def initialize(type) @@ -23,12 +23,12 @@ module Gitlab def build_issue_url(id) issue = Issue.find(id) - issue_url(issue, host: Gitlab.config.gitlab['url']) + issue_url(issue) end def build_merge_request_url(id) merge_request = MergeRequest.find(id) - merge_request_url(merge_request, host: Gitlab.config.gitlab['url']) + merge_request_url(merge_request) end def build_note_url(id) @@ -37,22 +37,18 @@ module Gitlab namespace_project_commit_url(namespace_id: note.project.namespace, id: note.commit_id, project_id: note.project, - host: Gitlab.config.gitlab['url'], anchor: "note_#{note.id}") elsif note.for_issue? issue = Issue.find(note.noteable_id) issue_url(issue, - host: Gitlab.config.gitlab['url'], anchor: "note_#{note.id}") elsif note.for_merge_request? merge_request = MergeRequest.find(note.noteable_id) merge_request_url(merge_request, - host: Gitlab.config.gitlab['url'], anchor: "note_#{note.id}") elsif note.for_project_snippet? snippet = Snippet.find(note.noteable_id) project_snippet_url(snippet, - host: Gitlab.config.gitlab['url'], anchor: "note_#{note.id}") end end diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 17f89c8beb6..7218a4d2f20 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -124,6 +124,15 @@ server { proxy_connect_timeout 300; proxy_redirect off; + # Do not buffer Git HTTP responses + proxy_buffering off; + + # The following settings only work with NGINX 1.7.11 or newer + # + # # Pass chunked request bodies to gitlab-git-http-server as-is + # proxy_request_buffering off; + # proxy_http_version 1.1; + proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 5ba39fc41a4..7dabfba87e2 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -171,6 +171,15 @@ server { proxy_connect_timeout 300; proxy_redirect off; + # Do not buffer Git HTTP responses + proxy_buffering off; + + # The following settings only work with NGINX 1.7.11 or newer + # + # # Pass chunked request bodies to gitlab-git-http-server as-is + # proxy_request_buffering off; + # proxy_http_version 1.1; + proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Ssl on; diff --git a/lib/support/nginx/gitlab_ci b/lib/support/nginx/gitlab_ci new file mode 100644 index 00000000000..bf05edfd780 --- /dev/null +++ b/lib/support/nginx/gitlab_ci @@ -0,0 +1,29 @@ +# GITLAB CI +server { + listen 80 default_server; # e.g., listen 192.168.1.1:80; + server_name YOUR_CI_SERVER_FQDN; # e.g., server_name source.example.com; + + access_log /var/log/nginx/gitlab_ci_access.log; + error_log /var/log/nginx/gitlab_ci_error.log; + + # expose API to fix runners + location /api { + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + + # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN + resolver 8.8.8.8 8.8.4.4; + proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri; + } + + # redirect all other CI requests + location / { + return 301 $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri; + } + + # adjust this to match the largest build log your runners might submit, + # set to 0 to disable limit + client_max_body_size 10m; +} \ No newline at end of file diff --git a/lib/tasks/ci/.gitkeep b/lib/tasks/ci/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/tasks/ci/cleanup.rake b/lib/tasks/ci/cleanup.rake new file mode 100644 index 00000000000..2f4d11bd942 --- /dev/null +++ b/lib/tasks/ci/cleanup.rake @@ -0,0 +1,8 @@ +namespace :ci do + namespace :cleanup do + desc "GitLab CI | Clean running builds" + task builds: :environment do + Ci::Build.running.update_all(status: 'canceled') + end + end +end diff --git a/lib/tasks/ci/migrate.rake b/lib/tasks/ci/migrate.rake new file mode 100644 index 00000000000..1de664c85e1 --- /dev/null +++ b/lib/tasks/ci/migrate.rake @@ -0,0 +1,87 @@ +namespace :ci do + desc 'GitLab | Import and migrate CI database' + task migrate: :environment do + warn_user_is_not_gitlab + configure_cron_mode + + unless ENV['force'] == 'yes' + puts 'This will remove all CI related data and restore it from the provided backup.' + ask_to_continue + puts '' + end + + # disable CI for time of migration + enable_ci(false) + + # unpack archives + migrate = Ci::Migrate::Manager.new + migrate.unpack + + Rake::Task['ci:migrate:db'].invoke + Rake::Task['ci:migrate:builds'].invoke + Rake::Task['ci:migrate:tags'].invoke + Rake::Task['ci:migrate:services'].invoke + + # enable CI for time of migration + enable_ci(true) + + migrate.cleanup + end + + namespace :migrate do + desc 'GitLab | Import CI database' + task db: :environment do + configure_cron_mode + $progress.puts 'Restoring database ... '.blue + Ci::Migrate::Database.new.restore + $progress.puts 'done'.green + end + + desc 'GitLab | Import CI builds' + task builds: :environment do + configure_cron_mode + $progress.puts 'Restoring builds ... '.blue + Ci::Migrate::Builds.new.restore + $progress.puts 'done'.green + end + + desc 'GitLab | Migrate CI tags' + task tags: :environment do + configure_cron_mode + $progress.puts 'Migrating tags ... '.blue + ::Ci::Migrate::Tags.new.restore + $progress.puts 'done'.green + end + + desc 'GitLab | Migrate CI auto-increments' + task autoincrements: :environment do + c = ActiveRecord::Base.connection + c.tables.select { |t| t.start_with?('ci_') }.each do |table| + result = c.select_one("SELECT id FROM #{table} ORDER BY id DESC LIMIT 1") + if result + ai_val = result['id'].to_i + 1 + puts "Resetting auto increment ID for #{table} to #{ai_val}" + if c.adapter_name == 'PostgreSQL' + c.execute("ALTER SEQUENCE #{table}_id_seq RESTART WITH #{ai_val}") + else + c.execute("ALTER TABLE #{table} AUTO_INCREMENT = #{ai_val}") + end + end + end + end + + desc 'GitLab | Migrate CI services' + task services: :environment do + $progress.puts 'Migrating services ... '.blue + c = ActiveRecord::Base.connection + c.execute("UPDATE ci_services SET type=CONCAT('Ci::', type) WHERE type NOT LIKE 'Ci::%'") + $progress.puts 'done'.green + end + end + + def enable_ci(enabled) + settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults + settings.ci_enabled = enabled + settings.save! + end +end diff --git a/lib/tasks/ci/schedule_builds.rake b/lib/tasks/ci/schedule_builds.rake new file mode 100644 index 00000000000..49435504c67 --- /dev/null +++ b/lib/tasks/ci/schedule_builds.rake @@ -0,0 +1,6 @@ +namespace :ci do + desc "GitLab CI | Clean running builds" + task schedule_builds: :environment do + Ci::Scheduler.new.perform + end +end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 4c73f90bbf2..f20c7f71ba5 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -11,6 +11,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:db:create"].invoke Rake::Task["gitlab:backup:repo:create"].invoke Rake::Task["gitlab:backup:uploads:create"].invoke + Rake::Task["gitlab:backup:builds:create"].invoke backup = Backup::Manager.new backup.pack @@ -30,6 +31,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:db:restore"].invoke unless backup.skipped?("db") Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories") Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads") + Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds") Rake::Task["gitlab:shell:setup"].invoke backup.cleanup @@ -73,6 +75,25 @@ namespace :gitlab do end end + namespace :builds do + task create: :environment do + $progress.puts "Dumping builds ... ".blue + + if ENV["SKIP"] && ENV["SKIP"].include?("builds") + $progress.puts "[SKIPPED]".cyan + else + Backup::Builds.new.dump + $progress.puts "done".green + end + end + + task restore: :environment do + $progress.puts "Restoring builds ... ".blue + Backup::Builds.new.restore + $progress.puts "done".green + end + end + namespace :uploads do task create: :environment do $progress.puts "Dumping uploads ... ".blue diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 2b9688c1b40..66f1ecf385f 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -2,7 +2,7 @@ namespace :gitlab do desc "GitLab | Check the configuration of GitLab and its environment" task check: %w{gitlab:gitlab_shell:check gitlab:sidekiq:check - gitlab:reply_by_email:check + gitlab:incoming_email:check gitlab:ldap:check gitlab:app:check} @@ -278,7 +278,7 @@ namespace :gitlab do fix_and_rerun end end - + def check_uploads print "Uploads directory setup correctly? ... " @@ -331,15 +331,18 @@ namespace :gitlab do end def check_redis_version - print "Redis version >= 2.0.0? ... " + min_redis_version = "2.4.0" + print "Redis version >= #{min_redis_version}? ... " redis_version = run(%W(redis-cli --version)) - if redis_version.try(:match, /redis-cli 2.\d.\d/) || redis_version.try(:match, /redis-cli 3.\d.\d/) + redis_version = redis_version.try(:match, /redis-cli (.*)/) + if redis_version && + (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version)) puts "yes".green else puts "no".red try_fixing_it( - "Update your redis server to a version >= 2.0.0" + "Update your redis server to a version >= #{min_redis_version}" ) for_more_information( "gitlab-public-wiki/wiki/Trouble-Shooting-Guide in section sidekiq" @@ -488,7 +491,7 @@ namespace :gitlab do else puts "wrong or missing hooks".red try_fixing_it( - sudo_gitlab("#{gitlab_shell_path}/bin/create-hooks"), + sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')}"), 'Check the hooks_path in config/gitlab.yml', 'Check your gitlab-shell installation' ) @@ -631,13 +634,13 @@ namespace :gitlab do end - namespace :reply_by_email do + namespace :incoming_email do desc "GitLab | Check the configuration of Reply by email" task check: :environment do warn_user_is_not_gitlab start_checking "Reply by email" - if Gitlab.config.reply_by_email.enabled + if Gitlab.config.incoming_email.enabled check_address_formatted_correctly check_mail_room_config_exists check_imap_authentication @@ -662,12 +665,12 @@ namespace :gitlab do def check_address_formatted_correctly print "Address formatted correctly? ... " - if Gitlab::ReplyByEmail.address_formatted_correctly? + if Gitlab::IncomingEmail.address_formatted_correctly? puts "yes".green else puts "no".red try_fixing_it( - "Make sure that the address in config/gitlab.yml includes the '%{reply_key}' placeholder." + "Make sure that the address in config/gitlab.yml includes the '%{key}' placeholder." ) fix_and_rerun end @@ -676,6 +679,11 @@ namespace :gitlab do def check_initd_configured_correctly print "Init.d configured correctly? ... " + if omnibus_gitlab? + puts 'skipped (omnibus-gitlab has no init script)'.magenta + return + end + path = "/etc/default/gitlab" if File.exist?(path) && File.read(path).include?("mail_room_enabled=true") @@ -686,7 +694,7 @@ namespace :gitlab do "Enable mail_room in the init.d configuration." ) for_more_information( - "doc/reply_by_email/README.md" + "doc/incoming_email/README.md" ) fix_and_rerun end @@ -705,7 +713,7 @@ namespace :gitlab do "Enable mail_room in your Procfile." ) for_more_information( - "doc/reply_by_email/README.md" + "doc/incoming_email/README.md" ) fix_and_rerun end @@ -750,7 +758,7 @@ namespace :gitlab do "Check that the information in config/mail_room.yml is correct" ) for_more_information( - "doc/reply_by_email/README.md" + "doc/incoming_email/README.md" ) fix_and_rerun end @@ -786,7 +794,7 @@ namespace :gitlab do "Check that the information in config/mail_room.yml is correct" ) for_more_information( - "doc/reply_by_email/README.md" + "doc/incoming_email/README.md" ) fix_and_rerun end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 6b1e3716147..9f5852ac613 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -46,43 +46,24 @@ namespace :gitlab do desc "GitLab | Cleanup | Clean repositories" task repos: :environment do warn_user_is_not_gitlab - remove_flag = ENV['REMOVE'] - git_base_path = Gitlab.config.gitlab_shell.repos_path - all_dirs = Dir.glob(git_base_path + '/*') - - global_projects = Project.in_namespace(nil).pluck(:path) - - puts git_base_path.yellow - puts "Looking for global repos to remove... " - - # skip non git repo - all_dirs.select! do |dir| - dir =~ /.git$/ - end - - # skip existing repos - all_dirs.reject! do |dir| - repo_name = File.basename dir - path = repo_name.gsub(/\.git$/, "") - global_projects.include?(path) - end - - all_dirs.each do |dir_path| - if remove_flag - if FileUtils.rm_rf dir_path - puts "Removed...#{dir_path}".red - else - puts "Cannot remove #{dir_path}".red - end - else - puts "Can be removed: #{dir_path}".red + move_suffix = "+orphaned+#{Time.now.to_i}" + repo_root = Gitlab.config.gitlab_shell.repos_path + # Look for global repos (legacy, depth 1) and normal repos (depth 2) + IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find| + find.each_line do |path| + path.chomp! + repo_with_namespace = path. + sub(repo_root, ''). + sub(%r{^/*}, ''). + chomp('.git'). + chomp('.wiki') + next if Project.find_with_namespace(repo_with_namespace) + new_path = path + move_suffix + puts path.inspect + ' -> ' + new_path.inspect + File.rename(path, new_path) end end - - unless remove_flag - puts "To cleanup this directories run this command with REMOVE=true".yellow - end end desc "GitLab | Cleanup | Block users that have been removed in LDAP" diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake new file mode 100644 index 00000000000..39541c0b9c6 --- /dev/null +++ b/lib/tasks/services.rake @@ -0,0 +1,98 @@ +services_template = <<-ERB +# Services + +<% services.each do |service| %> +## <%= service[:title] %> + + +<% unless service[:description].blank? %> +<%= service[:description] %> +<% end %> + + +### Create/Edit <%= service[:title] %> service + +Set <%= service[:title] %> service for a project. +<% unless service[:help].blank? %> + +> <%= service[:help].gsub("\n", ' ') %> + +<% end %> + +``` +PUT /projects/:id/services/<%= service[:dashed_name] %> + +``` + +Parameters: + +<% service[:params].each do |param| %> +- `<%= param[:name] %>` <%= param[:required] ? "(**required**)" : "(optional)" %><%= [" -", param[:description]].join(" ").gsub("\n", '') unless param[:description].blank? %> + +<% end %> + +### Delete <%= service[:title] %> service + +Delete <%= service[:title] %> service for a project. + +``` +DELETE /projects/:id/services/<%= service[:dashed_name] %> + +``` + +### Get <%= service[:title] %> service settings + +Get <%= service[:title] %> service settings for a project. + +``` +GET /projects/:id/services/<%= service[:dashed_name] %> + +``` + +<% end %> +ERB + +namespace :services do + task doc: :environment do + services = Service.available_services_names.map do |s| + service_start = Time.now + klass = "#{s}_service".classify.constantize + + service = klass.new + + service_hash = {} + + service_hash[:title] = service.title + service_hash[:dashed_name] = s.dasherize + service_hash[:description] = service.description + service_hash[:help] = service.help + service_hash[:params] = service.fields.map do |p| + param_hash = {} + + param_hash[:name] = p[:name] + param_hash[:description] = p[:placeholder] || p[:title] + param_hash[:required] = klass.validators_on(p[:name].to_sym).any? do |v| + v.class == ActiveRecord::Validations::PresenceValidator + end + + param_hash + end.sort_by { |p| p[:required] ? 0 : 1 } + + puts "Collected data for: #{service.title}, #{Time.now-service_start}" + service_hash + end + + doc_start = Time.now + doc_path = File.join(Rails.root, 'doc', 'api', 'services.md') + + result = ERB.new(services_template, 0 , '>') + .result(OpenStruct.new(services: services).instance_eval { binding }) + + File.open(doc_path, 'w') do |f| + f.write result + end + + puts "write a new service.md to: #{doc_path.to_s}, #{Time.now-doc_start}" + + end +end diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake index 831746815d7..365ff2defd4 100644 --- a/lib/tasks/spec.rake +++ b/lib/tasks/spec.rake @@ -19,11 +19,20 @@ namespace :spec do run_commands(cmds) end + desc 'GitLab | Rspec | Run benchmark specs' + task :benchmark do + cmds = [ + %W(rake gitlab:setup), + %W(rspec spec --tag @benchmark) + ] + run_commands(cmds) + end + desc 'GitLab | Rspec | Run other specs' task :other do cmds = [ %W(rake gitlab:setup), - %W(rspec spec --tag ~@api --tag ~@feature) + %W(rspec spec --tag ~@api --tag ~@feature --tag ~@benchmark) ] run_commands(cmds) end @@ -33,7 +42,7 @@ desc "GitLab | Run specs" task :spec do cmds = [ %W(rake gitlab:setup), - %W(rspec spec), + %W(rspec spec --tag ~@benchmark), ] run_commands(cmds) end diff --git a/public/ci/build-canceled.svg b/public/ci/build-canceled.svg new file mode 100644 index 00000000000..922e28bf696 --- /dev/null +++ b/public/ci/build-canceled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ci/build-failed.svg b/public/ci/build-failed.svg new file mode 100644 index 00000000000..1aefd3f1761 --- /dev/null +++ b/public/ci/build-failed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ci/build-pending.svg b/public/ci/build-pending.svg new file mode 100644 index 00000000000..536931af84d --- /dev/null +++ b/public/ci/build-pending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ci/build-running.svg b/public/ci/build-running.svg new file mode 100644 index 00000000000..0d71eef3c34 --- /dev/null +++ b/public/ci/build-running.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ci/build-skipped.svg b/public/ci/build-skipped.svg new file mode 100644 index 00000000000..f15507188e0 --- /dev/null +++ b/public/ci/build-skipped.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ci/build-success.svg b/public/ci/build-success.svg new file mode 100644 index 00000000000..43b67e45f42 --- /dev/null +++ b/public/ci/build-success.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ci/build-unknown.svg b/public/ci/build-unknown.svg new file mode 100644 index 00000000000..c72a2f5a7f5 --- /dev/null +++ b/public/ci/build-unknown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ci/favicon.ico b/public/ci/favicon.ico new file mode 100644 index 00000000000..9663d4d00b9 Binary files /dev/null and b/public/ci/favicon.ico differ diff --git a/scripts/ci/prepare_build.sh b/scripts/ci/prepare_build.sh new file mode 100755 index 00000000000..864a683a1bd --- /dev/null +++ b/scripts/ci/prepare_build.sh @@ -0,0 +1,22 @@ +#!/bin/bash +if [ -f /.dockerinit ]; then + export FLAGS=(--deployment --path /cache) + + apt-get update -qq + apt-get install -y -qq nodejs + + wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb + dpkg -i phantomjs_1.9.0-1+b1_amd64.deb + + cp config/database.yml.mysql config/database.yml + sed -i "s/username:.*/username: root/g" config/database.yml + sed -i "s/password:.*/password:/g" config/database.yml + sed -i "s/# socket:.*/host: mysql/g" config/database.yml +else + export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin + + cp config/database.yml.mysql config/database.yml + sed -i "s/username\:.*$/username\: runner/" config/database.yml + sed -i "s/password\:.*$/password\: 'password'/" config/database.yml + sed -i "s/gitlab_ci_test/gitlab_ci_test_$((RANDOM/5000))/" config/database.yml +fi diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb new file mode 100644 index 00000000000..168be20b7a5 --- /dev/null +++ b/spec/benchmarks/models/user_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe User, benchmark: true do + describe '.by_login' do + before do + %w{Alice Bob Eve}.each do |name| + create(:user, + email: "#{name}@gitlab.com", + username: name, + name: name) + end + end + + let(:iterations) { 1000 } + + describe 'using a capitalized username' do + benchmark_subject { User.by_login('Alice') } + + it { is_expected.to iterate_per_second(iterations) } + end + + describe 'using a lowercase username' do + benchmark_subject { User.by_login('alice') } + + it { is_expected.to iterate_per_second(iterations) } + end + + describe 'using a capitalized Email address' do + benchmark_subject { User.by_login('Alice@gitlab.com') } + + it { is_expected.to iterate_per_second(iterations) } + end + + describe 'using a lowercase Email address' do + benchmark_subject { User.by_login('alice@gitlab.com') } + + it { is_expected.to iterate_per_second(iterations) } + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index c40b2c2a583..7168db117d6 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -7,6 +7,21 @@ describe Admin::UsersController do sign_in(admin) end + describe 'POST login_as' do + let(:user) { create(:user) } + + it 'logs admin as another user' do + expect(warden.authenticate(scope: :user)).not_to eq(user) + post :login_as, id: user.username + expect(warden.authenticate(scope: :user)).to eq(user) + end + + it 'redirects user to homepage' do + post :login_as, id: user.username + expect(response).to redirect_to(root_path) + end + end + describe 'DELETE #user with projects' do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } diff --git a/spec/controllers/ci/commits_controller_spec.rb b/spec/controllers/ci/commits_controller_spec.rb new file mode 100644 index 00000000000..cc39ce7687c --- /dev/null +++ b/spec/controllers/ci/commits_controller_spec.rb @@ -0,0 +1,23 @@ +require "spec_helper" + +describe Ci::CommitsController do + describe "GET /status" do + it "returns status of commit" do + commit = FactoryGirl.create :ci_commit + get :status, id: commit.sha, ref_id: commit.ref, project_id: commit.project.id + + expect(response).to be_success + expect(response.code).to eq('200') + JSON.parse(response.body)["status"] == "pending" + end + + it "returns not_found status" do + commit = FactoryGirl.create :ci_commit + get :status, id: commit.sha, ref_id: "deploy", project_id: commit.project.id + + expect(response).to be_success + expect(response.code).to eq('200') + JSON.parse(response.body)["status"] == "not_found" + end + end +end diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb new file mode 100644 index 00000000000..27b11267d2a --- /dev/null +++ b/spec/controllers/import/fogbugz_controller_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' +require_relative 'import_spec_helper' + +describe Import::FogbugzController do + include ImportSpecHelper + + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET status' do + before do + @repo = OpenStruct.new(name: 'vim') + stub_client(valid?: true) + end + + it 'assigns variables' do + @project = create(:project, import_type: 'fogbugz', creator_id: user.id) + stub_client(repos: [@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo]) + end + + it 'does not show already added project' do + @project = create(:project, import_type: 'fogbugz', creator_id: user.id, import_source: 'vim') + stub_client(repos: [@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + end +end diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb index 9c8619722cd..77436958711 100644 --- a/spec/controllers/namespaces_controller_spec.rb +++ b/spec/controllers/namespaces_controller_spec.rb @@ -46,13 +46,11 @@ describe NamespacesController do context "when the project doesn't have public projects" do context "when not signed in" do - it "redirects to the sign in page" do + it "does not redirect to the sign in page" do get :show, id: group.path - - expect(response).to redirect_to(new_user_session_path) + expect(response).not_to redirect_to(new_user_session_path) end end - context "when signed in" do before do sign_in(user) @@ -86,10 +84,10 @@ describe NamespacesController do end context "when the user doesn't have access to the project" do - it "responds with status 404" do + it "redirects to the group's page" do get :show, id: group.path - expect(response.status).to eq(404) + expect(response).to redirect_to(group_path(group)) end end end diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index f54706e3aa3..4fb1473c2d2 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -37,7 +37,7 @@ describe Profiles::TwoFactorAuthsController do context 'with valid pin' do before do - expect(user).to receive(:valid_otp?).with(pin).and_return(true) + expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true) end it 'sets two_factor_enabled' do @@ -63,7 +63,7 @@ describe Profiles::TwoFactorAuthsController do context 'with invalid pin' do before do - expect(user).to receive(:valid_otp?).with(pin).and_return(false) + expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(false) end it 'assigns error' do diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index b643b354073..2a447248b70 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -22,4 +22,30 @@ describe Projects::CompareController do expect(assigns(:diffs).length).to be >= 1 expect(assigns(:commits).length).to be >= 1 end + + describe 'non-existent refs' do + it 'invalid source ref' do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: 'non-existent', + to: ref_to) + + expect(response).to be_success + expect(assigns(:diffs)).to eq([]) + expect(assigns(:commits)).to eq([]) + end + + it 'invalid target ref' do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: ref_from, + to: 'non-existent') + + expect(response).to be_success + expect(assigns(:diffs)).to eq(nil) + expect(assigns(:commits)).to eq(nil) + end + end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 871b9219ca9..76d56bc989d 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -8,28 +8,34 @@ describe Projects::IssuesController do before do sign_in(user) project.team << [user, :developer] - controller.instance_variable_set(:@project, project) end describe "GET #index" do it "returns index" do - get :index, namespace_id: project.namespace.id, project_id: project.id + get :index, namespace_id: project.namespace.path, project_id: project.path expect(response.status).to eq(200) end + it "return 301 if request path doesn't match project path" do + get :index, namespace_id: project.namespace.path, project_id: project.path.upcase + + expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project)) + end + it "returns 404 when issues are disabled" do project.issues_enabled = false project.save - get :index, namespace_id: project.namespace.id, project_id: project.id + get :index, namespace_id: project.namespace.path, project_id: project.path expect(response.status).to eq(404) end it "returns 404 when external issue tracker is enabled" do + controller.instance_variable_set(:@project, project) allow(project).to receive(:default_issues_tracker?).and_return(false) - get :index, namespace_id: project.namespace.id, project_id: project.id + get :index, namespace_id: project.namespace.path, project_id: project.path expect(response.status).to eq(404) end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index d3868c13202..8127efabe6e 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -5,6 +5,7 @@ describe Projects::MilestonesController do let(:user) { create(:user) } let(:milestone) { create(:milestone, project: project) } let(:issue) { create(:issue, project: project, milestone: milestone) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } before do sign_in(user) @@ -14,12 +15,21 @@ describe Projects::MilestonesController do describe "#destroy" do it "should remove milestone" do + merge_request.reload expect(issue.milestone_id).to eq(milestone.id) + delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.id, format: :js expect(response).to be_success + + expect(Event.first.action).to eq(Event::DESTROYED) + expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound) issue.reload expect(issue.milestone_id).to eq(nil) + + merge_request.reload + expect(merge_request.milestone_id).to eq(nil) + # Check system note left for milestone removal last_note = project.issues.find(issue.id).notes[-1].note expect(last_note).to eq('Milestone removed') diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb new file mode 100644 index 00000000000..c114f342021 --- /dev/null +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Projects::RawController do + let(:public_project) { create(:project, :public) } + + describe "#show" do + context 'regular filename' do + let(:id) { 'master/README.md' } + + it 'delivers ASCII file' do + get(:show, + namespace_id: public_project.namespace.to_param, + project_id: public_project.to_param, + id: id) + + expect(response.status).to eq(200) + expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') + expect(response.header['Content-Disposition']). + to eq("inline") + end + end + + context 'image header' do + let(:id) { 'master/files/images/6049019_460s.jpg' } + + it 'set image content type header' do + get(:show, + namespace_id: public_project.namespace.to_param, + project_id: public_project.to_param, + id: id) + + expect(response.status).to eq(200) + expect(response.header['Content-Type']).to eq('image/jpeg') + end + end + end +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 29233e9fae6..21beaf37fce 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -21,6 +21,20 @@ describe ProjectsController do expect(response.body).to include("content='#{content}'") end end + + context "when requested with case sensitive namespace and project path" do + it "redirects to the normalized path for case mismatch" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase + + expect(response).to redirect_to("/#{public_project.path_with_namespace}") + end + + it "loads the page if normalized path matches request path" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path + + expect(response.status).to eq(200) + end + end end describe "POST #toggle_star" do diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb index abbbf6855fc..5a104ae7c99 100644 --- a/spec/controllers/root_controller_spec.rb +++ b/spec/controllers/root_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe RootController do - describe 'GET show' do + describe 'GET index' do context 'with a user' do let(:user) { create(:user) } @@ -10,21 +10,43 @@ describe RootController do allow(subject).to receive(:current_user).and_return(user) end - context 'who has customized their dashboard setting' do + context 'who has customized their dashboard setting for starred projects' do before do user.update_attribute(:dashboard, 'stars') end it 'redirects to their specified dashboard' do - get :show + get :index expect(response).to redirect_to starred_dashboard_projects_path end end + context 'who has customized their dashboard setting for project activities' do + before do + user.update_attribute(:dashboard, 'project_activity') + end + + it 'redirects to the activity list' do + get :index + expect(response).to redirect_to activity_dashboard_path + end + end + + context 'who has customized their dashboard setting for starred project activities' do + before do + user.update_attribute(:dashboard, 'starred_project_activity') + end + + it 'redirects to the activity list' do + get :index + expect(response).to redirect_to activity_dashboard_path(filter: 'starred') + end + end + context 'who uses the default dashboard setting' do it 'renders the default dashboard' do - get :show - expect(response).to render_template 'dashboard/show' + get :index + expect(response).to render_template 'dashboard/projects/index' end end end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 0f9780356b1..af5d043cf02 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -156,14 +156,6 @@ describe UploadsController do end context "when the project doesn't have public projects" do - context "when not signed in" do - it "redirects to the sign in page" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" - - expect(response).to redirect_to(new_user_session_path) - end - end - context "when signed in" do before do sign_in(user) diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb index 29fcbc5e197..8d287ded292 100644 --- a/spec/factories/abuse_reports.rb +++ b/spec/factories/abuse_reports.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: abuse_reports +# +# id :integer not null, primary key +# reporter_id :integer +# user_id :integer +# message :text +# created_at :datetime +# updated_at :datetime +# + # Read about factories at https://github.com/thoughtbot/factory_girl FactoryGirl.define do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb new file mode 100644 index 00000000000..21b582afba4 --- /dev/null +++ b/spec/factories/ci/builds.rb @@ -0,0 +1,53 @@ +# == Schema Information +# +# Table name: 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 +# commit_id :integer +# coverage :float +# 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 +# + +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :ci_build, class: Ci::Build do + ref 'master' + tag false + started_at 'Di 29. Okt 09:51:28 CET 2013' + finished_at 'Di 29. Okt 09:53:28 CET 2013' + commands 'ls -a' + options do + { + image: "ruby:2.1", + services: ["postgres"] + } + end + + commit factory: :ci_commit + + factory :ci_not_started_build do + started_at nil + finished_at nil + end + + factory :ci_build_tag do + tag true + end + end +end diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb new file mode 100644 index 00000000000..79e000b7ccb --- /dev/null +++ b/spec/factories/ci/commits.rb @@ -0,0 +1,49 @@ +# == Schema Information +# +# Table name: commits +# +# id :integer not null, primary key +# project_id :integer +# ref :string(255) +# sha :string(255) +# before_sha :string(255) +# push_data :text +# created_at :datetime +# updated_at :datetime +# tag :boolean default(FALSE) +# yaml_errors :text +# committed_at :datetime +# + +# Read about factories at https://github.com/thoughtbot/factory_girl +FactoryGirl.define do + factory :ci_empty_commit, class: Ci::Commit do + sha '97de212e80737a608d939f648d959671fb0a0142' + + gl_project factory: :empty_project + + factory :ci_commit_without_jobs do + after(:build) do |commit| + allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) } + end + end + + factory :ci_commit_with_one_job do + after(:build) do |commit| + allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) } + end + end + + factory :ci_commit_with_two_jobs do + after(:build) do |commit| + allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) } + end + end + + factory :ci_commit do + after(:build) do |commit| + allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } + end + end + end +end diff --git a/spec/factories/ci/events.rb b/spec/factories/ci/events.rb new file mode 100644 index 00000000000..9638618a400 --- /dev/null +++ b/spec/factories/ci/events.rb @@ -0,0 +1,24 @@ +# == Schema Information +# +# Table name: events +# +# id :integer not null, primary key +# project_id :integer +# user_id :integer +# is_admin :integer +# description :text +# created_at :datetime +# updated_at :datetime +# + +FactoryGirl.define do + factory :ci_event, class: Ci::Event do + sequence :description do |n| + "updated project settings#{n}" + end + + factory :ci_admin_event do + is_admin true + end + end +end diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb new file mode 100644 index 00000000000..111e1a82816 --- /dev/null +++ b/spec/factories/ci/projects.rb @@ -0,0 +1,44 @@ +# == Schema Information +# +# Table name: projects +# +# id :integer not null, primary key +# name :string(255) not null +# timeout :integer default(3600), not null +# created_at :datetime +# updated_at :datetime +# token :string(255) +# default_ref :string(255) +# path :string(255) +# always_build :boolean default(FALSE), not null +# polling_interval :integer +# public :boolean default(FALSE), not null +# ssh_url_to_repo :string(255) +# gitlab_id :integer +# allow_git_fetch :boolean default(TRUE), not null +# email_recipients :string(255) default(""), not null +# email_add_pusher :boolean default(TRUE), not null +# email_only_broken_builds :boolean default(TRUE), not null +# skip_refs :string(255) +# coverage_regex :string(255) +# shared_runners_enabled :boolean default(FALSE) +# generated_yaml_config :text +# + +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :ci_project_without_token, class: Ci::Project do + default_ref 'master' + + gl_project factory: :empty_project + + factory :ci_project do + token 'iPWx6WM4lhHNedGfBpPJNP' + end + + factory :ci_public_project do + public true + end + end +end diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb new file mode 100644 index 00000000000..3aa14ca434d --- /dev/null +++ b/spec/factories/ci/runner_projects.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: runner_projects +# +# id :integer not null, primary key +# runner_id :integer not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :ci_runner_project, class: Ci::RunnerProject do + runner_id 1 + project_id 1 + end +end diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb new file mode 100644 index 00000000000..db759eca9ac --- /dev/null +++ b/spec/factories/ci/runners.rb @@ -0,0 +1,38 @@ +# == Schema Information +# +# Table name: runners +# +# id :integer not null, primary key +# token :string(255) +# created_at :datetime +# updated_at :datetime +# description :string(255) +# contacted_at :datetime +# active :boolean default(TRUE), not null +# is_shared :boolean default(FALSE) +# name :string(255) +# version :string(255) +# revision :string(255) +# platform :string(255) +# architecture :string(255) +# + +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :ci_runner, class: Ci::Runner do + sequence :description do |n| + "My runner#{n}" + end + + platform "darwin" + + factory :ci_shared_runner do + is_shared true + end + + factory :ci_specific_runner do + is_shared false + end + end +end diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb new file mode 100644 index 00000000000..db053c610cd --- /dev/null +++ b/spec/factories/ci/trigger_requests.rb @@ -0,0 +1,13 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :ci_trigger_request, class: Ci::TriggerRequest do + factory :ci_trigger_request_with_variables do + variables do + { + TRIGGER_KEY: 'TRIGGER_VALUE' + } + end + end + end +end diff --git a/spec/factories/ci/triggers.rb b/spec/factories/ci/triggers.rb new file mode 100644 index 00000000000..fd3afdb1ec2 --- /dev/null +++ b/spec/factories/ci/triggers.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :ci_trigger_without_token, class: Ci::Trigger do + factory :ci_trigger do + token 'token' + end + end +end diff --git a/spec/factories/ci/web_hook.rb b/spec/factories/ci/web_hook.rb new file mode 100644 index 00000000000..40d878ecb3c --- /dev/null +++ b/spec/factories/ci/web_hook.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :ci_web_hook, class: Ci::WebHook do + sequence(:url) { FFaker::Internet.uri('http') } + project factory: :ci_project + end +end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 3b7adfe4398..6080d0ccdef 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -19,6 +19,7 @@ # description :text # position :integer default(0) # locked_at :datetime +# updated_by_id :integer # FactoryGirl.define do diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index e1009d5916e..9d777ddfccd 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -15,6 +15,7 @@ # noteable_id :integer # system :boolean default(FALSE), not null # st_diff :text +# updated_by_id :integer # require_relative '../support/repo_helpers' diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 86717761582..c2c7364f6c5 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -111,6 +111,27 @@ describe "Admin::Users", feature: true do expect(page).to have_content(@user.name) end + describe 'Login as another user' do + it 'should show login button for other users and check that it works' do + another_user = create(:user) + + visit admin_user_path(another_user) + + click_link 'Log in as this user' + + expect(page).to have_content("Logged in as #{another_user.username}") + + page.within '.sidebar-user .username' do + expect(page).to have_content(another_user.username) + end + end + + it 'should not show login button for admin itself' do + visit admin_user_path(@user) + expect(page).not_to have_content('Log in as this user') + end + end + describe 'Two-factor Authentication status' do it 'shows when enabled' do @user.update_attribute(:two_factor_enabled, true) diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index ad157d742ff..f81a3c117ff 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -6,7 +6,7 @@ describe "Dashboard Feed", feature: true do context "projects atom feed via private token" do it "should render projects atom feed" do - visit dashboard_path(:atom, private_token: user.private_token) + visit dashboard_projects_path(:atom, private_token: user.private_token) expect(body).to have_selector('feed title') end end @@ -20,7 +20,7 @@ describe "Dashboard Feed", feature: true do project.team << [user, :master] issue_event(issue, user) note_event(note, user) - visit dashboard_path(:atom, private_token: user.private_token) + visit dashboard_projects_path(:atom, private_token: user.private_token) end it "should have issue opened event" do diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb new file mode 100644 index 00000000000..d0d60491b65 --- /dev/null +++ b/spec/features/builds_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe "Builds" do + + before do + login_as(:user) + @commit = FactoryGirl.create :ci_commit + @build = FactoryGirl.create :ci_build, commit: @commit + @gl_project = @commit.project.gl_project + @gl_project.team << [@user, :master] + end + + describe "GET /:project/builds/:id" do + before do + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + end + + it { expect(page).to have_content @commit.sha[0..7] } + it { expect(page).to have_content @commit.git_commit_message } + it { expect(page).to have_content @commit.git_author_name } + end +end diff --git a/spec/features/ci/admin/builds_spec.rb b/spec/features/ci/admin/builds_spec.rb new file mode 100644 index 00000000000..623d466c67b --- /dev/null +++ b/spec/features/ci/admin/builds_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe "Admin Builds" do + let(:commit) { FactoryGirl.create :ci_commit } + let(:build) { FactoryGirl.create :ci_build, commit: commit } + + before do + skip_ci_admin_auth + login_as :user + end + + describe "GET /admin/builds" do + before do + build + visit ci_admin_builds_path + end + + it { expect(page).to have_content "All builds" } + it { expect(page).to have_content build.short_sha } + end + + 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" + + visit ci_admin_builds_path + + expect(page.all(".build-link").size).to eq(4) + end + + it "shows pending 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" + build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed" + + visit ci_admin_builds_path + + within ".nav.nav-tabs" do + click_on "Pending" + end + + expect(page.find(".build-link")).to have_content(build.id) + 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")).not_to have_content(build3.id) + end + + it "shows running 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" + build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed" + + visit ci_admin_builds_path + + within ".nav.nav-tabs" do + click_on "Running" + end + + expect(page.find(".build-link")).to have_content(build1.id) + expect(page.find(".build-link")).not_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) + end + end +end diff --git a/spec/features/ci/admin/events_spec.rb b/spec/features/ci/admin/events_spec.rb new file mode 100644 index 00000000000..a7e75cc4f6b --- /dev/null +++ b/spec/features/ci/admin/events_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe "Admin Events" do + let(:event) { FactoryGirl.create :ci_admin_event } + + before do + skip_ci_admin_auth + login_as :user + end + + describe "GET /admin/events" do + before do + event + visit ci_admin_events_path + end + + it { expect(page).to have_content "Events" } + it { expect(page).to have_content event.description } + end +end diff --git a/spec/features/ci/admin/projects_spec.rb b/spec/features/ci/admin/projects_spec.rb new file mode 100644 index 00000000000..b88f55a6807 --- /dev/null +++ b/spec/features/ci/admin/projects_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe "Admin Projects" do + let(:project) { FactoryGirl.create :ci_project } + + before do + skip_ci_admin_auth + login_as :user + end + + describe "GET /admin/projects" do + before do + project + visit ci_admin_projects_path + end + + it { expect(page).to have_content "Projects" } + end +end diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/ci/admin/runners_spec.rb new file mode 100644 index 00000000000..b83744f53a8 --- /dev/null +++ b/spec/features/ci/admin/runners_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe "Admin Runners" do + before do + login_as :admin + end + + describe "Runners page" do + before do + runner = FactoryGirl.create(:ci_runner) + commit = FactoryGirl.create(:ci_commit) + FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) + visit ci_admin_runners_path + end + + it { page.has_text? "Manage Runners" } + it { page.has_text? "To register a new runner" } + it { page.has_text? "Runners with last contact less than a minute ago: 1" } + + describe 'search' do + before do + FactoryGirl.create :ci_runner, description: 'runner-foo' + FactoryGirl.create :ci_runner, description: 'runner-bar' + + search_form = find('#runners-search') + search_form.fill_in 'search', with: 'runner-foo' + search_form.click_button 'Search' + end + + it { expect(page).to have_content("runner-foo") } + it { expect(page).not_to have_content("runner-bar") } + end + end + + describe "Runner show page" do + let(:runner) { FactoryGirl.create :ci_runner } + + before do + @project1 = FactoryGirl.create(:ci_project) + @project2 = FactoryGirl.create(:ci_project) + visit ci_admin_runner_path(runner) + end + + describe 'runner info' do + it { expect(find_field('runner_token').value).to eq runner.token } + end + + describe 'projects' do + it { expect(page).to have_content(@project1.name_with_namespace) } + it { expect(page).to have_content(@project2.name_with_namespace) } + end + + describe 'search' do + before do + search_form = find('#runner-projects-search') + search_form.fill_in 'search', with: @project1.gl_project.name + search_form.click_button 'Search' + end + + it { expect(page).to have_content(@project1.name_with_namespace) } + it { expect(page).not_to have_content(@project2.name_with_namespace) } + end + end +end diff --git a/spec/features/ci/builds_spec.rb b/spec/features/ci/builds_spec.rb new file mode 100644 index 00000000000..aa0df59c04d --- /dev/null +++ b/spec/features/ci/builds_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe "Builds" do + before do + login_as(:user) + @commit = FactoryGirl.create :ci_commit + @build = FactoryGirl.create :ci_build, commit: @commit + @gl_project = @commit.project.gl_project + @gl_project.team << [@user, :master] + end + + describe "GET /:project/builds/:id/cancel" do + before do + @build.run! + visit cancel_ci_project_build_path(@commit.project, @build) + end + + it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content 'Retry' } + end + + describe "POST /:project/builds/:id/retry" do + before do + visit cancel_ci_project_build_path(@commit.project, @build) + click_link 'Retry' + end + + it { expect(page).to have_content 'pending' } + it { expect(page).to have_content 'Cancel' } + end +end diff --git a/spec/features/ci/events_spec.rb b/spec/features/ci/events_spec.rb new file mode 100644 index 00000000000..5b9fd404159 --- /dev/null +++ b/spec/features/ci/events_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe "Events" do + let(:user) { create(:user) } + let(:project) { FactoryGirl.create :ci_project } + let(:event) { FactoryGirl.create :ci_admin_event, project: project } + + before do + login_as(user) + project.gl_project.team << [user, :master] + end + + describe "GET /ci/project/:id/events" do + before do + event + visit ci_project_events_path(project) + end + + it { expect(page).to have_content "Events" } + it { expect(page).to have_content event.description } + end +end diff --git a/spec/features/ci/lint_spec.rb b/spec/features/ci/lint_spec.rb new file mode 100644 index 00000000000..5d8f56e2cfb --- /dev/null +++ b/spec/features/ci/lint_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe "Lint" do + before do + login_as :user + end + + it "Yaml parsing", js: true do + content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + visit ci_lint_path + fill_in "content", with: content + click_on "Validate" + within "table" do + expect(page).to have_content("Job - rspec") + expect(page).to have_content("Job - spinach") + expect(page).to have_content("Deploy Job - staging") + expect(page).to have_content("Deploy Job - production") + end + end + + it "Yaml parsing with error", js: true do + visit ci_lint_path + fill_in "content", with: "" + click_on "Validate" + expect(page).to have_content("Status: syntax is incorrect") + expect(page).to have_content("Error: Please provide content of .gitlab-ci.yml") + end +end diff --git a/spec/features/ci/projects_spec.rb b/spec/features/ci/projects_spec.rb new file mode 100644 index 00000000000..c633acf85fb --- /dev/null +++ b/spec/features/ci/projects_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe "Projects" do + let(:user) { create(:user) } + + before do + login_as(user) + @project = FactoryGirl.create :ci_project, name: "GitLab / gitlab-shell" + @project.gl_project.team << [user, :master] + end + + describe "GET /ci/projects/:id" do + before do + visit ci_project_path(@project) + end + + it { expect(page).to have_content @project.name } + it { expect(page).to have_content 'All commits' } + end +end diff --git a/spec/features/ci_settings_spec.rb b/spec/features/ci_settings_spec.rb new file mode 100644 index 00000000000..7e25e883018 --- /dev/null +++ b/spec/features/ci_settings_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe "CI settings" do + let(:user) { create(:user) } + before { login_as(user) } + + before do + @project = FactoryGirl.create :ci_project + @gl_project = @project.gl_project + @gl_project.team << [user, :master] + visit edit_namespace_project_ci_settings_path(@gl_project.namespace, @gl_project) + end + + it { expect(page).to have_content 'Build Schedule' } + + it "updates configuration" do + fill_in 'Timeout', with: '70' + click_button 'Save changes' + expect(page).to have_content 'was successfully updated' + expect(find_field('Timeout').value).to eq '70' + end +end diff --git a/spec/features/ci_web_hooks_spec.rb b/spec/features/ci_web_hooks_spec.rb new file mode 100644 index 00000000000..efae0a42c1e --- /dev/null +++ b/spec/features/ci_web_hooks_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe 'CI web hooks' do + let(:user) { create(:user) } + before { login_as(user) } + + before do + @project = FactoryGirl.create :ci_project + @gl_project = @project.gl_project + @gl_project.team << [user, :master] + visit namespace_project_ci_web_hooks_path(@gl_project.namespace, @gl_project) + end + + context 'create a trigger' do + before do + fill_in 'web_hook_url', with: 'http://example.com' + click_on 'Add Web Hook' + end + + it { expect(@project.web_hooks.count).to eq(1) } + + it 'revokes the trigger' do + click_on 'Remove' + expect(@project.web_hooks.count).to eq(0) + end + end +end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb new file mode 100644 index 00000000000..5da220859e3 --- /dev/null +++ b/spec/features/commits_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe "Commits" do + include CiStatusHelper + + let(:project) { create(:project) } + + describe "CI" do + before do + login_as :user + project.team << [@user, :master] + @ci_project = project.ensure_gitlab_ci_project + @commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha + @build = FactoryGirl.create :ci_build, commit: @commit + end + + before do + stub_ci_commit_to_return_yaml_file + end + + describe "GET /:project/commits/:sha" do + before do + visit ci_status_path(@commit) + end + + it { expect(page).to have_content @commit.sha[0..7] } + it { expect(page).to have_content @commit.git_commit_message } + it { expect(page).to have_content @commit.git_author_name } + end + + describe "Cancel commit" do + it "cancels commit" do + visit ci_status_path(@commit) + click_on "Cancel" + expect(page).to have_content "canceled" + end + end + + describe ".gitlab-ci.yml not found warning" do + it "does not show warning" do + visit ci_status_path(@commit) + expect(page).not_to have_content ".gitlab-ci.yml not found in this commit" + end + + it "shows warning" do + stub_ci_commit_yaml_file(nil) + visit ci_status_path(@commit) + expect(page).to have_content ".gitlab-ci.yml not found in this commit" + end + end + end +end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index cef432e512b..922c76285d1 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -95,7 +95,7 @@ feature 'Login', feature: true do user = create(:user, password: 'not-the-default') login_with(user) - expect(page).to have_content('Invalid email or password.') + expect(page).to have_content('Invalid login or password.') end end end diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index 2b6311e4fd7..85e70b4d47f 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -1,53 +1,43 @@ require 'spec_helper' feature 'Password reset', feature: true do - def forgot_password + describe 'throttling' do + it 'sends reset instructions when not previously sent' do + visit root_path + forgot_password(create(:user)) + + expect(page).to have_content(I18n.t('devise.passwords.send_instructions')) + expect(current_path).to eq new_user_session_path + end + + it 'sends reset instructions when previously sent more than a minute ago' do + user = create(:user) + user.send_reset_password_instructions + user.update_attribute(:reset_password_sent_at, 5.minutes.ago) + + visit root_path + forgot_password(user) + + expect(page).to have_content(I18n.t('devise.passwords.send_instructions')) + expect(current_path).to eq new_user_session_path + end + + it "throttles multiple resets in a short timespan" do + user = create(:user) + user.send_reset_password_instructions + + visit root_path + forgot_password(user) + + expect(page).to have_content(I18n.t('devise.passwords.recently_reset')) + expect(current_path).to eq new_user_password_path + end + end + + def forgot_password(user) click_on 'Forgot your password?' fill_in 'Email', with: user.email click_button 'Reset password' user.reload end - - def get_reset_token - mail = ActionMailer::Base.deliveries.last - body = mail.body.encoded - body.scan(/reset_password_token=(.+)\"/).flatten.first - end - - def reset_password(password = 'password') - visit edit_user_password_path(reset_password_token: get_reset_token) - - fill_in 'New password', with: password - fill_in 'Confirm new password', with: password - click_button 'Change your password' - end - - describe 'with two-factor authentication' do - let(:user) { create(:user, :two_factor) } - - it 'requires login after password reset' do - visit root_path - - forgot_password - reset_password - - expect(page).to have_content("Your password was changed successfully.") - expect(page).not_to have_content("You are now signed in.") - expect(current_path).to eq new_user_session_path - end - end - - describe 'without two-factor authentication' do - let(:user) { create(:user) } - - it 'automatically logs in after password reset' do - visit root_path - - forgot_password - reset_password - - expect(current_path).to eq root_path - expect(page).to have_content("Your password was changed successfully. You are now signed in.") - end - end end diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index 9bc6145dda4..8f645438cff 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -70,7 +70,7 @@ describe 'Profile > Preferences', feature: true do expect(page.current_path).to eq starred_dashboard_projects_path click_link 'Your Projects' - expect(page.current_path).to eq dashboard_path + expect(page.current_path).to eq dashboard_projects_path end end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb new file mode 100644 index 00000000000..06adb7633b2 --- /dev/null +++ b/spec/features/runners_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe "Runners" do + include GitlabRoutingHelper + + let(:user) { create(:user) } + before { login_as(user) } + + describe "specific runners" do + before do + @project = FactoryGirl.create :ci_project + @project.gl_project.team << [user, :master] + + @project2 = FactoryGirl.create :ci_project + @project2.gl_project.team << [user, :master] + + @shared_runner = FactoryGirl.create :ci_shared_runner + @specific_runner = FactoryGirl.create :ci_specific_runner + @specific_runner2 = FactoryGirl.create :ci_specific_runner + @project.runners << @specific_runner + @project2.runners << @specific_runner2 + + visit runners_path(@project.gl_project) + end + + it "places runners in right places" do + expect(page.find(".available-specific-runners")).to have_content(@specific_runner2.display_name) + expect(page.find(".activated-specific-runners")).to have_content(@specific_runner.display_name) + expect(page.find(".available-shared-runners")).to have_content(@shared_runner.display_name) + end + + it "enables specific runner for project" do + within ".available-specific-runners" do + click_on "Enable for this project" + end + + expect(page.find(".activated-specific-runners")).to have_content(@specific_runner2.display_name) + end + + it "disables specific runner for project" do + @project2.runners << @specific_runner + visit runners_path(@project.gl_project) + + within ".activated-specific-runners" do + click_on "Disable for this project" + end + + expect(page.find(".available-specific-runners")).to have_content(@specific_runner.display_name) + end + + it "removes specific runner for project if this is last project for that runners" do + within ".activated-specific-runners" do + click_on "Remove runner" + end + + expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey + end + end + + describe "shared runners" do + before do + @project = FactoryGirl.create :ci_project + @project.gl_project.team << [user, :master] + visit runners_path(@project.gl_project) + end + + it "enables shared runners" do + click_on "Enable shared runners" + expect(@project.reload.shared_runners_enabled).to be_truthy + end + end + + describe "show page" do + before do + @project = FactoryGirl.create :ci_project + @project.gl_project.team << [user, :master] + @specific_runner = FactoryGirl.create :ci_specific_runner + @project.runners << @specific_runner + visit runners_path(@project.gl_project) + end + + it "shows runner information" do + click_on @specific_runner.short_sha + expect(page).to have_content(@specific_runner.platform) + end + end +end diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index c38cddbb904..788581a26cb 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -4,7 +4,7 @@ describe "Dashboard access", feature: true do include AccessMatchers describe "GET /dashboard" do - subject { dashboard_path } + subject { dashboard_projects_path } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } @@ -40,7 +40,7 @@ describe "Dashboard access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for :visitor } end describe "GET /projects/new" do diff --git a/spec/features/security/group_access_spec.rb b/spec/features/security/group_access_spec.rb index 8ce15388605..4b78e3a61f0 100644 --- a/spec/features/security/group_access_spec.rb +++ b/spec/features/security/group_access_spec.rb @@ -68,7 +68,7 @@ describe 'Group access', feature: true do it { is_expected.to be_allowed_for group_member(:guest) } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for :visitor } end context 'with no projects' do @@ -77,8 +77,8 @@ describe 'Group access', feature: true do it { is_expected.to be_allowed_for group_member(:reporter) } it { is_expected.to be_allowed_for group_member(:guest) } it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end end diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb new file mode 100644 index 00000000000..69492d58878 --- /dev/null +++ b/spec/features/triggers_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'Triggers' do + let(:user) { create(:user) } + before { login_as(user) } + + before do + @project = FactoryGirl.create :ci_project + @gl_project = @project.gl_project + @gl_project.team << [user, :master] + visit namespace_project_triggers_path(@gl_project.namespace, @gl_project) + end + + context 'create a trigger' do + before do + click_on 'Add Trigger' + expect(@project.triggers.count).to eq(1) + end + + it 'contains trigger token' do + expect(page).to have_content(@project.triggers.first.token) + end + + it 'revokes the trigger' do + click_on 'Revoke' + expect(@project.triggers.count).to eq(0) + end + end +end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index efcb8a31abe..c1248162031 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Users', feature: true do + let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } + scenario 'GET /users/sign_in creates a new user account' do visit new_user_session_path fill_in 'user_name', with: 'Name Surname' @@ -11,7 +13,6 @@ feature 'Users', feature: true do end scenario 'Successful user signin invalidates password reset token' do - user = create(:user) expect(user.reset_password_token).to be_nil visit new_user_password_path @@ -28,7 +29,6 @@ feature 'Users', feature: true do expect(user.reset_password_token).to be_nil end - let!(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } scenario 'Should show one error if email is already taken' do visit new_user_session_path fill_in 'user_name', with: 'Another user name' diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb new file mode 100644 index 00000000000..adb602f3edd --- /dev/null +++ b/spec/features/variables_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe "Variables" do + let(:user) { create(:user) } + before { login_as(user) } + + describe "specific runners" do + before do + @project = FactoryGirl.create :ci_project + @gl_project = @project.gl_project + @gl_project.team << [user, :master] + end + + it "creates variable", js: true do + visit namespace_project_variables_path(@gl_project.namespace, @gl_project) + click_on "Add a variable" + fill_in "Key", with: "SECRET_KEY" + fill_in "Value", with: "SECRET_VALUE" + click_on "Save changes" + + expect(page).to have_content("Variables were successfully updated.") + expect(@project.variables.count).to eq(1) + end + end +end diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb new file mode 100644 index 00000000000..7fc53eb1472 --- /dev/null +++ b/spec/helpers/ci_status_helper_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe CiStatusHelper do + include IconsHelper + + let(:success_commit) { double("Ci::Commit", status: 'success') } + let(:failed_commit) { double("Ci::Commit", status: 'failed') } + + describe 'ci_status_color' do + it { expect(ci_status_icon(success_commit)).to include('fa-check') } + it { expect(ci_status_icon(failed_commit)).to include('fa-close') } + end + + describe 'ci_status_color' do + it { expect(ci_status_color(success_commit)).to eq('green') } + it { expect(ci_status_color(failed_commit)).to eq('red') } + end +end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 0c2b3003092..5d471f2f6ee 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -41,6 +41,17 @@ describe GitlabMarkdownHelper do expect(helper.markdown(actual)).to match(expected) end end + + describe "override default project" do + let(:actual) { issue.to_reference } + let(:second_project) { create(:project) } + let(:second_issue) { create(:issue, project: second_project) } + + it 'should link to the issue' do + expected = namespace_project_issue_path(second_project.namespace, second_project, second_issue) + expect(markdown(actual, project: second_project)).to match(expected) + end + end end describe '#link_to_gfm' do @@ -98,6 +109,12 @@ describe GitlabMarkdownHelper do act = helper.link_to_gfm(text, '/foo') expect(act).to eq %Q(#{issues[0].to_reference}) end + + it 'should replace commit message with emoji to link' do + actual = link_to_gfm(':book:Book', '/foo') + expect(actual). + to eq %Q(Book) + end end describe '#render_wiki_content' do @@ -138,4 +155,24 @@ describe GitlabMarkdownHelper do expect(random_markdown_tip).to eq 'Random tip' end end + + describe '#first_line_in_markdown' do + let(:text) { "@#{user.username}, can you look at this?\nHello world\n"} + + it 'truncates Markdown properly' do + actual = first_line_in_markdown(text, 100, project: project) + + doc = Nokogiri::HTML.parse(actual) + + # Make sure we didn't create invalid markup + expect(doc.errors).to be_empty + + # Leading user link + expect(doc.css('a').length).to eq(1) + expect(doc.css('a')[0].attr('href')).to eq user_path(user) + expect(doc.css('a')[0].text).to eq "@#{user.username}" + + expect(doc.content).to eq "@#{user.username}, can you look at this?..." + end + end end diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb new file mode 100644 index 00000000000..4acf38771b7 --- /dev/null +++ b/spec/helpers/graph_helper_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe GraphHelper do + describe '#get_refs' do + let(:project) { create(:project) } + let(:commit) { project.commit("master") } + let(:graph) { Network::Graph.new(project, 'master', commit, '') } + + it 'filter our refs used by GitLab' do + allow(commit).to receive(:ref_names).and_return(['refs/merge-requests/abc', 'master', 'refs/tmp/xyz']) + self.instance_variable_set(:@graph, graph) + refs = get_refs(project.repository, commit) + expect(refs).to eq('master') + end + end +end diff --git a/spec/helpers/merge_requests_helper.rb b/spec/helpers/merge_requests_helper.rb deleted file mode 100644 index 5262d644048..00000000000 --- a/spec/helpers/merge_requests_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe MergeRequestsHelper do - describe :issues_sentence do - subject { issues_sentence(issues) } - let(:issues) do - [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)] - end - - it { is_expected.to eq('#1, #2, and #3') } - end -end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb new file mode 100644 index 00000000000..0ef1efb8bce --- /dev/null +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe MergeRequestsHelper do + describe "#issues_sentence" do + subject { issues_sentence(issues) } + let(:issues) do + [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)] + end + + it { is_expected.to eq('#1, #2, and #3') } + end + + describe "#format_mr_branch_names" do + describe "within the same project" do + let(:merge_request) { create(:merge_request) } + subject { format_mr_branch_names(merge_request) } + + it { is_expected.to eq([merge_request.source_branch, merge_request.target_branch]) } + end + + describe "within different projects" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) } + subject { format_mr_branch_names(merge_request) } + let(:source_title) { "#{fork_project.path_with_namespace}:#{merge_request.source_branch}" } + let(:target_title) { "#{project.path_with_namespace}:#{merge_request.target_branch}" } + + it { is_expected.to eq([source_title, target_title]) } + end + end +end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 06f69262b71..e5df59c4fba 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -8,14 +8,18 @@ describe PreferencesHelper do end it 'raises an exception when defined choices may be using the wrong key' do - expect(User).to receive(:dashboards).and_return(foo: 'foo', bar: 'bar') + dashboards = User.dashboards.dup + dashboards[:projects_changed] = dashboards.delete :projects + expect(User).to receive(:dashboards).and_return(dashboards) expect { helper.dashboard_choices }.to raise_error(KeyError) end it 'provides better option descriptions' do expect(helper.dashboard_choices).to match_array [ ['Your Projects (default)', 'projects'], - ['Starred Projects', 'stars'] + ['Starred Projects', 'stars'], + ["Your Projects' Activity", 'project_activity'], + ["Starred Projects' Activity", 'starred_project_activity'] ] end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 99abb95d906..53e56ebff44 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -61,13 +61,13 @@ describe ProjectsHelper do end it "returns a valid cach key" do - expect(helper.send(:readme_cache_key)).to eq("#{project.id}-#{project.commit.id}-readme") + expect(helper.send(:readme_cache_key)).to eq("#{project.path_with_namespace}-#{project.commit.id}-readme") end it "returns a valid cache key if HEAD does not exist" do allow(project).to receive(:commit) { nil } - expect(helper.send(:readme_cache_key)).to eq("#{project.id}-nil-readme") + expect(helper.send(:readme_cache_key)).to eq("#{project.path_with_namespace}-nil-readme") end end end diff --git a/spec/helpers/runners_helper_spec.rb b/spec/helpers/runners_helper_spec.rb new file mode 100644 index 00000000000..b3d635a1932 --- /dev/null +++ b/spec/helpers/runners_helper_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe RunnersHelper do + it "returns - not contacted yet" do + runner = FactoryGirl.build :ci_runner + expect(runner_status_icon(runner)).to include("not connected yet") + end + + it "returns offline text" do + runner = FactoryGirl.build(:ci_runner, contacted_at: 1.day.ago, active: true) + expect(runner_status_icon(runner)).to include("Runner is offline") + end + + it "returns online text" do + runner = FactoryGirl.build(:ci_runner, contacted_at: 1.hour.ago, active: true) + expect(runner_status_icon(runner)).to include("Runner is online") + end +end diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb new file mode 100644 index 00000000000..3f62527c5bb --- /dev/null +++ b/spec/helpers/time_helper_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe TimeHelper do + describe "#duration_in_words" do + it "returns minutes and seconds" do + intervals_in_words = { + 100 => "1 minute 40 seconds", + 121 => "2 minutes 1 second", + 3721 => "62 minutes 1 second", + 0 => "0 seconds" + } + + intervals_in_words.each do |interval, expectation| + expect(duration_in_words(Time.now + interval, Time.now)).to eq(expectation) + end + end + + it "calculates interval from now if there is no finished_at" do + expect(duration_in_words(nil, Time.now - 5)).to eq("5 seconds") + end + end + + describe "#time_interval_in_words" do + it "returns minutes and seconds" do + intervals_in_words = { + 100 => "1 minute 40 seconds", + 121 => "2 minutes 1 second", + 3721 => "62 minutes 1 second", + 0 => "0 seconds" + } + + intervals_in_words.each do |interval, expectation| + expect(time_interval_in_words(interval)).to eq(expectation) + end + end + end +end diff --git a/spec/javascripts/syntax_highlight_spec.js.coffee b/spec/javascripts/syntax_highlight_spec.js.coffee new file mode 100644 index 00000000000..6a73b6bf32c --- /dev/null +++ b/spec/javascripts/syntax_highlight_spec.js.coffee @@ -0,0 +1,42 @@ +#= require syntax_highlight + +describe 'Syntax Highlighter', -> + stubUserColorScheme = (value) -> + window.gon ?= {} + window.gon.user_color_scheme = value + + describe 'on a js-syntax-highlight element', -> + beforeEach -> + fixture.set('') + + it 'applies syntax highlighting', -> + stubUserColorScheme('monokai') + + $('.js-syntax-highlight').syntaxHighlight() + + expect($('.js-syntax-highlight')).toHaveClass('monokai') + + describe 'on a parent element', -> + beforeEach -> + fixture.set """ +
def fun end
') + expect(result.to_html).to eq("\n") + end + + it 'passes through invalid code blocks' do + allow_any_instance_of(SyntaxHighlightFilter).to receive(:block_code).and_raise(StandardError) + + result = filter('def fun end
') + expect(result.to_html).to eq('This is a test
This is a test') + end + end +end diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index e4a6cd954cc..5632f2306ec 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' describe Gitlab::OAuth::AuthHash do let(:auth_hash) do Gitlab::OAuth::AuthHash.new( - double({ + OmniAuth::AuthHash.new( provider: provider_ascii, uid: uid_ascii, - info: double(info_hash) - }) + info: info_hash + ) ) end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index c6cca98a037..fd3ab1fb7c8 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::OAuth::User do let(:gl_user) { oauth_user.gl_user } let(:uid) { 'my-uid' } let(:provider) { 'my-provider' } - let(:auth_hash) { double(uid: uid, provider: provider, info: double(info_hash)) } + let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) } let(:info_hash) do { nickname: '-john+gitlab-ETC%.git@gmail.com', @@ -19,10 +19,6 @@ describe Gitlab::OAuth::User do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } it "finds an existing user based on uid and provider (facebook)" do - # FIXME (rspeicher): It's unlikely that this test is actually doing anything - # `auth` is never used and removing it entirely doesn't break the test, so - # what's it doing? - auth = double(info: double(name: 'John'), uid: 'my-uid', provider: 'my-provider') expect( oauth_user.persisted? ).to be_truthy end diff --git a/spec/lib/gitlab/reply_by_email_spec.rb b/spec/lib/gitlab/reply_by_email_spec.rb deleted file mode 100644 index a678c7e1a76..00000000000 --- a/spec/lib/gitlab/reply_by_email_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -require "spec_helper" - -describe Gitlab::ReplyByEmail do - describe "self.enabled?" do - context "when reply by email is enabled" do - before do - stub_reply_by_email_setting(enabled: true) - end - - context "when the address is valid" do - before do - stub_reply_by_email_setting(address: "replies+%{reply_key}@example.com") - end - - it "returns true" do - expect(described_class.enabled?).to be_truthy - end - end - - context "when the address is invalid" do - before do - stub_reply_by_email_setting(address: "replies@example.com") - end - - it "returns false" do - expect(described_class.enabled?).to be_falsey - end - end - end - - context "when reply by email is disabled" do - before do - stub_reply_by_email_setting(enabled: false) - end - - it "returns false" do - expect(described_class.enabled?).to be_falsey - end - end - end - - describe "self.reply_key" do - context "when enabled" do - before do - allow(described_class).to receive(:enabled?).and_return(true) - end - - it "returns a random hex" do - key = described_class.reply_key - key2 = described_class.reply_key - - expect(key).not_to eq(key2) - end - end - - context "when disabled" do - before do - allow(described_class).to receive(:enabled?).and_return(false) - end - - it "returns nil" do - expect(described_class.reply_key).to be_nil - end - end - end - - context "self.reply_address" do - before do - stub_reply_by_email_setting(address: "replies+%{reply_key}@example.com") - end - - it "returns the address with an interpolated reply key" do - expect(described_class.reply_address("key")).to eq("replies+key@example.com") - end - end - - context "self.reply_key_from_address" do - before do - stub_reply_by_email_setting(address: "replies+%{reply_key}@example.com") - end - - it "returns reply key" do - expect(described_class.reply_key_from_address("replies+key@example.com")).to eq("key") - end - end -end diff --git a/spec/mailers/ci/notify_spec.rb b/spec/mailers/ci/notify_spec.rb new file mode 100644 index 00000000000..b83fb41603b --- /dev/null +++ b/spec/mailers/ci/notify_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Ci::Notify do + include EmailSpec::Helpers + include EmailSpec::Matchers + + before do + @commit = FactoryGirl.create :ci_commit + @build = FactoryGirl.create :ci_build, commit: @commit + end + + describe 'build success' do + subject { Ci::Notify.build_success_email(@build.id, 'wow@example.com') } + + it 'has the correct subject' do + should have_subject /Build success for/ + end + + it 'contains name of project' do + should have_body_text /build successful/ + end + end + + describe 'build fail' do + subject { Ci::Notify.build_fail_email(@build.id, 'wow@example.com') } + + it 'has the correct subject' do + should have_subject /Build failed for/ + end + + it 'contains name of project' do + should have_body_text /build failed/ + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 97c07ad7d55..cb67ec95d57 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -52,6 +52,7 @@ describe Notify do end it 'has headers that reference an existing thread' do + is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ @@ -399,7 +400,7 @@ describe Notify do describe 'project was moved' do let(:project) { create(:project) } let(:user) { create(:user) } - subject { Notify.project_was_moved_email(project.id, user.id) } + subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } it_behaves_like 'an email sent from GitLab' @@ -712,7 +713,7 @@ describe Notify do before do user.update_attribute(:email, "user@company.com") - user.confirm! + user.confirm end it "is sent from the committer email" do @@ -730,7 +731,7 @@ describe Notify do before do user.update_attribute(:email, "user@something.company.com") - user.confirm! + user.confirm end it "is sent from the default email" do @@ -748,7 +749,7 @@ describe Notify do before do user.update_attribute(:email, "user@mpany.com") - user.confirm! + user.confirm end it "is sent from the default email" do diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index d83004a8388..d45319b25d4 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -1,7 +1,31 @@ +# == Schema Information +# +# Table name: abuse_reports +# +# id :integer not null, primary key +# reporter_id :integer +# user_id :integer +# message :text +# created_at :datetime +# updated_at :datetime +# + require 'rails_helper' RSpec.describe AbuseReport, type: :model do subject { create(:abuse_report) } it { expect(subject).to be_valid } + + describe 'associations' do + it { is_expected.to belong_to(:reporter).class_name('User') } + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:reporter) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:message) } + it { is_expected.to validate_uniqueness_of(:user_id) } + end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index bc14ff98fd8..de0b2ef4cda 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -22,6 +22,7 @@ # user_oauth_applications :boolean default(TRUE) # after_sign_out_path :string(255) # session_expire_delay :integer default(10080), not null +# import_sources :text # require 'spec_helper' diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 8ab72151a69..d80748f23a4 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -27,12 +27,12 @@ describe BroadcastMessage do end it "should return nil if time not come" do - broadcast_message = create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days) + create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days) expect(BroadcastMessage.current).to be_nil end it "should return nil if time has passed" do - broadcast_message = create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday) + create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday) expect(BroadcastMessage.current).to be_nil end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb new file mode 100644 index 00000000000..da56f6e31ae --- /dev/null +++ b/spec/models/ci/build_spec.rb @@ -0,0 +1,387 @@ +# == Schema Information +# +# Table name: 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 +# commit_id :integer +# coverage :float +# 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 +# + +require 'spec_helper' + +describe Ci::Build do + let(:project) { FactoryGirl.create :ci_project } + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let(:build) { FactoryGirl.create :ci_build, commit: commit } + subject { build } + + it { is_expected.to belong_to(:commit) } + it { is_expected.to belong_to(:user) } + it { is_expected.to validate_presence_of :status } + it { is_expected.to validate_presence_of :ref } + + it { is_expected.to respond_to :success? } + it { is_expected.to respond_to :failed? } + it { is_expected.to respond_to :running? } + it { is_expected.to respond_to :pending? } + it { is_expected.to respond_to :trace_html } + + describe :first_pending do + let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday } + let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' } + before { first; second } + subject { Ci::Build.first_pending } + + it { is_expected.to be_a(Ci::Build) } + it('returns with the first pending build') { is_expected.to eq(first) } + end + + describe :create_from do + before do + build.status = 'success' + build.save + end + let(:create_from_build) { Ci::Build.create_from build } + + it 'there should be a pending task' do + expect(Ci::Build.pending.count(:all)).to eq 0 + create_from_build + expect(Ci::Build.pending.count(:all)).to be > 0 + end + end + + describe :started? do + subject { build.started? } + + context 'without started_at' do + before { build.started_at = nil } + + it { is_expected.to be_falsey } + end + + %w(running success failed).each do |status| + context "if build status is #{status}" do + before { build.status = status } + + it { is_expected.to be_truthy } + end + end + + %w(pending canceled).each do |status| + context "if build status is #{status}" do + before { build.status = status } + + it { is_expected.to be_falsey } + end + end + end + + describe :active? do + subject { build.active? } + + %w(pending running).each do |state| + context "if build.status is #{state}" do + before { build.status = state } + + it { is_expected.to be_truthy } + end + end + + %w(success failed canceled).each do |state| + context "if build.status is #{state}" do + before { build.status = state } + + it { is_expected.to be_falsey } + end + end + end + + describe :complete? do + subject { build.complete? } + + %w(success failed canceled).each do |state| + context "if build.status is #{state}" do + before { build.status = state } + + it { is_expected.to be_truthy } + end + end + + %w(pending running).each do |state| + context "if build.status is #{state}" do + before { build.status = state } + + it { is_expected.to be_falsey } + end + end + end + + describe :ignored? do + subject { build.ignored? } + + context 'if build is not allowed to fail' do + before { build.allow_failure = false } + + context 'and build.status is success' do + before { build.status = 'success' } + + it { is_expected.to be_falsey } + end + + context 'and build.status is failed' do + before { build.status = 'failed' } + + it { is_expected.to be_falsey } + end + end + + context 'if build is allowed to fail' do + before { build.allow_failure = true } + + context 'and build.status is success' do + before { build.status = 'success' } + + it { is_expected.to be_falsey } + end + + context 'and build.status is failed' do + before { build.status = 'failed' } + + it { is_expected.to be_truthy } + end + end + end + + describe :trace do + subject { build.trace_html } + + it { is_expected.to be_empty } + + context 'if build.trace contains text' do + let(:text) { 'example output' } + before { build.trace = text } + + it { is_expected.to include(text) } + it { expect(subject.length).to be >= text.length } + end + + context 'if build.trace hides token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update_attributes(token: token) + build.update_attributes(trace: token) + end + + it { is_expected.to_not include(token) } + end + end + + describe :timeout do + subject { build.timeout } + + it { is_expected.to eq(commit.project.timeout) } + end + + describe :duration do + subject { build.duration } + + it { is_expected.to eq(120.0) } + + context 'if the building process has not started yet' do + before do + build.started_at = nil + build.finished_at = nil + end + + it { is_expected.to be_nil } + end + + context 'if the building process has started' do + before do + build.started_at = Time.now - 1.minute + build.finished_at = nil + end + + it { is_expected.to be_a(Float) } + it { is_expected.to be > 0.0 } + end + end + + describe :options do + let(:options) do + { + image: "ruby:2.1", + services: [ + "postgres" + ] + } + end + + subject { build.options } + it { is_expected.to eq(options) } + end + + describe :sha do + subject { build.sha } + + it { is_expected.to eq(commit.sha) } + end + + describe :short_sha do + subject { build.short_sha } + + it { is_expected.to eq(commit.short_sha) } + end + + describe :allow_git_fetch do + subject { build.allow_git_fetch } + + it { is_expected.to eq(project.allow_git_fetch) } + end + + describe :project do + subject { build.project } + + it { is_expected.to eq(commit.project) } + end + + describe :project_id do + subject { build.project_id } + + it { is_expected.to eq(commit.project_id) } + end + + describe :project_name do + subject { build.project_name } + + it { is_expected.to eq(project.name) } + end + + describe :repo_url do + subject { build.repo_url } + + it { is_expected.to eq(project.repo_url_with_auth) } + end + + describe :extract_coverage do + context 'valid content & regex' do + subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } + + it { is_expected.to eq(98.29) } + end + + context 'valid content & bad regex' do + subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') } + + it { is_expected.to be_nil } + end + + context 'no coverage content & regex' do + subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') } + + it { is_expected.to be_nil } + end + + context 'multiple results in content & regex' do + subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') } + + it { is_expected.to eq(98.29) } + end + end + + describe :variables do + context 'returns variables' do + subject { build.variables } + + let(:variables) do + [ + { key: :DB_NAME, value: 'postgres', public: true } + ] + end + + it { is_expected.to eq(variables) } + + context 'and secure variables' do + let(:secure_variables) do + [ + { key: 'SECRET_KEY', value: 'secret_value', public: false } + ] + end + + before do + build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + end + + it { is_expected.to eq(variables + secure_variables) } + + context 'and trigger variables' do + let(:trigger) { FactoryGirl.create :ci_trigger, project: project } + let(:trigger_request) { FactoryGirl.create :ci_trigger_request_with_variables, commit: commit, trigger: trigger } + let(:trigger_variables) do + [ + { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } + ] + end + + before do + build.trigger_request = trigger_request + end + + it { is_expected.to eq(variables + secure_variables + trigger_variables) } + end + end + end + end + + describe :project_recipients do + let(:pusher_email) { 'pusher@gitlab.test' } + let(:user) { User.new(notification_email: pusher_email) } + subject { build.project_recipients } + + before do + build.update_attributes(user: user) + end + + it 'should return pusher_email as only recipient when no additional recipients are given' do + project.update_attributes(email_add_pusher: true, + email_recipients: '') + is_expected.to eq([pusher_email]) + end + + it 'should return pusher_email and additional recipients' do + project.update_attributes(email_add_pusher: true, + email_recipients: 'rec1 rec2') + is_expected.to eq(['rec1', 'rec2', pusher_email]) + end + + it 'should return recipients' do + project.update_attributes(email_add_pusher: false, + email_recipients: 'rec1 rec2') + is_expected.to eq(['rec1', 'rec2']) + end + + it 'should return unique recipients only' do + project.update_attributes(email_add_pusher: true, + email_recipients: "rec1 rec1 #{pusher_email}") + is_expected.to eq(['rec1', pusher_email]) + end + end +end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb new file mode 100644 index 00000000000..acff1ddf0fc --- /dev/null +++ b/spec/models/ci/commit_spec.rb @@ -0,0 +1,307 @@ +# == Schema Information +# +# Table name: commits +# +# id :integer not null, primary key +# project_id :integer +# ref :string(255) +# sha :string(255) +# before_sha :string(255) +# push_data :text +# created_at :datetime +# updated_at :datetime +# tag :boolean default(FALSE) +# yaml_errors :text +# committed_at :datetime +# + +require 'spec_helper' + +describe Ci::Commit do + let(:project) { FactoryGirl.create :ci_project } + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + + it { is_expected.to belong_to(:gl_project) } + it { is_expected.to have_many(:builds) } + it { is_expected.to validate_presence_of :sha } + + it { is_expected.to respond_to :git_author_name } + it { is_expected.to respond_to :git_author_email } + it { is_expected.to respond_to :short_sha } + + describe :last_build do + subject { commit.last_build } + before do + @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday + @second = FactoryGirl.create :ci_build, commit: commit + end + + it { is_expected.to be_a(Ci::Build) } + it('returns with the most recently created build') { is_expected.to eq(@second) } + end + + describe :retry do + before do + @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday + @second = FactoryGirl.create :ci_build, commit: commit + end + + it "creates new build" do + expect(commit.builds.count(:all)).to eq 2 + commit.retry + expect(commit.builds.count(:all)).to eq 3 + end + end + + describe :valid_commit_sha do + context 'commit.sha can not start with 00000000' do + before do + commit.sha = '0' * 40 + commit.valid_commit_sha + end + + it('commit errors should not be empty') { expect(commit.errors).not_to be_empty } + end + end + + describe :short_sha do + subject { commit.short_sha } + + it 'has 8 items' do + expect(subject.size).to eq(8) + end + it { expect(commit.sha).to start_with(subject) } + end + + describe :stage do + subject { commit.stage } + + before do + @second = FactoryGirl.create :ci_build, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: :pending + @first = FactoryGirl.create :ci_build, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: :pending + end + + it 'returns first running stage' do + is_expected.to eq('test') + end + + context 'first build succeeded' do + before do + @first.update_attributes(status: :success) + end + + it 'returns last running stage' do + is_expected.to eq('deploy') + end + end + + context 'all builds succeeded' do + before do + @first.update_attributes(status: :success) + @second.update_attributes(status: :success) + end + + it 'returns nil' do + is_expected.to be_nil + end + end + end + + describe :create_next_builds do + end + + describe :create_builds do + let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + + def create_builds(trigger_request = nil) + commit.create_builds('master', false, nil, trigger_request) + end + + def create_next_builds(trigger_request = nil) + commit.create_next_builds('master', false, nil, trigger_request) + end + + it 'creates builds' do + expect(create_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + + expect(create_next_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(4) + + expect(create_next_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(5) + + expect(create_next_builds).to be_falsey + end + + context 'for different ref' do + def create_develop_builds + commit.create_builds('develop', false, nil, nil) + end + + it 'creates builds' do + expect(create_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + + expect(create_develop_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(4) + expect(commit.refs.size).to eq(2) + expect(commit.builds.pluck(:name).uniq.size).to eq(2) + end + end + + context 'for build triggers' do + let(:trigger) { FactoryGirl.create :ci_trigger, project: project } + let(:trigger_request) { FactoryGirl.create :ci_trigger_request, commit: commit, trigger: trigger } + + it 'creates builds' do + expect(create_builds(trigger_request)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + end + + it 'rebuilds commit' do + expect(create_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + + expect(create_builds(trigger_request)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(4) + end + + it 'creates next builds' do + expect(create_builds(trigger_request)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + + expect(create_next_builds(trigger_request)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(4) + end + + context 'for [ci skip]' do + before do + allow(commit).to receive(:git_commit_message) { 'message [ci skip]' } + end + + it 'rebuilds commit' do + expect(commit.status).to eq('skipped') + expect(create_builds(trigger_request)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + expect(commit.status).to eq('pending') + end + end + end + end + + describe "#finished_at" do + let(:commit) { FactoryGirl.create :ci_commit } + + it "returns finished_at of latest build" do + build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60 + FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120 + + expect(commit.finished_at.to_i).to eq(build.finished_at.to_i) + end + + it "returns nil if there is no finished build" do + FactoryGirl.create :ci_not_started_build, commit: commit + + expect(commit.finished_at).to be_nil + end + end + + describe "coverage" do + let(:project) { FactoryGirl.create :ci_project, coverage_regex: "/.*/" } + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + + it "calculates average when there are two builds with coverage" do + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit + expect(commit.coverage).to eq("35.00") + end + + it "calculates average when there are two builds with coverage and one with nil" do + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit + FactoryGirl.create :ci_build, commit: commit + expect(commit.coverage).to eq("35.00") + end + + it "calculates average when there are two builds with coverage and one is retried" do + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit + FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, commit: commit + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit + expect(commit.coverage).to eq("35.00") + end + + it "calculates average when there is one build without coverage" do + FactoryGirl.create :ci_build, commit: commit + expect(commit.coverage).to be_nil + end + end + + describe :should_create_next_builds? do + before do + @build1 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: false, status: :success + @build2 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'develop', tag: false, status: :failed + @build3 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: true, status: :failed + @build4 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: :success + end + + context 'for success' do + it 'to create if all succeeded' do + expect(commit.should_create_next_builds?(@build4)).to be_truthy + end + end + + context 'for failed' do + before do + @build4.update_attributes(status: :failed) + end + + it 'to not create' do + expect(commit.should_create_next_builds?(@build4)).to be_falsey + end + + context 'and ignore failures for current' do + before do + @build4.update_attributes(allow_failure: true) + end + + it 'to create' do + expect(commit.should_create_next_builds?(@build4)).to be_truthy + end + end + end + + context 'for running' do + before do + @build4.update_attributes(status: :running) + end + + it 'to not create' do + expect(commit.should_create_next_builds?(@build4)).to be_falsey + end + end + + context 'for retried' do + before do + @build5 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: :failed + end + + it 'to not create' do + expect(commit.should_create_next_builds?(@build4)).to be_falsey + end + end + end +end diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb new file mode 100644 index 00000000000..e23d6ae2c28 --- /dev/null +++ b/spec/models/ci/project_services/hip_chat_message_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Ci::HipChatMessage do + subject { Ci::HipChatMessage.new(build) } + + let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } + + let(:build) do + commit.builds.first + end + + context 'when all matrix builds succeed' do + it 'returns a successful message' do + commit.create_builds('master', false, nil) + commit.builds.update_all(status: "success") + commit.reload + + expect(subject.status_color).to eq 'green' + expect(subject.notify?).to be_falsey + expect(subject.to_s).to match(/Commit #\d+/) + expect(subject.to_s).to match(/Successful in \d+ second\(s\)\./) + end + end + + context 'when at least one matrix build fails' do + it 'returns a failure message' do + commit.create_builds('master', false, nil) + first_build = commit.builds.first + second_build = commit.builds.last + first_build.update(status: "success") + second_build.update(status: "failed") + + expect(subject.status_color).to eq 'red' + expect(subject.notify?).to be_truthy + expect(subject.to_s).to match(/Commit #\d+/) + expect(subject.to_s).to match(/Failed in \d+ second\(s\)\./) + end + end +end diff --git a/spec/models/ci/project_services/hip_chat_service_spec.rb b/spec/models/ci/project_services/hip_chat_service_spec.rb new file mode 100644 index 00000000000..d9ccc855edf --- /dev/null +++ b/spec/models/ci/project_services/hip_chat_service_spec.rb @@ -0,0 +1,73 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + + +require 'spec_helper' + +describe Ci::HipChatService do + + describe "Validations" do + + context "active" do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of :hipchat_room } + it { is_expected.to validate_presence_of :hipchat_token } + + end + end + + describe "Execute" do + + let(:service) { Ci::HipChatService.new } + let(:commit) { FactoryGirl.create :ci_commit } + let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' } + let(:api_url) { 'https://api.hipchat.com/v2/room/123/notification?auth_token=a1b2c3d4e5f6' } + + before do + allow(service).to receive_messages( + project: commit.project, + project_id: commit.project_id, + notify_only_broken_builds: false, + hipchat_room: 123, + hipchat_token: 'a1b2c3d4e5f6' + ) + + WebMock.stub_request(:post, api_url) + end + + + it "should call the HipChat API" do + service.execute(build) + Ci::HipChatNotifierWorker.drain + + expect( WebMock ).to have_requested(:post, api_url).once + end + + it "calls the worker with expected arguments" do + expect( Ci::HipChatNotifierWorker ).to receive(:perform_async) \ + .with(an_instance_of(String), hash_including( + token: 'a1b2c3d4e5f6', + room: 123, + server: 'https://api.hipchat.com', + color: 'red', + notify: true + )) + + service.execute(build) + end + end +end diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb new file mode 100644 index 00000000000..04e870dce7f --- /dev/null +++ b/spec/models/ci/project_services/mail_service_spec.rb @@ -0,0 +1,191 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +require 'spec_helper' + +describe Ci::MailService do + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe "Validations" do + context "active" do + before do + subject.active = true + end + end + end + + describe 'Sends email for' do + let(:mail) { Ci::MailService.new } + let(:user) { User.new(notification_email: 'git@example.com')} + + describe 'failed build' do + let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } + let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) } + + before do + allow(mail).to receive_messages( + project: project + ) + end + + it do + should_email("git@example.com") + mail.execute(build) + end + + def should_email(email) + expect(Ci::Notify).to receive(:build_fail_email).with(build.id, email) + expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email) + end + end + + describe 'successfull build' do + let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } + let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + + before do + allow(mail).to receive_messages( + project: project + ) + end + + it do + should_email("git@example.com") + mail.execute(build) + end + + def should_email(email) + expect(Ci::Notify).to receive(:build_success_email).with(build.id, email) + expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email) + end + end + + describe 'successfull build and project has email_recipients' do + let(:project) do + FactoryGirl.create(:ci_project, + email_add_pusher: true, + email_only_broken_builds: false, + email_recipients: "jeroen@example.com") + end + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } + let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + + before do + allow(mail).to receive_messages( + project: project + ) + end + + it do + should_email("git@example.com") + should_email("jeroen@example.com") + mail.execute(build) + end + + def should_email(email) + expect(Ci::Notify).to receive(:build_success_email).with(build.id, email) + expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email) + end + end + + describe 'successful build and notify only broken builds' do + let(:project) do + FactoryGirl.create(:ci_project, + email_add_pusher: true, + email_only_broken_builds: true, + email_recipients: "jeroen@example.com") + end + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } + let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + + before do + allow(mail).to receive_messages( + project: project + ) + end + + it do + should_email(commit.git_author_email) + should_email("jeroen@example.com") + mail.execute(build) if mail.can_execute?(build) + end + + def should_email(email) + expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email) + expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email) + end + end + + describe 'successful build and can test service' do + let(:project) do + FactoryGirl.create(:ci_project, + email_add_pusher: true, + email_only_broken_builds: false, + email_recipients: "jeroen@example.com") + end + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } + let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + + before do + allow(mail).to receive_messages( + project: project + ) + build + end + + it do + expect(mail.can_test?).to eq(true) + end + end + + describe 'retried build should not receive email' do + let(:project) do + FactoryGirl.create(:ci_project, + email_add_pusher: true, + email_only_broken_builds: true, + email_recipients: "jeroen@example.com") + end + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } + let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) } + + before do + allow(mail).to receive_messages( + project: project + ) + end + + it do + Ci::Build.retry(build) + should_email(commit.git_author_email) + should_email("jeroen@example.com") + mail.execute(build) if mail.can_execute?(build) + end + + def should_email(email) + expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email) + expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email) + end + end + end +end diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb new file mode 100644 index 00000000000..8adda6c86cc --- /dev/null +++ b/spec/models/ci/project_services/slack_message_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Ci::SlackMessage do + subject { Ci::SlackMessage.new(commit) } + + let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) } + + context 'when all matrix builds succeeded' do + let(:color) { 'good' } + + it 'returns a message with success' do + commit.create_builds('master', false, nil) + commit.builds.update_all(status: "success") + commit.reload + + expect(subject.color).to eq(color) + expect(subject.fallback).to include('Commit') + expect(subject.fallback).to include("\##{commit.id}") + expect(subject.fallback).to include('succeeded') + expect(subject.attachments.first[:fields]).to be_empty + end + end + + context 'when one of matrix builds failed' do + let(:color) { 'danger' } + + it 'returns a message with information about failed build' do + commit.create_builds('master', false, nil) + first_build = commit.builds.first + second_build = commit.builds.last + first_build.update(status: "success") + second_build.update(status: "failed") + + expect(subject.color).to eq(color) + expect(subject.fallback).to include('Commit') + expect(subject.fallback).to include("\##{commit.id}") + expect(subject.fallback).to include('failed') + expect(subject.attachments.first[:fields].size).to eq(1) + expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name) + expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}") + end + end +end diff --git a/spec/models/ci/project_services/slack_service_spec.rb b/spec/models/ci/project_services/slack_service_spec.rb new file mode 100644 index 00000000000..1ac7dfe568d --- /dev/null +++ b/spec/models/ci/project_services/slack_service_spec.rb @@ -0,0 +1,57 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +require 'spec_helper' + +describe Ci::SlackService do + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe "Validations" do + context "active" do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of :webhook } + end + end + + describe "Execute" do + let(:slack) { Ci::SlackService.new } + let(:commit) { FactoryGirl.create :ci_commit } + let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' } + let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } + let(:notify_only_broken_builds) { false } + + before do + allow(slack).to receive_messages( + project: commit.project, + project_id: commit.project_id, + webhook: webhook_url, + notify_only_broken_builds: notify_only_broken_builds + ) + + WebMock.stub_request(:post, webhook_url) + end + + it "should call Slack API" do + slack.execute(build) + Ci::SlackNotifierWorker.drain + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end +end diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb new file mode 100644 index 00000000000..dec4720a711 --- /dev/null +++ b/spec/models/ci/project_spec.rb @@ -0,0 +1,263 @@ +# == Schema Information +# +# Table name: projects +# +# id :integer not null, primary key +# name :string(255) not null +# timeout :integer default(3600), not null +# created_at :datetime +# updated_at :datetime +# token :string(255) +# default_ref :string(255) +# path :string(255) +# always_build :boolean default(FALSE), not null +# polling_interval :integer +# public :boolean default(FALSE), not null +# ssh_url_to_repo :string(255) +# gitlab_id :integer +# allow_git_fetch :boolean default(TRUE), not null +# email_recipients :string(255) default(""), not null +# email_add_pusher :boolean default(TRUE), not null +# email_only_broken_builds :boolean default(TRUE), not null +# skip_refs :string(255) +# coverage_regex :string(255) +# shared_runners_enabled :boolean default(FALSE) +# generated_yaml_config :text +# + +require 'spec_helper' + +describe Ci::Project do + let(:gl_project) { FactoryGirl.create :empty_project } + let(:project) { FactoryGirl.create :ci_project, gl_project: gl_project } + subject { project } + + it { is_expected.to have_many(:runner_projects) } + it { is_expected.to have_many(:runners) } + it { is_expected.to have_many(:web_hooks) } + it { is_expected.to have_many(:events) } + it { is_expected.to have_many(:variables) } + it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:services) } + + it { is_expected.to validate_presence_of :timeout } + it { is_expected.to validate_presence_of :gitlab_id } + + describe 'before_validation' do + it 'should set an random token if none provided' do + project = FactoryGirl.create :ci_project_without_token + expect(project.token).not_to eq("") + end + + it 'should not set an random toke if one provided' do + project = FactoryGirl.create :ci_project + expect(project.token).to eq("iPWx6WM4lhHNedGfBpPJNP") + end + end + + describe :name_with_namespace do + subject { project.name_with_namespace } + + it { is_expected.to eq(project.name) } + it { is_expected.to eq(gl_project.name_with_namespace) } + end + + describe :path_with_namespace do + subject { project.path_with_namespace } + + it { is_expected.to eq(project.path) } + it { is_expected.to eq(gl_project.path_with_namespace) } + end + + describe :path_with_namespace do + subject { project.web_url } + + it { is_expected.to eq(gl_project.web_url) } + end + + describe :web_url do + subject { project.web_url } + + it { is_expected.to eq(project.gitlab_url) } + it { is_expected.to eq(gl_project.web_url) } + end + + describe :http_url_to_repo do + subject { project.http_url_to_repo } + + it { is_expected.to eq(gl_project.http_url_to_repo) } + end + + describe :ssh_url_to_repo do + subject { project.ssh_url_to_repo } + + it { is_expected.to eq(gl_project.ssh_url_to_repo) } + end + + describe :commits do + subject { project.commits } + + before do + FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project + end + + it { is_expected.to eq(gl_project.ci_commits) } + end + + describe :builds do + subject { project.builds } + + before do + commit = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project + FactoryGirl.create :ci_build, commit: commit + end + + it { is_expected.to eq(gl_project.ci_builds) } + end + + describe "ordered_by_last_commit_date" do + it "returns ordered projects" do + newest_project = FactoryGirl.create :empty_project + newest_ci_project = newest_project.ensure_gitlab_ci_project + oldest_project = FactoryGirl.create :empty_project + oldest_ci_project = oldest_project.ensure_gitlab_ci_project + project_without_commits = FactoryGirl.create :empty_project + ci_project_without_commits = project_without_commits.ensure_gitlab_ci_project + + FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: newest_project + FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: oldest_project + + expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_ci_project, oldest_ci_project, ci_project_without_commits]) + end + end + + describe 'ordered commits' do + let(:project) { FactoryGirl.create :empty_project } + + it 'returns ordered list of commits' do + commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project + commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project + expect(project.ci_commits).to eq([commit2, commit1]) + end + + it 'returns commits ordered by committed_at and id, with nulls last' do + commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project + commit2 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project + commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project + commit4 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project + expect(project.ci_commits).to eq([commit2, commit4, commit3, commit1]) + end + end + + context :valid_project do + let(:commit) { FactoryGirl.create(:ci_commit) } + + context :project_with_commit_and_builds do + let(:project) { commit.project } + + before do + FactoryGirl.create(:ci_build, commit: commit) + end + + it { expect(project.status).to eq('pending') } + it { expect(project.last_commit).to be_kind_of(Ci::Commit) } + it { expect(project.human_status).to eq('pending') } + end + end + + describe '#email_notification?' do + it do + project = FactoryGirl.create :ci_project, email_add_pusher: true + expect(project.email_notification?).to eq(true) + end + + it do + project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: 'test tesft' + expect(project.email_notification?).to eq(true) + end + + it do + project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: '' + expect(project.email_notification?).to eq(false) + end + end + + describe '#broken_or_success?' do + it do + project = FactoryGirl.create :ci_project, email_add_pusher: true + allow(project).to receive(:broken?).and_return(true) + allow(project).to receive(:success?).and_return(true) + expect(project.broken_or_success?).to eq(true) + end + + it do + project = FactoryGirl.create :ci_project, email_add_pusher: true + allow(project).to receive(:broken?).and_return(true) + allow(project).to receive(:success?).and_return(false) + expect(project.broken_or_success?).to eq(true) + end + + it do + project = FactoryGirl.create :ci_project, email_add_pusher: true + allow(project).to receive(:broken?).and_return(false) + allow(project).to receive(:success?).and_return(true) + expect(project.broken_or_success?).to eq(true) + end + + it do + project = FactoryGirl.create :ci_project, email_add_pusher: true + allow(project).to receive(:broken?).and_return(false) + allow(project).to receive(:success?).and_return(false) + expect(project.broken_or_success?).to eq(false) + end + end + + describe 'Project.parse' do + let(:project) { FactoryGirl.create :project } + + subject { Ci::Project.parse(project) } + + it { is_expected.to be_valid } + it { is_expected.to be_kind_of(Ci::Project) } + it { expect(subject.name).to eq(project.name_with_namespace) } + it { expect(subject.gitlab_id).to eq(project.id) } + it { expect(subject.gitlab_url).to eq(project.web_url) } + end + + describe :repo_url_with_auth do + let(:project) { FactoryGirl.create :ci_project } + subject { project.repo_url_with_auth } + + it { is_expected.to be_a(String) } + it { is_expected.to end_with(".git") } + it { is_expected.to start_with(project.gitlab_url[0..6]) } + it { is_expected.to include(project.token) } + it { is_expected.to include('gitlab-ci-token') } + it { is_expected.to include(project.gitlab_url[7..-1]) } + end + + describe :any_runners do + it "there are no runners available" do + project = FactoryGirl.create(:ci_project) + expect(project.any_runners?).to be_falsey + end + + it "there is a specific runner" do + project = FactoryGirl.create(:ci_project) + project.runners << FactoryGirl.create(:ci_specific_runner) + expect(project.any_runners?).to be_truthy + end + + it "there is a shared runner" do + project = FactoryGirl.create(:ci_project, shared_runners_enabled: true) + FactoryGirl.create(:ci_shared_runner) + expect(project.any_runners?).to be_truthy + end + + it "there is a shared runner, but they are prohibited to use" do + project = FactoryGirl.create(:ci_project) + FactoryGirl.create(:ci_shared_runner) + expect(project.any_runners?).to be_falsey + end + end +end diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb new file mode 100644 index 00000000000..0218d484130 --- /dev/null +++ b/spec/models/ci/runner_project_spec.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: runner_projects +# +# id :integer not null, primary key +# runner_id :integer not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +describe Ci::RunnerProject do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb new file mode 100644 index 00000000000..757593a7ab8 --- /dev/null +++ b/spec/models/ci/runner_spec.rb @@ -0,0 +1,70 @@ +# == Schema Information +# +# Table name: runners +# +# id :integer not null, primary key +# token :string(255) +# created_at :datetime +# updated_at :datetime +# description :string(255) +# contacted_at :datetime +# active :boolean default(TRUE), not null +# is_shared :boolean default(FALSE) +# name :string(255) +# version :string(255) +# revision :string(255) +# platform :string(255) +# architecture :string(255) +# + +require 'spec_helper' + +describe Ci::Runner do + describe '#display_name' do + it 'should return the description if it has a value' do + runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') + expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448' + end + + it 'should return the token if it does not have a description' do + runner = FactoryGirl.create(:ci_runner) + expect(runner.display_name).to eq runner.description + end + + it 'should return the token if the description is an empty string' do + runner = FactoryGirl.build(:ci_runner, description: '') + expect(runner.display_name).to eq runner.token + end + end + + describe :assign_to do + let!(:project) { FactoryGirl.create :ci_project } + let!(:shared_runner) { FactoryGirl.create(:ci_shared_runner) } + + before { shared_runner.assign_to(project) } + + it { expect(shared_runner).to be_specific } + it { expect(shared_runner.projects).to eq([project]) } + it { expect(shared_runner.only_for?(project)).to be_truthy } + end + + describe "belongs_to_one_project?" do + it "returns false if there are two projects runner assigned to" do + runner = FactoryGirl.create(:ci_specific_runner) + project = FactoryGirl.create(:ci_project) + project1 = FactoryGirl.create(:ci_project) + project.runners << runner + project1.runners << runner + + expect(runner.belongs_to_one_project?).to be_falsey + end + + it "returns true" do + runner = FactoryGirl.create(:ci_specific_runner) + project = FactoryGirl.create(:ci_project) + project.runners << runner + + expect(runner.belongs_to_one_project?).to be_truthy + end + end +end diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb new file mode 100644 index 00000000000..2df70e88888 --- /dev/null +++ b/spec/models/ci/service_spec.rb @@ -0,0 +1,48 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +require 'spec_helper' + +describe Ci::Service do + + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe "Mass assignment" do + end + + describe "Test Button" do + before do + @service = Ci::Service.new + end + + describe "Testable" do + let(:commit) { FactoryGirl.create :ci_commit } + let(:build) { FactoryGirl.create :ci_build, commit: commit } + + before do + allow(@service).to receive_messages( + project: commit.project + ) + build + @testable = @service.can_test? + end + + describe :can_test do + it { expect(@testable).to eq(true) } + end + end + end +end diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb new file mode 100644 index 00000000000..19c14ef2da2 --- /dev/null +++ b/spec/models/ci/trigger_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Ci::Trigger do + let(:project) { FactoryGirl.create :ci_project } + + describe 'before_validation' do + it 'should set an random token if none provided' do + trigger = FactoryGirl.create :ci_trigger_without_token, project: project + expect(trigger.token).not_to be_nil + end + + it 'should not set an random token if one provided' do + trigger = FactoryGirl.create :ci_trigger, project: project + expect(trigger.token).to eq('token') + end + end +end diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb new file mode 100644 index 00000000000..d034a6c7b9f --- /dev/null +++ b/spec/models/ci/variable_spec.rb @@ -0,0 +1,45 @@ +# == Schema Information +# +# Table name: variables +# +# id :integer not null, primary key +# project_id :integer not null +# key :string(255) +# value :text +# encrypted_value :text +# encrypted_value_salt :string(255) +# encrypted_value_iv :string(255) +# + +require 'spec_helper' + +describe Ci::Variable do + subject { Ci::Variable.new } + + let(:secret_value) { 'secret' } + + before :each do + subject.value = secret_value + end + + describe :value do + it 'stores the encrypted value' do + expect(subject.encrypted_value).not_to be_nil + end + + it 'stores an iv for value' do + expect(subject.encrypted_value_iv).not_to be_nil + end + + it 'stores a salt for value' do + expect(subject.encrypted_value_salt).not_to be_nil + end + + it 'fails to decrypt if iv is incorrect' do + subject.encrypted_value_iv = nil + subject.instance_variable_set(:@value, nil) + expect { subject.value }. + to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt') + end + end +end diff --git a/spec/models/ci/web_hook_spec.rb b/spec/models/ci/web_hook_spec.rb new file mode 100644 index 00000000000..bf9481ab81d --- /dev/null +++ b/spec/models/ci/web_hook_spec.rb @@ -0,0 +1,63 @@ +# == Schema Information +# +# Table name: web_hooks +# +# id :integer not null, primary key +# url :string(255) not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +describe Ci::WebHook do + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe "Validations" do + it { is_expected.to validate_presence_of(:url) } + + context "url format" do + it { is_expected.to allow_value("http://example.com").for(:url) } + it { is_expected.to allow_value("https://excample.com").for(:url) } + it { is_expected.to allow_value("http://test.com/api").for(:url) } + it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) } + it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) } + + it { is_expected.not_to allow_value("example.com").for(:url) } + it { is_expected.not_to allow_value("ftp://example.com").for(:url) } + it { is_expected.not_to allow_value("herp-and-derp").for(:url) } + end + end + + describe "execute" do + before(:each) do + @web_hook = FactoryGirl.create(:ci_web_hook) + @project = @web_hook.project + @data = { before: 'oldrev', after: 'newrev', ref: 'ref' } + + WebMock.stub_request(:post, @web_hook.url) + end + + it "POSTs to the web hook URL" do + @web_hook.execute(@data) + expect(WebMock).to have_requested(:post, @web_hook.url).once + end + + it "POSTs the data as JSON" do + json = @data.to_json + + @web_hook.execute(@data) + expect(WebMock).to have_requested(:post, @web_hook.url).with(body: json).once + end + + it "catches exceptions" do + expect(Ci::WebHook).to receive(:post).and_raise("Some HTTP Post error") + + expect{ @web_hook.execute(@data) }. + to raise_error(RuntimeError, 'Some HTTP Post error') + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index b6d80451d2e..8f706f8934b 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Issue, "Issuable" do let(:issue) { create(:issue) } + let(:user) { create(:user) } describe "Associations" do it { is_expected.to belong_to(:project) } @@ -66,4 +67,19 @@ describe Issue, "Issuable" do expect(issue.new?).to be_falsey end end + + + describe "#to_hook_data" do + let(:hook_data) { issue.to_hook_data(user) } + + it "returns correct hook data" do + expect(hook_data[:object_kind]).to eq("issue") + expect(hook_data[:user]).to eq(user.hook_attrs) + expect(hook_data[:repository][:name]).to eq(issue.project.name) + expect(hook_data[:repository][:url]).to eq(issue.project.url_to_repo) + expect(hook_data[:repository][:description]).to eq(issue.project.description) + expect(hook_data[:repository][:homepage]).to eq(issue.project.web_url) + expect(hook_data[:object_attributes]).to eq(issue.hook_attrs) + end + end end diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index dae7e399cfb..a2dc66fce3e 100644 --- a/spec/models/hooks/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -22,7 +22,7 @@ describe ProjectHook do describe '.push_hooks' do it 'should return hooks for push events only' do hook = create(:project_hook, push_events: true) - hook2 = create(:project_hook, push_events: false) + create(:project_hook, push_events: false) expect(ProjectHook.push_hooks).to eq([hook]) end end @@ -30,7 +30,7 @@ describe ProjectHook do describe '.tag_push_hooks' do it 'should return hooks for tag push events only' do hook = create(:project_hook, tag_push_events: true) - hook2 = create(:project_hook, tag_push_events: false) + create(:project_hook, tag_push_events: false) expect(ProjectHook.tag_push_hooks).to eq([hook]) end end diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 4c8b8910ae7..16641c12124 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -39,8 +39,6 @@ describe ServiceHook do end it "POSTs the data as JSON" do - json = @data.to_json - @service_hook.execute(@data) expect(WebMock).to have_requested(:post, @service_hook.url).with( headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' } diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 23f30881d99..2fdc49f02ee 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -60,8 +60,6 @@ describe ProjectHook do end it "POSTs the data as JSON" do - json = @data.to_json - @project_hook.execute(@data, 'push_hooks') expect(WebMock).to have_requested(:post, @project_hook.url).with( headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 9bac451c28c..cf336d82957 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -2,19 +2,20 @@ # # Table name: issues # -# id :integer not null, primary key -# title :string(255) -# assignee_id :integer -# author_id :integer -# project_id :integer -# created_at :datetime -# updated_at :datetime -# position :integer default(0) -# branch_name :string(255) -# description :text -# milestone_id :integer -# state :string(255) -# iid :integer +# id :integer not null, primary key +# title :string(255) +# assignee_id :integer +# author_id :integer +# project_id :integer +# created_at :datetime +# updated_at :datetime +# position :integer default(0) +# branch_name :string(255) +# description :text +# milestone_id :integer +# state :string(255) +# iid :integer +# updated_by_id :integer # require 'spec_helper' diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index b91687bc09f..17a49013d25 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -19,6 +19,7 @@ # description :text # position :integer default(0) # locked_at :datetime +# updated_by_id :integer # require 'spec_helper' diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 36352e1ecce..c88d5349663 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -111,8 +111,8 @@ describe Milestone do describe :is_empty? do before do - issue = create :closed_issue, milestone: milestone - merge_request = create :merge_request, milestone: milestone + create :closed_issue, milestone: milestone + create :merge_request, milestone: milestone end it 'Should return total count of issues and merge requests assigned to milestone' do @@ -125,7 +125,7 @@ describe Milestone do milestone = create :milestone create :closed_issue, milestone: milestone - issue = create :issue + create :issue end it 'should be true if milestone active and all nested issues closed' do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 331505a01b3..3a0b194ba1e 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -15,6 +15,7 @@ # noteable_id :integer # system :boolean default(FALSE), not null # st_diff :text +# updated_by_id :integer # require 'spec_helper' diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 9445d96c337..230807ea672 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -63,19 +63,5 @@ describe BuildkiteService do ) end end - - describe :builds_page do - it 'returns the correct path to the builds page' do - expect(@service.builds_path).to eq( - 'https://buildkite.com/account-name/example-project/builds?branch=default-brancho' - ) - end - end - - describe :status_img_path do - it 'returns the correct path to the status image' do - expect(@service.status_img_path).to eq('https://badge.buildkite.com/secret-sauce-status-token.svg') - end - end end end diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb new file mode 100644 index 00000000000..e9967f5fe0b --- /dev/null +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -0,0 +1,104 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe DroneCiService do + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_one(:service_hook) } + end + + describe 'validations' do + context 'active' do + before { allow(subject).to receive(:activated?).and_return(true) } + + it { is_expected.to validate_presence_of(:token) } + it { is_expected.to validate_presence_of(:drone_url) } + it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) } + it { is_expected.to allow_value('http://ci.example.com').for(:drone_url) } + it { is_expected.not_to allow_value('this is not url').for(:drone_url) } + it { is_expected.not_to allow_value('http//noturl').for(:drone_url) } + it { is_expected.not_to allow_value('ftp://ci.example.com').for(:drone_url) } + end + + context 'inactive' do + before { allow(subject).to receive(:activated?).and_return(false) } + + it { is_expected.not_to validate_presence_of(:token) } + it { is_expected.not_to validate_presence_of(:drone_url) } + it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) } + it { is_expected.to allow_value('http://drone.example.com').for(:drone_url) } + it { is_expected.to allow_value('ftp://drone.example.com').for(:drone_url) } + end + end + + shared_context :drone_ci_service do + let(:drone) { DroneCiService.new } + let(:project) { create(:project, name: 'project') } + let(:path) { "#{project.namespace.path}/#{project.path}" } + let(:drone_url) { 'http://drone.example.com' } + let(:sha) { '2ab7834c' } + let(:branch) { 'dev' } + let(:token) { 'secret' } + let(:iid) { rand(1..9999) } + + before(:each) do + allow(drone).to receive_messages( + project_id: project.id, + project: project, + active: true, + drone_url: drone_url, + token: token + ) + end + end + + describe "service page/path methods" do + include_context :drone_ci_service + + # URL's + let(:commit_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" } + let(:merge_request_page) { "#{drone_url}/gitlab/#{path}/redirect/pulls/#{iid}" } + let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" } + let(:merge_request_status_path) { "#{drone_url}/gitlab/#{path}/pulls/#{iid}?access_token=#{token}" } + + it { expect(drone.build_page(sha, branch)).to eq(commit_page) } + it { expect(drone.commit_page(sha, branch)).to eq(commit_page) } + it { expect(drone.merge_request_page(iid, sha, branch)).to eq(merge_request_page) } + it { expect(drone.commit_status_path(sha, branch)).to eq(commit_status_path) } + it { expect(drone.merge_request_status_path(iid, sha, branch)).to eq(merge_request_status_path) } + end + + describe "execute" do + include_context :drone_ci_service + + let(:user) { create(:user, username: 'username') } + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + it do + service_hook = double + expect(service_hook).to receive(:execute) + expect(drone).to receive(:service_hook).and_return(service_hook) + + drone.execute(push_sample_data) + end + end +end diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index a14384c87b4..842089ebe0d 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -26,51 +26,20 @@ describe GitlabCiService do it { is_expected.to have_one(:service_hook) } end - describe 'validations' do - context 'active' do - before { allow(subject).to receive(:activated?).and_return(true) } - - it { is_expected.to validate_presence_of(:token) } - it { is_expected.to validate_presence_of(:project_url) } - it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) } - it { is_expected.to allow_value('http://ci.example.com/project/1').for(:project_url) } - it { is_expected.not_to allow_value('token with spaces').for(:token) } - it { is_expected.not_to allow_value('token/with%spaces').for(:token) } - it { is_expected.not_to allow_value('this is not url').for(:project_url) } - it { is_expected.not_to allow_value('http//noturl').for(:project_url) } - it { is_expected.not_to allow_value('ftp://ci.example.com/projects/3').for(:project_url) } - end - - context 'inactive' do - before { allow(subject).to receive(:activated?).and_return(false) } - - it { is_expected.not_to validate_presence_of(:token) } - it { is_expected.not_to validate_presence_of(:project_url) } - it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) } - it { is_expected.to allow_value('http://ci.example.com/project/1').for(:project_url) } - it { is_expected.to allow_value('token with spaces').for(:token) } - it { is_expected.to allow_value('ftp://ci.example.com/projects/3').for(:project_url) } - end - end - describe 'commits methods' do before do + @ci_project = create(:ci_project) @service = GitlabCiService.new allow(@service).to receive_messages( service_hook: true, project_url: 'http://ci.gitlab.org/projects/2', - token: 'verySecret' + token: 'verySecret', + project: @ci_project.gl_project ) end - describe :commit_status_path do - it { expect(@service.commit_status_path("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c/status.json?token=verySecret")} - it { expect(@service.commit_status_path("issue#2", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/issue%232/status.json?token=verySecret")} - end - describe :build_page do - it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c")} - it { expect(@service.build_page("issue#2", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/issue%232")} + it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/#{@ci_project.gl_project.path_with_namespace}/commit/2ab7834c/ci")} end describe "execute" do @@ -78,35 +47,11 @@ describe GitlabCiService do let(:project) { create(:project, name: 'project') } let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } - it "calls ci_yaml_file" do - service_hook = double - expect(service_hook).to receive(:execute) - expect(@service).to receive(:service_hook).and_return(service_hook) - expect(@service).to receive(:ci_yaml_file).with(push_sample_data[:checkout_sha]) + it "calls CreateCommitService" do + expect_any_instance_of(Ci::CreateCommitService).to receive(:execute).with(@ci_project, user, push_sample_data) @service.execute(push_sample_data) end end end - - describe "Fork registration" do - before do - @old_project = create(:empty_project) - @project = create(:empty_project) - @user = create(:user) - - @service = GitlabCiService.new - allow(@service).to receive_messages( - service_hook: true, - project_url: 'http://ci.gitlab.org/projects/2', - token: 'verySecret', - project: @old_project - ) - end - - it "performs http reuquest to ci" do - stub_request(:post, "http://ci.gitlab.org/api/v1/forks") - @service.fork_registration(@project, @user.private_token) - end - end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 65d16beef91..f67d7b30980 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -87,7 +87,7 @@ describe HipchatService do it "should create a push message" do message = hipchat.send(:create_push_message, push_sample_data) - obj_attr = push_sample_data[:object_attributes] + push_sample_data[:object_attributes] branch = push_sample_data[:ref].gsub('refs/heads/', '') expect(message).to include("#{user.name} pushed to branch " \ "#{branch} of " \ @@ -107,7 +107,7 @@ describe HipchatService do it "should create a tag push message" do message = hipchat.send(:create_push_message, push_sample_data) - obj_attr = push_sample_data[:object_attributes] + push_sample_data[:object_attributes] expect(message).to eq("#{user.name} pushed new tag " \ "test to " \ "#{project_name}\n") diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2fcbd5ae108..8b5d2c3a1c1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -140,7 +140,7 @@ describe Project do describe 'last_activity_date' do it 'returns the creation date of the project\'s last event if present' do - last_activity_event = create(:event, project: project) + create(:event, project: project) expect(project.last_activity_at.to_i).to eq(last_event.created_at.to_i) end @@ -220,6 +220,7 @@ describe Project do end it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) } + it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) } it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil } end end @@ -401,4 +402,25 @@ describe Project do it { should eq "http://localhost#{avatar_path}" } end end + + describe :ci_commit do + let(:project) { create :project } + let(:commit) { create :ci_commit, gl_project: project } + + before do + project.ensure_gitlab_ci_project + project.create_gitlab_ci_service(active: true) + end + + it { expect(project.ci_commit(commit.sha)).to eq(commit) } + end + + describe :enable_ci do + let(:project) { create :project } + + before { project.enable_ci } + + it { expect(project.gitlab_ci?).to be_truthy } + it { expect(project.gitlab_ci_project).to be_a(Ci::Project) } + end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index cc1138490a0..26e8fdae472 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -66,4 +66,16 @@ describe ProjectTeam do it { expect(project.team.member?(guest)).to be_truthy } end end + + describe "#human_max_access" do + it "return master role" do + user = create :user + group = create :group + group.add_users([user.id], GroupMember::MASTER) + project = create(:project, namespace: group) + project.team << [user, :guest] + + expect(project.team.human_max_access(user.id)).to eq("Master") + end + end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index f785203af7d..94802dcfb79 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -231,7 +231,7 @@ describe ProjectWiki do end def commit_details - commit = { name: user.name, email: user.email, message: "test commit" } + { name: user.name, email: user.email, message: "test commit" } end def create_page(name, content) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a46e789eab4..c71cfb3ebe3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,62 +2,58 @@ # # 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 -# 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 -# github_access_token :string(255) -# gitlab_access_token :string(255) -# notification_email :string(255) -# hide_no_password :boolean default(FALSE) -# password_automatically_set :boolean default(FALSE) -# bitbucket_access_token :string(255) -# bitbucket_access_token_secret :string(255) -# 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) +# 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) # require 'spec_helper' @@ -89,6 +85,7 @@ describe User do it { is_expected.to have_many(:merge_requests).dependent(:destroy) } it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) } it { is_expected.to have_many(:identities).dependent(:destroy) } + it { is_expected.to have_one(:abuse_report) } end describe 'validations' do @@ -192,7 +189,7 @@ describe User do end it 'confirms a user' do - user.confirm! + user.confirm expect(user.confirmed?).to be_truthy end end @@ -231,6 +228,26 @@ describe User do end end + describe '#recently_sent_password_reset?' do + it 'is false when reset_password_sent_at is nil' do + user = build_stubbed(:user, reset_password_sent_at: nil) + + expect(user.recently_sent_password_reset?).to eq false + end + + it 'is false when sent more than one minute ago' do + user = build_stubbed(:user, reset_password_sent_at: 5.minutes.ago) + + expect(user.recently_sent_password_reset?).to eq false + end + + it 'is true when sent less than one minute ago' do + user = build_stubbed(:user, reset_password_sent_at: Time.now) + + expect(user.recently_sent_password_reset?).to eq true + end + end + describe '#disable_two_factor!' do it 'clears all 2FA-related fields' do user = create(:user, :two_factor) diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index dc84a14bb40..d7802d1734f 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -196,7 +196,7 @@ describe WikiPage do end def commit_details - commit = { name: user.name, email: user.email, message: "test commit" } + { name: user.name, email: user.email, message: "test commit" } end def create_page(name, content) diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb new file mode 100644 index 00000000000..d2b87f88712 --- /dev/null +++ b/spec/requests/api/keys_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:key) { create(:key, user: user) } + let(:email) { create(:email, user: user) } + + describe 'GET /keys/:uid' do + before { admin } + + context 'when unauthenticated' do + it 'should return authentication error' do + get api("/keys/#{key.id}") + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'should return 404 for non-existing key' do + get api('/keys/999999', admin) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') + end + + it 'should return single ssh key with user information' do + user.keys << key + user.save + get api("/keys/#{key.id}", admin) + expect(response.status).to eq(200) + expect(json_response['title']).to eq(key.title) + expect(json_response['user']['id']).to eq(user.id) + expect(json_response['user']['username']).to eq(user.username) + end + end + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 942768fa254..35b3d3e296a 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -2,11 +2,12 @@ require "spec_helper" describe API::API, api: true do include ApiHelpers + let(:base_time) { Time.now } let(:user) { create(:user) } let!(:project) {create(:project, creator_id: user.id, namespace: user.namespace) } - let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") } - let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test") } - let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test") } + let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } + let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.seconds) } + let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } @@ -74,8 +75,8 @@ describe API::API, api: true do expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.length).to eq(3) - expect(json_response.last['id']).to eq(@mr_earlier.id) - expect(json_response.first['id']).to eq(@mr_later.id) + response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort) end it "should return an array of merge_requests in descending order" do @@ -83,8 +84,8 @@ describe API::API, api: true do expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.length).to eq(3) - expect(json_response.first['id']).to eq(@mr_later.id) - expect(json_response.last['id']).to eq(@mr_earlier.id) + response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort.reverse) end it "should return an array of merge_requests ordered by updated_at" do @@ -92,17 +93,17 @@ describe API::API, api: true do expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.length).to eq(3) - expect(json_response.last['id']).to eq(@mr_earlier.id) - expect(json_response.first['id']).to eq(@mr_later.id) + response_dates = json_response.map{ |merge_request| merge_request['updated_at'] } + expect(response_dates).to eq(response_dates.sort.reverse) end it "should return an array of merge_requests ordered by created_at" do - get api("/projects/#{project.id}/merge_requests?sort=created_at", user) + get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.length).to eq(3) - expect(json_response.last['id']).to eq(@mr_earlier.id) - expect(json_response.first['id']).to eq(@mr_later.id) + response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort) end end end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 5037575d355..606b226ad77 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -5,7 +5,7 @@ describe API::API, 'ProjectHooks', api: true do let(:user) { create(:user) } let(:user3) { create(:user) } let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let!(:hook) { create(:project_hook, project: project, url: "http://example.com") } + let!(:hook) { create(:project_hook, project: project, url: "http://example.com", push_events: true, merge_requests_events: true, tag_push_events: true, issues_events: true, note_events: true, enable_ssl_verification: true) } before do project.team << [user, :master] @@ -21,6 +21,12 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response).to be_an Array expect(json_response.count).to eq(1) expect(json_response.first['url']).to eq("http://example.com") + expect(json_response.first['issues_events']).to eq(true) + expect(json_response.first['push_events']).to eq(true) + expect(json_response.first['merge_requests_events']).to eq(true) + expect(json_response.first['tag_push_events']).to eq(true) + expect(json_response.first['note_events']).to eq(true) + expect(json_response.first['enable_ssl_verification']).to eq(true) end end @@ -38,6 +44,12 @@ describe API::API, 'ProjectHooks', api: true do get api("/projects/#{project.id}/hooks/#{hook.id}", user) expect(response.status).to eq(200) expect(json_response['url']).to eq(hook.url) + expect(json_response['issues_events']).to eq(hook.issues_events) + expect(json_response['push_events']).to eq(hook.push_events) + expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) + expect(json_response['tag_push_events']).to eq(hook.tag_push_events) + expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) end it "should return a 404 error if hook id is not available" do @@ -65,6 +77,13 @@ describe API::API, 'ProjectHooks', api: true do post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true end.to change {project.hooks.count}.by(1) expect(response.status).to eq(201) + expect(json_response['url']).to eq('http://example.com') + expect(json_response['issues_events']).to eq(true) + expect(json_response['push_events']).to eq(true) + expect(json_response['merge_requests_events']).to eq(false) + expect(json_response['tag_push_events']).to eq(false) + expect(json_response['note_events']).to eq(false) + expect(json_response['enable_ssl_verification']).to eq(true) end it "should return a 400 error if url not given" do @@ -84,6 +103,12 @@ describe API::API, 'ProjectHooks', api: true do url: 'http://example.org', push_events: false expect(response.status).to eq(200) expect(json_response['url']).to eq('http://example.org') + expect(json_response['issues_events']).to eq(hook.issues_events) + expect(json_response['push_events']).to eq(false) + expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) + expect(json_response['tag_push_events']).to eq(hook.tag_push_events) + expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) end it "should return 404 error if hook id not found" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 5bd8206b890..580bbec77d1 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -89,7 +89,7 @@ describe API::API, api: true do it 'returns projects in the correct order when ci_enabled_first parameter is passed' do [project, project2, project3].each{ |project| project.build_missing_services } - project2.gitlab_ci_service.update(active: true, token: "token", project_url: "http://ci.example.com/projects/1") + project2.gitlab_ci_service.update(active: true) get api('/projects', user), { ci_enabled_first: 'true' } expect(response.status).to eq(200) expect(json_response).to be_an Array diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 6d29a28580a..9aa60826f21 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -5,64 +5,57 @@ describe API::API, api: true do let(:user) { create(:user) } let(:project) {create(:project, creator_id: user.id, namespace: user.namespace) } - describe "POST /projects/:id/services/gitlab-ci" do - it "should update gitlab-ci settings" do - put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'secrettoken', project_url: "http://ci.example.com/projects/1" + Service.available_services_names.each do |service| + describe "PUT /projects/:id/services/#{service.dasherize}" do + include_context service - expect(response.status).to eq(200) + it "should update #{service} settings" do + put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs + + expect(response.status).to eq(200) + end + + it "should return if required fields missing" do + attrs = service_attrs + + required_attributes = service_attrs_list.select do |attr| + service_klass.validators_on(attr).any? do |v| + v.class == ActiveRecord::Validations::PresenceValidator + end + end + + if required_attributes.empty? + expected_code = 200 + else + attrs.delete(required_attributes.shuffle.first) + expected_code = 400 + end + + put api("/projects/#{project.id}/services/#{dashed_service}", user), attrs + + expect(response.status).to eq(expected_code) + end end - it "should return if required fields missing" do - put api("/projects/#{project.id}/services/gitlab-ci", user), project_url: "http://ci.example.com/projects/1", active: true + describe "DELETE /projects/:id/services/#{service.dasherize}" do + include_context service - expect(response.status).to eq(400) + it "should delete #{service}" do + delete api("/projects/#{project.id}/services/#{dashed_service}", user) + + expect(response.status).to eq(200) + expect(project.send(service_method).activated?).to be_falsey + end end - it "should return if the format of token is invalid" do - put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'token-with dashes and spaces%', project_url: "http://ci.example.com/projects/1", active: true + describe "GET /projects/:id/services/#{service.dasherize}" do + include_context service - expect(response.status).to eq(404) - end + it "should get #{service} settings" do + get api("/projects/#{project.id}/services/#{dashed_service}", user) - it "should return if the format of token is invalid" do - put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'token-with dashes and spaces%', project_url: "ftp://ci.example/projects/1", active: true - - expect(response.status).to eq(404) - end - end - - describe "DELETE /projects/:id/services/gitlab-ci" do - it "should update gitlab-ci settings" do - delete api("/projects/#{project.id}/services/gitlab-ci", user) - - expect(response.status).to eq(200) - expect(project.gitlab_ci_service).to be_nil - end - end - - describe 'PUT /projects/:id/services/hipchat' do - it 'should update hipchat settings' do - put api("/projects/#{project.id}/services/hipchat", user), - token: 'secret-token', room: 'test' - - expect(response.status).to eq(200) - expect(project.hipchat_service).not_to be_nil - end - - it 'should return if required fields missing' do - put api("/projects/#{project.id}/services/gitlab-ci", user), - token: 'secret-token', active: true - - expect(response.status).to eq(400) - end - end - - describe 'DELETE /projects/:id/services/hipchat' do - it 'should delete hipchat settings' do - delete api("/projects/#{project.id}/services/hipchat", user) - - expect(response.status).to eq(200) - expect(project.hipchat_service).to be_nil + expect(response.status).to eq(200) + end end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index f2aa369985e..d26a300ed82 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -7,6 +7,7 @@ describe API::API, api: true do let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } let(:email) { create(:email, user: user) } + let(:omniauth_user) { create(:omniauth_user) } describe "GET /users" do context "when unauthenticated" do @@ -58,6 +59,11 @@ describe API::API, api: true do expect(response.status).to eq(404) expect(json_response['message']).to eq('404 Not found') end + + it "should return a 404 if invalid ID" do + get api("/users/1ASDF", user) + expect(response.status).to eq(404) + end end describe "POST /users" do @@ -225,6 +231,19 @@ describe API::API, api: true do expect(user.reload.username).to eq(user.username) end + it "should update user's existing identity" do + put api("/users/#{omniauth_user.id}", admin), provider: 'ldapmain', extern_uid: '654321' + expect(response.status).to eq(200) + expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321') + end + + it 'should update user with new identity' do + put api("/users/#{user.id}", admin), provider: 'github', extern_uid: '67890' + expect(response.status).to eq(200) + expect(user.reload.identities.first.extern_uid).to eq('67890') + expect(user.reload.identities.first.provider).to eq('github') + end + it "should update admin status" do put api("/users/#{user.id}", admin), { admin: true } expect(response.status).to eq(200) @@ -257,6 +276,10 @@ describe API::API, api: true do expect(json_response['message']).to eq('404 Not found') end + it "should raise error for invalid ID" do + expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError) + end + it 'should return 400 error if user does not validate' do put api("/users/#{user.id}", admin), password: 'pass', @@ -319,6 +342,10 @@ describe API::API, api: true do post api("/users/#{user.id}/keys", admin), key_attrs end.to change{ user.keys.count }.by(1) end + + it "should raise error for invalid ID" do + expect{post api("/users/ASDF/keys", admin) }.to raise_error(ActionController::RoutingError) + end end describe 'GET /user/:uid/keys' do @@ -346,6 +373,11 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.first['title']).to eq(key.title) end + + it "should return 404 for invalid ID" do + get api("/users/ASDF/keys", admin) + expect(response.status).to eq(404) + end end end @@ -400,6 +432,10 @@ describe API::API, api: true do post api("/users/#{user.id}/emails", admin), email_attrs end.to change{ user.emails.count }.by(1) end + + it "should raise error for invalid ID" do + expect{post api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError) + end end describe 'GET /user/:uid/emails' do @@ -427,6 +463,10 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.first['email']).to eq(email.email) end + + it "should raise error for invalid ID" do + expect{put api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError) + end end end @@ -463,6 +503,10 @@ describe API::API, api: true do expect(response.status).to eq(404) expect(json_response['message']).to eq('404 Email Not Found') end + + it "should raise error for invalid ID" do + expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError) + end end end @@ -491,6 +535,10 @@ describe API::API, api: true do expect(response.status).to eq(404) expect(json_response['message']).to eq('404 User Not Found') end + + it "should raise error for invalid ID" do + expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError) + end end describe "GET /user" do @@ -553,6 +601,11 @@ describe API::API, api: true do expect(response.status).to eq(404) expect(json_response['message']).to eq('404 Not found') end + + it "should return 404 for invalid ID" do + get api("/users/keys/ASDF", admin) + expect(response.status).to eq(404) + end end describe "POST /user/keys" do @@ -608,6 +661,10 @@ describe API::API, api: true do delete api("/user/keys/#{key.id}") expect(response.status).to eq(401) end + + it "should raise error for invalid ID" do + expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError) + end end describe "GET /user/emails" do @@ -653,6 +710,11 @@ describe API::API, api: true do expect(response.status).to eq(404) expect(json_response['message']).to eq('404 Not found') end + + it "should return 404 for invalid ID" do + get api("/users/emails/ASDF", admin) + expect(response.status).to eq(404) + end end describe "POST /user/emails" do @@ -697,6 +759,10 @@ describe API::API, api: true do delete api("/user/emails/#{email.id}") expect(response.status).to eq(401) end + + it "should raise error for invalid ID" do + expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError) + end end describe 'PUT /user/:id/block' do @@ -748,5 +814,9 @@ describe API::API, api: true do expect(response.status).to eq(404) expect(json_response['message']).to eq('404 User Not Found') end + + it "should raise error for invalid ID" do + expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError) + end end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb new file mode 100644 index 00000000000..54c1d0199f6 --- /dev/null +++ b/spec/requests/ci/api/builds_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +describe Ci::API::API do + include ApiHelpers + + let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) } + let(:project) { FactoryGirl.create(:ci_project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + + before do + stub_ci_commit_to_return_yaml_file + end + + describe "Builds API for runners" do + let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") } + let(:shared_project) { FactoryGirl.create(:ci_project, name: "SharedProject") } + let(:shared_gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: shared_project) } + + before do + FactoryGirl.create :ci_runner_project, project_id: project.id, runner_id: runner.id + end + + describe "POST /builds/register" do + it "should start a build" do + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + commit.create_builds('master', false, nil) + build = commit.builds.first + + post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + + expect(response.status).to eq(201) + expect(json_response['sha']).to eq(build.sha) + expect(runner.reload.platform).to eq("darwin") + end + + it "should return 404 error if no pending build found" do + post ci_api("/builds/register"), token: runner.token + + expect(response.status).to eq(404) + end + + it "should return 404 error if no builds for specific runner" do + commit = FactoryGirl.create(:ci_commit, gl_project: shared_gl_project) + FactoryGirl.create(:ci_build, commit: commit, status: 'pending' ) + + post ci_api("/builds/register"), token: runner.token + + expect(response.status).to eq(404) + end + + it "should return 404 error if no builds for shared runner" do + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + FactoryGirl.create(:ci_build, commit: commit, status: 'pending' ) + + post ci_api("/builds/register"), token: shared_runner.token + + expect(response.status).to eq(404) + end + + it "returns options" do + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + commit.create_builds('master', false, nil) + + post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + + expect(response.status).to eq(201) + expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] }) + end + + it "returns variables" do + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + commit.create_builds('master', false, nil) + project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") + + post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + + expect(response.status).to eq(201) + expect(json_response["variables"]).to eq([ + { "key" => "DB_NAME", "value" => "postgres", "public" => true }, + { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, + ]) + end + + it "returns variables for triggers" do + trigger = FactoryGirl.create(:ci_trigger, project: project) + commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + + trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) + commit.create_builds('master', false, nil, trigger_request) + project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") + + post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + + expect(response.status).to eq(201) + expect(json_response["variables"]).to eq([ + { "key" => "DB_NAME", "value" => "postgres", "public" => true }, + { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, + { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false }, + ]) + end + end + + describe "PUT /builds/:id" do + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project)} + let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } + + it "should update a running build" do + build.run! + put ci_api("/builds/#{build.id}"), token: runner.token + expect(response.status).to eq(200) + end + + it 'Should not override trace information when no trace is given' do + build.run! + build.update!(trace: 'hello_world') + put ci_api("/builds/#{build.id}"), token: runner.token + expect(build.reload.trace).to eq 'hello_world' + end + end + end +end diff --git a/spec/requests/ci/api/commits_spec.rb b/spec/requests/ci/api/commits_spec.rb new file mode 100644 index 00000000000..6049135fd10 --- /dev/null +++ b/spec/requests/ci/api/commits_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Ci::API::API, 'Commits' do + include ApiHelpers + + let(:project) { FactoryGirl.create(:ci_project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } + + let(:options) do + { + project_token: project.token, + project_id: project.id + } + end + + describe "GET /commits" do + before { commit } + + it "should return commits per project" do + get ci_api("/commits"), options + + expect(response.status).to eq(200) + expect(json_response.count).to eq(1) + expect(json_response.first["project_id"]).to eq(project.id) + expect(json_response.first["sha"]).to eq(commit.sha) + end + end + + describe "POST /commits" do + let(:data) do + { + "before" => "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after" => "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref" => "refs/heads/master", + "commits" => [ + { + "id" => "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "message" => "Update Catalan translation to e38cb41.", + "timestamp" => "2011-12-12T14:27:31+02:00", + "url" => "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "author" => { + "name" => "Jordi Mallach", + "email" => "jordi@softcatala.org", + } + } + ] + } + end + + it "should create a build" do + post ci_api("/commits"), options.merge(data: data) + + expect(response.status).to eq(201) + expect(json_response['sha']).to eq("da1560886d4f094c3e6c9ef40349f7d38b5d27d7") + end + + it "should return 400 error if no data passed" do + post ci_api("/commits"), options + + expect(response.status).to eq(400) + expect(json_response['message']).to eq("400 (Bad request) \"data\" not given") + end + end +end diff --git a/spec/requests/ci/api/projects_spec.rb b/spec/requests/ci/api/projects_spec.rb new file mode 100644 index 00000000000..53f7f91cc1f --- /dev/null +++ b/spec/requests/ci/api/projects_spec.rb @@ -0,0 +1,266 @@ +require 'spec_helper' + +describe Ci::API::API do + include ApiHelpers + + let(:gitlab_url) { GitlabCi.config.gitlab_ci.url } + let(:user) { create(:user) } + let(:private_token) { user.private_token } + + let(:options) do + { + private_token: private_token, + url: gitlab_url + } + end + + before do + stub_gitlab_calls + end + + context "requests for scoped projects" do + # NOTE: These ids are tied to the actual projects on demo.gitlab.com + describe "GET /projects" do + let!(:project1) { FactoryGirl.create(:ci_project) } + let!(:project2) { FactoryGirl.create(:ci_project) } + + before do + project1.gl_project.team << [user, :developer] + project2.gl_project.team << [user, :developer] + end + + it "should return all projects on the CI instance" do + get ci_api("/projects"), options + expect(response.status).to eq(200) + expect(json_response.count).to eq(2) + expect(json_response.first["id"]).to eq(project1.id) + expect(json_response.last["id"]).to eq(project2.id) + end + end + + describe "GET /projects/owned" do + let!(:gl_project1) {FactoryGirl.create(:empty_project, namespace: user.namespace)} + let!(:gl_project2) {FactoryGirl.create(:empty_project, namespace: user.namespace)} + let!(:project1) { FactoryGirl.create(:ci_project, gl_project: gl_project1) } + let!(:project2) { FactoryGirl.create(:ci_project, gl_project: gl_project2) } + + before do + project1.gl_project.team << [user, :developer] + project2.gl_project.team << [user, :developer] + end + + it "should return all projects on the CI instance" do + get ci_api("/projects/owned"), options + + expect(response.status).to eq(200) + expect(json_response.count).to eq(2) + end + end + end + + describe "POST /projects/:project_id/webhooks" do + let!(:project) { FactoryGirl.create(:ci_project) } + + context "Valid Webhook URL" do + let!(:webhook) { { web_hook: "http://example.com/sth/1/ala_ma_kota" } } + + before do + options.merge!(webhook) + end + + it "should create webhook for specified project" do + project.gl_project.team << [user, :master] + post ci_api("/projects/#{project.id}/webhooks"), options + expect(response.status).to eq(201) + expect(json_response["url"]).to eq(webhook[:web_hook]) + end + + it "fails to create webhook for non existsing project" do + post ci_api("/projects/non-existant-id/webhooks"), options + expect(response.status).to eq(404) + end + + it "non-manager is not authorized" do + post ci_api("/projects/#{project.id}/webhooks"), options + expect(response.status).to eq(401) + end + end + + context "Invalid Webhook URL" do + let!(:webhook) { { web_hook: "ala_ma_kota" } } + + before do + options.merge!(webhook) + end + + it "fails to create webhook for not valid url" do + project.gl_project.team << [user, :master] + post ci_api("/projects/#{project.id}/webhooks"), options + expect(response.status).to eq(400) + end + end + + context "Missed web_hook parameter" do + it "fails to create webhook for not provided url" do + project.gl_project.team << [user, :master] + post ci_api("/projects/#{project.id}/webhooks"), options + expect(response.status).to eq(400) + end + end + end + + describe "GET /projects/:id" do + let!(:project) { FactoryGirl.create(:ci_project) } + + before do + project.gl_project.team << [user, :developer] + end + + context "with an existing project" do + it "should retrieve the project info" do + get ci_api("/projects/#{project.id}"), options + expect(response.status).to eq(200) + expect(json_response['id']).to eq(project.id) + end + end + + context "with a non-existing project" do + it "should return 404 error if project not found" do + get ci_api("/projects/non_existent_id"), options + expect(response.status).to eq(404) + end + end + end + + describe "PUT /projects/:id" do + let!(:project) { FactoryGirl.create(:ci_project) } + let!(:project_info) { { default_ref: "develop" } } + + before do + options.merge!(project_info) + end + + it "should update a specific project's information" do + project.gl_project.team << [user, :master] + put ci_api("/projects/#{project.id}"), options + expect(response.status).to eq(200) + expect(json_response["default_ref"]).to eq(project_info[:default_ref]) + end + + it "fails to update a non-existing project" do + put ci_api("/projects/non-existant-id"), options + expect(response.status).to eq(404) + end + + it "non-manager is not authorized" do + put ci_api("/projects/#{project.id}"), options + expect(response.status).to eq(401) + end + end + + describe "DELETE /projects/:id" do + let!(:project) { FactoryGirl.create(:ci_project) } + + it "should delete a specific project" do + project.gl_project.team << [user, :master] + delete ci_api("/projects/#{project.id}"), options + expect(response.status).to eq(200) + expect { project.reload }. + to raise_error(ActiveRecord::RecordNotFound) + end + + it "non-manager is not authorized" do + delete ci_api("/projects/#{project.id}"), options + expect(response.status).to eq(401) + end + + it "is getting not found error" do + delete ci_api("/projects/not-existing_id"), options + expect(response.status).to eq(404) + end + end + + describe "POST /projects" do + let(:gl_project) { FactoryGirl.create :empty_project } + let(:project_info) do + { + gitlab_id: gl_project.id + } + end + + let(:invalid_project_info) { {} } + + context "with valid project info" do + before do + options.merge!(project_info) + end + + it "should create a project with valid data" do + post ci_api("/projects"), options + expect(response.status).to eq(201) + expect(json_response['name']).to eq(gl_project.name_with_namespace) + end + end + + context "with invalid project info" do + before do + options.merge!(invalid_project_info) + end + + it "should error with invalid data" do + post ci_api("/projects"), options + expect(response.status).to eq(400) + end + end + + describe "POST /projects/:id/runners/:id" do + let(:project) { FactoryGirl.create(:ci_project) } + let(:runner) { FactoryGirl.create(:ci_runner) } + + it "should add the project to the runner" do + project.gl_project.team << [user, :master] + post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + expect(response.status).to eq(201) + + project.reload + expect(project.runners.first.id).to eq(runner.id) + end + + it "should fail if it tries to link a non-existing project or runner" do + post ci_api("/projects/#{project.id}/runners/non-existing"), options + expect(response.status).to eq(404) + + post ci_api("/projects/non-existing/runners/#{runner.id}"), options + expect(response.status).to eq(404) + end + + it "non-manager is not authorized" do + allow_any_instance_of(User).to receive(:can_manage_project?).and_return(false) + post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + expect(response.status).to eq(401) + end + end + + describe "DELETE /projects/:id/runners/:id" do + let(:project) { FactoryGirl.create(:ci_project) } + let(:runner) { FactoryGirl.create(:ci_runner) } + + it "should remove the project from the runner" do + project.gl_project.team << [user, :master] + post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + + expect(project.runners).to be_present + delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + expect(response.status).to eq(200) + + project.reload + expect(project.runners).to be_empty + end + + it "non-manager is not authorized" do + delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb new file mode 100644 index 00000000000..11dc089e1f5 --- /dev/null +++ b/spec/requests/ci/api/runners_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe Ci::API::API do + include ApiHelpers + include StubGitlabCalls + + before do + stub_gitlab_calls + end + + describe "GET /runners" do + let(:gitlab_url) { GitlabCi.config.gitlab_ci.url } + let(:private_token) { create(:user).private_token } + let(:options) do + { + private_token: private_token, + url: gitlab_url + } + end + + before do + 5.times { FactoryGirl.create(:ci_runner) } + end + + it "should retrieve a list of all runners" do + get ci_api("/runners", nil), options + expect(response.status).to eq(200) + expect(json_response.count).to eq(5) + expect(json_response.last).to have_key("id") + expect(json_response.last).to have_key("token") + end + end + + describe "POST /runners/register" do + describe "should create a runner if token provided" do + before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN } + + it { expect(response.status).to eq(201) } + end + + describe "should create a runner with description" do + before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, description: "server.hostname" } + + it { expect(response.status).to eq(201) } + it { expect(Ci::Runner.first.description).to eq("server.hostname") } + end + + describe "should create a runner with tags" do + before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, tag_list: "tag1, tag2" } + + it { expect(response.status).to eq(201) } + it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) } + end + + describe "should create a runner if project token provided" do + let(:project) { FactoryGirl.create(:ci_project) } + before { post ci_api("/runners/register"), token: project.token } + + it { expect(response.status).to eq(201) } + it { expect(project.runners.size).to eq(1) } + end + + it "should return 403 error if token is invalid" do + post ci_api("/runners/register"), token: 'invalid' + + expect(response.status).to eq(403) + end + + it "should return 400 error if no token" do + post ci_api("/runners/register") + + expect(response.status).to eq(400) + end + end + + describe "DELETE /runners/delete" do + let!(:runner) { FactoryGirl.create(:ci_runner) } + before { delete ci_api("/runners/delete"), token: runner.token } + + it { expect(response.status).to eq(200) } + it { expect(Ci::Runner.count).to eq(0) } + end +end diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb new file mode 100644 index 00000000000..93617fc4b3f --- /dev/null +++ b/spec/requests/ci/api/triggers_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe Ci::API::API do + include ApiHelpers + + describe 'POST /projects/:project_id/refs/:ref/trigger' do + let!(:trigger_token) { 'secure token' } + let!(:gl_project) { FactoryGirl.create(:project) } + let!(:project) { FactoryGirl.create(:ci_project, gl_project: gl_project) } + let!(:project2) { FactoryGirl.create(:ci_project) } + let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } + let(:options) do + { + token: trigger_token + } + end + + before do + stub_ci_commit_to_return_yaml_file + end + + context 'Handles errors' do + it 'should return bad request if token is missing' do + post ci_api("/projects/#{project.id}/refs/master/trigger") + expect(response.status).to eq(400) + end + + it 'should return not found if project is not found' do + post ci_api('/projects/0/refs/master/trigger'), options + expect(response.status).to eq(404) + end + + it 'should return unauthorized if token is for different project' do + post ci_api("/projects/#{project2.id}/refs/master/trigger"), options + expect(response.status).to eq(401) + end + end + + context 'Have a commit' do + let(:commit) { project.commits.last } + + it 'should create builds' do + post ci_api("/projects/#{project.id}/refs/master/trigger"), options + expect(response.status).to eq(201) + commit.builds.reload + expect(commit.builds.size).to eq(2) + end + + it 'should return bad request with no builds created if there\'s no commit for that ref' do + post ci_api("/projects/#{project.id}/refs/other-branch/trigger"), options + expect(response.status).to eq(400) + expect(json_response['message']).to eq('No builds created') + end + + context 'Validates variables' do + let(:variables) do + { 'TRIGGER_KEY' => 'TRIGGER_VALUE' } + end + + it 'should validate variables to be a hash' do + post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: 'value') + expect(response.status).to eq(400) + expect(json_response['message']).to eq('variables needs to be a hash') + end + + it 'should validate variables needs to be a map of key-valued strings' do + post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) }) + expect(response.status).to eq(400) + expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') + end + + it 'create trigger request with variables' do + post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: variables) + expect(response.status).to eq(201) + commit.builds.reload + expect(commit.builds.first.trigger_request.variables).to eq(variables) + end + end + end + end +end diff --git a/spec/requests/ci/builds_spec.rb b/spec/requests/ci/builds_spec.rb new file mode 100644 index 00000000000..f68116c52aa --- /dev/null +++ b/spec/requests/ci/builds_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe "Builds" do + before do + @commit = FactoryGirl.create :ci_commit + @build = FactoryGirl.create :ci_build, commit: @commit + end + + describe "GET /:project/builds/:id/status.json" do + before do + get status_ci_project_build_path(@commit.project, @build), format: :json + end + + it { expect(response.status).to eq(200) } + it { expect(response.body).to include(@build.sha) } + end +end diff --git a/spec/requests/ci/commits_spec.rb b/spec/requests/ci/commits_spec.rb new file mode 100644 index 00000000000..f43a3982d71 --- /dev/null +++ b/spec/requests/ci/commits_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe "Commits" do + before do + @commit = FactoryGirl.create :ci_commit + end + + describe "GET /:project/refs/:ref_name/commits/:id/status.json" do + before do + get status_ci_project_commits_path(@commit.project, @commit.sha), format: :json + end + + it { expect(response.status).to eq(200) } + it { expect(response.body).to include(@commit.sha) } + end +end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index dd045826692..dfa18f69e05 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -206,7 +206,7 @@ end # dashboard_merge_requests GET /dashboard/merge_requests(.:format) dashboard#merge_requests describe DashboardController, "routing" do it "to #index" do - expect(get("/dashboard")).to route_to('dashboard#show') + expect(get("/dashboard")).to route_to('dashboard/projects#index') end it "to #issues" do @@ -220,8 +220,8 @@ end # root / root#show describe RootController, 'routing' do - it 'to #show' do - expect(get('/')).to route_to('root#show') + it 'to #index' do + expect(get('/')).to route_to('root#index') end end diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb new file mode 100644 index 00000000000..e3a8fe9681b --- /dev/null +++ b/spec/services/ci/create_commit_service_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +module Ci + describe CreateCommitService do + let(:service) { CreateCommitService.new } + let(:project) { FactoryGirl.create(:ci_project) } + let(:user) { nil } + + before do + stub_ci_commit_to_return_yaml_file + end + + describe :execute do + context 'valid params' do + let(:commit) do + service.execute(project, user, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: [ { message: "Message" } ] + ) + end + + it { expect(commit).to be_kind_of(Commit) } + it { expect(commit).to be_valid } + it { expect(commit).to be_persisted } + it { expect(commit).to eq(project.commits.last) } + it { expect(commit.builds.first).to be_kind_of(Build) } + end + + context "skip tag if there is no build for it" do + it "creates commit if there is appropriate job" do + result = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: [ { message: "Message" } ] + ) + expect(result).to be_persisted + end + + it "creates commit if there is no appropriate job but deploy job has right ref setting" do + config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } }) + stub_ci_commit_yaml_file(config) + + result = service.execute(project, user, + ref: 'refs/heads/0_1', + before: '00000000', + after: '31das312', + commits: [ { message: "Message" } ] + ) + expect(result).to be_persisted + end + end + + it 'fails commits without .gitlab-ci.yml' do + stub_ci_commit_yaml_file(nil) + result = service.execute(project, user, + ref: 'refs/heads/0_1', + before: '00000000', + after: '31das312', + commits: [ { message: 'Message' } ] + ) + expect(result).to be_persisted + expect(result.builds.any?).to be_falsey + expect(result.status).to eq('failed') + end + + describe :ci_skip? do + let(:message) { "some message[ci skip]" } + + before do + allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } + end + + it "skips builds creation if there is [ci skip] tag in commit message" do + commits = [{ message: message }] + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.any?).to be false + expect(commit.status).to eq("skipped") + end + + it "does not skips builds creation if there is no [ci skip] tag in commit message" do + allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" } + + commits = [{ message: "some message" }] + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + + expect(commit.builds.first.name).to eq("staging") + end + + it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do + stub_ci_commit_yaml_file('invalid: file') + commits = [{ message: message }] + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.any?).to be false + expect(commit.status).to eq("skipped") + end + end + + it "skips build creation if there are already builds" do + allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml } + + commits = [{ message: "message" }] + commit = service.execute(project, user, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.count(:all)).to eq(2) + + commit = service.execute(project, user, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.count(:all)).to eq(2) + end + + it "creates commit with failed status if yaml is invalid" do + stub_ci_commit_yaml_file('invalid: file') + + commits = [{ message: "some message" }] + + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + + expect(commit.status).to eq("failed") + expect(commit.builds.any?).to be false + end + end + end +end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb new file mode 100644 index 00000000000..fcafae38644 --- /dev/null +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Ci::CreateTriggerRequestService do + let(:service) { Ci::CreateTriggerRequestService.new } + let(:gl_project) { create(:project) } + let(:project) { create(:ci_project, gl_project: gl_project) } + let(:trigger) { create(:ci_trigger, project: project) } + + before do + stub_ci_commit_to_return_yaml_file + end + + describe :execute do + context 'valid params' do + subject { service.execute(project, trigger, 'master') } + + it { expect(subject).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.builds.first).to be_kind_of(Ci::Build) } + end + + context 'no commit for ref' do + subject { service.execute(project, trigger, 'other-branch') } + + it { expect(subject).to be_nil } + end + + context 'no builds created' do + subject { service.execute(project, trigger, 'master') } + + before do + stub_ci_commit_yaml_file('{}') + FactoryGirl.create :ci_commit, gl_project: gl_project + end + + it { expect(subject).to be_nil } + end + end +end diff --git a/spec/services/ci/event_service_spec.rb b/spec/services/ci/event_service_spec.rb new file mode 100644 index 00000000000..1264e17ff5e --- /dev/null +++ b/spec/services/ci/event_service_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Ci::EventService do + let(:project) { FactoryGirl.create :ci_project } + let(:user) { double(username: "root", id: 1) } + + before do + Event.destroy_all + end + + describe :remove_project do + it "creates event" do + Ci::EventService.new.remove_project(user, project) + + expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been removed by root") + end + end + + describe :create_project do + it "creates event" do + Ci::EventService.new.create_project(user, project) + + expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been created by root") + end + end + + describe :change_project_settings do + it "creates event" do + Ci::EventService.new.change_project_settings(user, project) + + expect(Ci::Event.last.description).to eq("User \"root\" updated projects settings") + end + end +end diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb new file mode 100644 index 00000000000..d7242d684c6 --- /dev/null +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +module Ci + describe ImageForBuildService do + let(:service) { ImageForBuildService.new } + let(:project) { FactoryGirl.create(:ci_project) } + let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } + let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project, ref: 'master') } + let(:build) { FactoryGirl.create(:ci_build, commit: commit) } + + describe :execute do + before { build } + + context 'branch name' do + before { build.run! } + let(:image) { service.execute(project, ref: 'master') } + + it { expect(image).to be_kind_of(OpenStruct) } + it { expect(image.path.to_s).to include('public/ci/build-running.svg') } + it { expect(image.name).to eq('build-running.svg') } + end + + context 'unknown branch name' do + let(:image) { service.execute(project, ref: 'feature') } + + it { expect(image).to be_kind_of(OpenStruct) } + it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') } + it { expect(image.name).to eq('build-unknown.svg') } + end + + context 'commit sha' do + before { build.run! } + let(:image) { service.execute(project, sha: build.sha) } + + it { expect(image).to be_kind_of(OpenStruct) } + it { expect(image.path.to_s).to include('public/ci/build-running.svg') } + it { expect(image.name).to eq('build-running.svg') } + end + + context 'unknown commit sha' do + let(:image) { service.execute(project, sha: '0000000') } + + it { expect(image).to be_kind_of(OpenStruct) } + it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') } + it { expect(image.name).to eq('build-unknown.svg') } + end + end + end +end diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb new file mode 100644 index 00000000000..781764627ac --- /dev/null +++ b/spec/services/ci/register_build_service_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +module Ci + describe RegisterBuildService do + let!(:service) { RegisterBuildService.new } + let!(:gl_project) { FactoryGirl.create :empty_project } + let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit } + let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } + let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } + + before do + specific_runner.assign_to(gl_project.ensure_gitlab_ci_project) + end + + describe :execute do + context 'runner follow tag list' do + it "picks build with the same tag" do + pending_build.tag_list = ["linux"] + pending_build.save + specific_runner.tag_list = ["linux"] + expect(service.execute(specific_runner)).to eq(pending_build) + end + + it "does not pick build with different tag" do + pending_build.tag_list = ["linux"] + pending_build.save + specific_runner.tag_list = ["win32"] + expect(service.execute(specific_runner)).to be_falsey + end + + it "picks build without tag" do + expect(service.execute(specific_runner)).to eq(pending_build) + end + + it "does not pick build with tag" do + pending_build.tag_list = ["linux"] + pending_build.save + expect(service.execute(specific_runner)).to be_falsey + end + + it "pick build without tag" do + specific_runner.tag_list = ["win32"] + expect(service.execute(specific_runner)).to eq(pending_build) + end + end + + context 'allow shared runners' do + before do + gl_project.gitlab_ci_project.update(shared_runners_enabled: true) + end + + context 'shared runner' do + let(:build) { service.execute(shared_runner) } + + it { expect(build).to be_kind_of(Build) } + it { expect(build).to be_valid } + it { expect(build).to be_running } + it { expect(build.runner).to eq(shared_runner) } + end + + context 'specific runner' do + let(:build) { service.execute(specific_runner) } + + it { expect(build).to be_kind_of(Build) } + it { expect(build).to be_valid } + it { expect(build).to be_running } + it { expect(build.runner).to eq(specific_runner) } + end + end + + context 'disallow shared runners' do + context 'shared runner' do + let(:build) { service.execute(shared_runner) } + + it { expect(build).to be_nil } + end + + context 'specific runner' do + let(:build) { service.execute(specific_runner) } + + it { expect(build).to be_kind_of(Build) } + it { expect(build).to be_valid } + it { expect(build).to be_running } + it { expect(build.runner).to eq(specific_runner) } + end + end + end + end +end diff --git a/spec/services/ci/web_hook_service_spec.rb b/spec/services/ci/web_hook_service_spec.rb new file mode 100644 index 00000000000..aa48fcbcbfd --- /dev/null +++ b/spec/services/ci/web_hook_service_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Ci::WebHookService do + let(:project) { FactoryGirl.create :ci_project } + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let(:build) { FactoryGirl.create :ci_build, commit: commit } + let(:hook) { FactoryGirl.create :ci_web_hook, project: project } + + describe :execute do + it "should execute successfully" do + stub_request(:post, hook.url).to_return(status: 200) + expect(Ci::WebHookService.new.build_end(build)).to be_truthy + end + end + + context 'build_data' do + it "contains all needed fields" do + expect(build_data(build)).to include( + :build_id, + :project_id, + :ref, + :build_status, + :build_started_at, + :build_finished_at, + :before_sha, + :project_name, + :gitlab_url, + :build_name + ) + end + end + + def build_data(build) + Ci::WebHookService.new.send :build_data, build + end +end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 007a9eed192..7756b973ecd 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -99,5 +99,15 @@ describe EventCreateService do expect { service.close_milestone(milestone, user) }.to change { Event.count } end end + + describe :destroy_mr do + let(:milestone) { create(:milestone) } + + it { expect(service.destroy_milestone(milestone, user)).to be_truthy } + + it "should create new event" do + expect { service.destroy_milestone(milestone, user) }.to change { Event.count } + end + end end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 7b564d34d7b..7483f51de03 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -35,5 +35,19 @@ describe MergeRequests::MergeService do expect(note.note).to include 'Status changed to merged' end end + + context "error handling" do + let(:service) { MergeRequests::MergeService.new(project, user, {}) } + + it 'saves error if there is an exception' do + allow(service).to receive(:repository).and_raise("error") + + allow(service).to receive(:execute_hooks) + + service.execute(merge_request, 'Awesome message') + + expect(merge_request.merge_error).to eq("Something went wrong during merge") + end + end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 8865335d0d1..520140917aa 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -427,15 +427,15 @@ describe NotificationService do should_email(@u_watcher.id) should_email(@u_participating.id) should_not_email(@u_disabled.id) - notification.project_was_moved(project) + notification.project_was_moved(project, "gitlab/gitlab") end def should_email(user_id) - expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id) + expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab") end def should_not_email(user_id) - expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id) + expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab") end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 66cdfd5d758..25277f07482 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -17,6 +17,14 @@ describe Projects::CreateService do expect(project.services).not_to be_empty end + it 'creates labels on Project creation if there are templates' do + Label.create(title: "bug", template: true) + project = create_project(@user, @opts) + project.reload + + expect(project.labels).not_to be_empty + end + context 'user namespace' do before do @project = create_project(@user, @opts) @@ -88,6 +96,17 @@ describe Projects::CreateService do expect(project.saved?).to be(true) end end + + context 'repository creation' do + it 'should synchronously create the repository' do + expect_any_instance_of(Project).to receive(:create_repository) + + project = create_project(@user, @opts) + expect(project).to be_valid + expect(project.owner).to eq(@user) + expect(project.namespace).to eq(@user.namespace) + end + end end def create_project(user, opts) diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb new file mode 100644 index 00000000000..f12e09c58c3 --- /dev/null +++ b/spec/services/projects/download_service_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Projects::DownloadService do + describe 'File service' do + before do + @user = create :user + @project = create :project, creator_id: @user.id, namespace: @user.namespace + end + + context 'for a URL that is not on whitelist' do + before do + url = 'https://code.jquery.com/jquery-2.1.4.min.js' + @link_to_file = download_file(@project, url) + end + + it { expect(@link_to_file).to eq(nil) } + end + + context 'for URLs that are on the whitelist' do + before do + sham_rack_app = ShamRack.at('mycompany.fogbugz.com').stub + sham_rack_app.register_resource('/rails_sample.jpg', File.read(Rails.root + 'spec/fixtures/rails_sample.jpg'), 'image/jpg') + sham_rack_app.register_resource('/doc_sample.txt', File.read(Rails.root + 'spec/fixtures/doc_sample.txt'), 'text/plain') + end + + after do + ShamRack.unmount_all + end + + context 'an image file' do + before do + url = 'http://mycompany.fogbugz.com/rails_sample.jpg' + @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("/#{@project.path_with_namespace}") } + 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 + before do + url = 'http://mycompany.fogbugz.com/doc_sample.txt' + @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("/#{@project.path_with_namespace}") } + 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 + + def download_file(repository, url) + Projects::DownloadService.new(repository, url).execute + end +end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index c04e842c67e..65a8c81204d 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -28,8 +28,7 @@ describe Projects::ForkService do context 'fork project failure' do it "fails due to transaction failure" do @to_project = fork_project(@from_project, @to_user, false) - expect(@to_project.errors).not_to be_empty - expect(@to_project.errors[:base]).to include("Failed to fork repository via gitlab-shell") + expect(@to_project.import_failed?) end end @@ -44,13 +43,10 @@ describe Projects::ForkService do end context 'GitLab CI is enabled' do - it "calls fork registrator for CI" do - @from_project.build_missing_services - @from_project.gitlab_ci_service.update_attributes(active: true) - - expect(ForkRegistrationWorker).to receive(:perform_async) - - fork_project(@from_project, @to_user) + it "fork and enable CI for fork" do + @from_project.enable_ci + @to_project = fork_project(@from_project, @to_user) + expect(@to_project.gitlab_ci?).to be_truthy end end end @@ -100,7 +96,7 @@ describe Projects::ForkService do end def fork_project(from_project, user, fork_success = true, params = {}) - allow_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(fork_success) + allow(RepositoryForkWorker).to receive(:perform_async).and_return(fork_success) Projects::ForkService.new(from_project, user, params).execute end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d0f1873ee2d..2be13bb3e6a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' +require 'benchmark/ips' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -28,7 +29,11 @@ RSpec.configure do |config| config.include LoginHelpers, type: :feature config.include LoginHelpers, type: :request config.include StubConfiguration + config.include RelativeUrl, type: feature config.include TestEnv + config.include StubGitlabCalls + config.include StubGitlabData + config.include BenchmarkMatchers, benchmark: true config.infer_spec_type_from_file_location! config.raise_errors_for_deprecations! @@ -38,4 +43,8 @@ RSpec.configure do |config| end end +FactoryGirl::SyntaxRunner.class_eval do + include RSpec::Mocks::ExampleMethods +end + ActiveRecord::Migration.maintain_test_schema! diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index f63322776d4..1b3cafb497c 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -28,6 +28,17 @@ module ApiHelpers "&private_token=#{user.private_token}" : "") end + def ci_api(path, user = nil) + "/ci/api/v1/#{path}" + + + # Normalize query string + (path.index('?') ? '' : '?') + + + # Append private_token if given a User object + (user.respond_to?(:private_token) ? + "&private_token=#{user.private_token}" : "") + end + def json_response @_json_response ||= JSON.parse(response.body) end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index 25df3896291..56ce88abb48 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -58,6 +58,6 @@ module FilterSpecHelper # Shortcut to Rails' auto-generated routes helpers, to avoid including the # module def urls - Rails.application.routes.url_helpers + Gitlab::Application.routes.url_helpers end end diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml new file mode 100644 index 00000000000..3482145404e --- /dev/null +++ b/spec/support/gitlab_stubs/gitlab_ci.yml @@ -0,0 +1,63 @@ +image: ruby:2.1 +services: + - postgres + +before_script: + - gem install bundler + - bundle install + - bundle exec rake db:create + +variables: + DB_NAME: postgres + +types: + - test + - deploy + - notify + +rspec: + script: "rake spec" + tags: + - ruby + - postgres + only: + - branches + +spinach: + script: "rake spinach" + allow_failure: true + tags: + - ruby + - mysql + except: + - tags + +staging: + script: "cap deploy stating" + type: deploy + tags: + - capistrano + - debian + except: + - stable + +production: + type: deploy + script: + - cap deploy production + - cap notify + tags: + - capistrano + - debian + only: + - master + - /^deploy-.*$/ + +dockerhub: + type: notify + script: "curl http://dockerhub/URL" + tags: + - ruby + - postgres + only: + - branches diff --git a/spec/support/gitlab_stubs/project_8.json b/spec/support/gitlab_stubs/project_8.json new file mode 100644 index 00000000000..f0a9fce859c --- /dev/null +++ b/spec/support/gitlab_stubs/project_8.json @@ -0,0 +1,45 @@ +{ + "id":8, + "description":"ssh access and repository management app for GitLab", + "default_branch":"master", + "public":false, + "visibility_level":0, + "ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git", + "http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git", + "web_url":"http://demo.gitlab.com/gitlab/gitlab-shell", + "owner": { + "id":4, + "name":"GitLab", + "created_at":"2012-12-21T13:03:05Z" + }, + "name":"gitlab-shell", + "name_with_namespace":"GitLab / gitlab-shell", + "path":"gitlab-shell", + "path_with_namespace":"gitlab/gitlab-shell", + "issues_enabled":true, + "merge_requests_enabled":true, + "wall_enabled":false, + "wiki_enabled":true, + "snippets_enabled":false, + "created_at":"2013-03-20T13:28:53Z", + "last_activity_at":"2013-11-30T00:11:17Z", + "namespace":{ + "created_at":"2012-12-21T13:03:05Z", + "description":"Self hosted Git management software", + "id":4, + "name":"GitLab", + "owner_id":1, + "path":"gitlab", + "updated_at":"2013-03-20T13:29:13Z" + }, + "permissions":{ + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +} \ No newline at end of file diff --git a/spec/support/gitlab_stubs/project_8_hooks.json b/spec/support/gitlab_stubs/project_8_hooks.json new file mode 100644 index 00000000000..93d51406d63 --- /dev/null +++ b/spec/support/gitlab_stubs/project_8_hooks.json @@ -0,0 +1 @@ +[{}] diff --git a/spec/support/gitlab_stubs/projects.json b/spec/support/gitlab_stubs/projects.json new file mode 100644 index 00000000000..ca42c14c5d8 --- /dev/null +++ b/spec/support/gitlab_stubs/projects.json @@ -0,0 +1 @@ +[{"id":3,"description":"GitLab is open source software to collaborate on code. Create projects and repositories, manage access and do code reviews.","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlabhq.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlabhq.git","web_url":"http://demo.gitlab.com/gitlab/gitlabhq","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlabhq","name_with_namespace":"GitLab / gitlabhq","path":"gitlabhq","path_with_namespace":"gitlab/gitlabhq","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:34Z","last_activity_at":"2013-12-02T19:10:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":4,"description":"Component of GitLab CI. Web application","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-ci.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-ci.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-ci","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-ci","name_with_namespace":"GitLab / gitlab-ci","path":"gitlab-ci","path_with_namespace":"gitlab/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:50Z","last_activity_at":"2013-11-28T19:26:54Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":5,"description":"","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-recipes.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-recipes.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-recipes","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-recipes","name_with_namespace":"GitLab / gitlab-recipes","path":"gitlab-recipes","path_with_namespace":"gitlab/gitlab-recipes","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:07:02Z","last_activity_at":"2013-12-02T13:54:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":8,"description":"ssh access and repository management app for GitLab","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-shell","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-shell","name_with_namespace":"GitLab / gitlab-shell","path":"gitlab-shell","path_with_namespace":"gitlab/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-03-20T13:28:53Z","last_activity_at":"2013-11-30T00:11:17Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":9,"description":null,"default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab_git.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab_git.git","web_url":"http://demo.gitlab.com/gitlab/gitlab_git","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab_git","name_with_namespace":"GitLab / gitlab_git","path":"gitlab_git","path_with_namespace":"gitlab/gitlab_git","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-04-28T19:15:08Z","last_activity_at":"2013-12-02T13:07:13Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":10,"description":"ultra lite authorization library http://randx.github.com/six/\\r\\n ","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/six.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/six.git","web_url":"http://demo.gitlab.com/sandbox/six","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Six","name_with_namespace":"Sandbox / Six","path":"six","path_with_namespace":"sandbox/six","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:45:02Z","last_activity_at":"2013-11-29T11:30:56Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":11,"description":"Simple HTML5 Charts using the